Skip to content

Commit 07d5ab0

Browse files
authored
Merge pull request #571 from lutovich/fix-mvn-resource-usage
Fix resource leak in Maven plugin
2 parents 2135539 + 2395492 commit 07d5ab0

File tree

10 files changed

+217
-41
lines changed

10 files changed

+217
-41
lines changed

CHANGES.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,10 @@ We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format (
1414
* `LineEnding.GIT_ATTRIBUTES` now creates a policy whose serialized state can be relocated from one machine to another. No user-visible change, but paves the way for remote build cache support in Gradle. ([#621](https://github.com/diffplug/spotless/pull/621))
1515
### Added
1616
* `prettier` will now autodetect the parser (and formatter) to use based on the filename, unless you override this using `config` or `configFile` with the option `parser` or `filepath`. ([#620](https://github.com/diffplug/spotless/pull/620))
17+
### Changed
18+
* **BREAKING** `FileSignature` can no longer sign folders, only files. Signatures are now based only on filename (not path), size, and a content hash. It throws an error if a signature is attempted on a folder or on multiple files with different paths but the same filename - it never breaks silently. This change does not break any of Spotless' internal logic, so it is unlikely to affect any of Spotless' consumers either. ([#571](https://github.com/diffplug/spotless/pull/571))
19+
* This change allows the maven plugin to cache classloaders across subprojects when loading config resources from the classpath (fixes [#559](https://github.com/diffplug/spotless/issues/559)).
20+
* This change also allows the gradle plugin to work with the remote buildcache (fixes [#280](https://github.com/diffplug/spotless/issues/280)).
1721

1822
## [1.34.1] - 2020-06-17
1923
### Changed
@@ -162,7 +166,7 @@ We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format (
162166
* Updated default npm package version of `prettier` from 1.13.4 to 1.16.4
163167
* Updated default npm package version of internally used typescript package from 2.9.2 to 3.3.3 and tslint package from 5.1.0 to 5.12.0 (both used by `tsfmt`)
164168
* Updated default eclipse-wtp from 4.7.3a to 4.7.3b ([#371](https://github.com/diffplug/spotless/pull/371)).
165-
* Configured `buìld-scan` plugin in build ([#356](https://github.com/diffplug/spotless/pull/356)).
169+
* Configured `build-scan` plugin in build ([#356](https://github.com/diffplug/spotless/pull/356)).
166170
* Runs on every CI build automatically.
167171
* Users need to opt-in on their local machine.
168172
* Default behavior of XML formatter changed to ignore external URIs ([#369](https://github.com/diffplug/spotless/issues/369)).

lib/src/main/java/com/diffplug/spotless/FileSignature.java

Lines changed: 97 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -17,20 +17,26 @@
1717

1818
import static com.diffplug.spotless.MoreIterables.toNullHostileList;
1919
import static com.diffplug.spotless.MoreIterables.toSortedSet;
20+
import static java.util.Comparator.comparing;
2021

2122
import java.io.File;
23+
import java.io.FileInputStream;
2224
import java.io.IOException;
25+
import java.io.InputStream;
2326
import java.io.Serializable;
27+
import java.security.MessageDigest;
2428
import java.util.Arrays;
2529
import java.util.Collection;
2630
import java.util.Collections;
31+
import java.util.HashMap;
2732
import java.util.List;
33+
import java.util.Map;
2834

2935
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
3036

3137
/** Computes a signature for any needed files. */
3238
public final class FileSignature implements Serializable {
33-
private static final long serialVersionUID = 1L;
39+
private static final long serialVersionUID = 2L;
3440

3541
/*
3642
* Transient because not needed to uniquely identify a FileSignature instance, and also because
@@ -39,10 +45,7 @@ public final class FileSignature implements Serializable {
3945
*/
4046
@SuppressFBWarnings("SE_TRANSIENT_FIELD_NOT_RESTORED")
4147
private final transient List<File> files;
42-
43-
private final String[] filenames;
44-
private final long[] filesizes;
45-
private final long[] lastModified;
48+
private final Sig[] signatures;
4649

4750
/** Method has been renamed to {@link FileSignature#signAsSet}.
4851
* In case no sorting and removal of duplicates is required,
@@ -77,21 +80,30 @@ public static FileSignature signAsSet(File... files) throws IOException {
7780

7881
/** Creates file signature insensitive to the order of the files. */
7982
public static FileSignature signAsSet(Iterable<File> files) throws IOException {
80-
return new FileSignature(toSortedSet(files));
83+
List<File> natural = toSortedSet(files);
84+
List<File> onNameOnly = toSortedSet(files, comparing(File::getName));
85+
if (natural.size() != onNameOnly.size()) {
86+
StringBuilder builder = new StringBuilder();
87+
builder.append("For these files:\n");
88+
for (File file : files) {
89+
builder.append(" " + file.getAbsolutePath() + "\n");
90+
}
91+
builder.append("a caching signature is being generated, which will be based only on their\n");
92+
builder.append("names, not their full path (foo.txt, not C:\folder\foo.txt). Unexpectedly,\n");
93+
builder.append("you have two files with different paths, but the same names. You must\n");
94+
builder.append("rename one of them so that all files have unique names.");
95+
throw new IllegalArgumentException(builder.toString());
96+
}
97+
return new FileSignature(onNameOnly);
8198
}
8299

83100
private FileSignature(final List<File> files) throws IOException {
84-
this.files = files;
85-
86-
filenames = new String[this.files.size()];
87-
filesizes = new long[this.files.size()];
88-
lastModified = new long[this.files.size()];
101+
this.files = validateInputFiles(files);
102+
this.signatures = new Sig[this.files.size()];
89103

90104
int i = 0;
91105
for (File file : this.files) {
92-
filenames[i] = file.getCanonicalPath();
93-
filesizes[i] = file.length();
94-
lastModified[i] = file.lastModified();
106+
signatures[i] = cache.sign(file);
95107
++i;
96108
}
97109
}
@@ -120,6 +132,77 @@ public static String pathUnixToNative(String pathUnix) {
120132
return LineEnding.nativeIsWin() ? pathUnix.replace('/', '\\') : pathUnix;
121133
}
122134

135+
private static List<File> validateInputFiles(List<File> files) {
136+
for (File file : files) {
137+
if (!file.isFile()) {
138+
throw new IllegalArgumentException(
139+
"File signature can only be created for existing regular files, given: "
140+
+ file);
141+
}
142+
}
143+
return files;
144+
}
145+
146+
/**
147+
* It is very common for a given set of files to be "signed" many times. For example,
148+
* the jars which constitute any given formatter live in a central cache, but will be signed
149+
* over and over. To save this I/O, we maintain a cache, invalidated by lastModified time.
150+
*/
151+
static final Cache cache = new Cache();
152+
153+
private static final class Cache {
154+
Map<String, Sig> cache = new HashMap<>();
155+
156+
synchronized Sig sign(File fileInput) throws IOException {
157+
String canonicalPath = fileInput.getCanonicalPath();
158+
Sig sig = cache.computeIfAbsent(canonicalPath, ThrowingEx.<String, Sig> wrap(p -> {
159+
MessageDigest digest = MessageDigest.getInstance("SHA-256");
160+
File file = new File(p);
161+
// calculate the size and content hash of the file
162+
long size = 0;
163+
byte[] buf = new byte[1024];
164+
long lastModified;
165+
try (InputStream input = new FileInputStream(file)) {
166+
lastModified = file.lastModified();
167+
int numRead;
168+
while ((numRead = input.read(buf)) != -1) {
169+
size += numRead;
170+
digest.update(buf, 0, numRead);
171+
}
172+
}
173+
return new Sig(file.getName(), size, digest.digest(), lastModified);
174+
}));
175+
long lastModified = fileInput.lastModified();
176+
if (sig.lastModified != lastModified) {
177+
cache.remove(canonicalPath);
178+
return sign(fileInput);
179+
} else {
180+
return sig;
181+
}
182+
}
183+
}
184+
185+
@SuppressFBWarnings("SE_TRANSIENT_FIELD_NOT_RESTORED")
186+
private static final class Sig implements Serializable {
187+
private static final long serialVersionUID = 6727302747168655222L;
188+
189+
@SuppressWarnings("unused")
190+
final String name;
191+
@SuppressWarnings("unused")
192+
final long size;
193+
@SuppressWarnings("unused")
194+
final byte[] hash;
195+
/** transient because state should be transferable from machine to machine. */
196+
final transient long lastModified;
197+
198+
Sig(String name, long size, byte[] hash, long lastModified) {
199+
this.name = name;
200+
this.size = size;
201+
this.hash = hash;
202+
this.lastModified = lastModified;
203+
}
204+
}
205+
123206
/** Asserts that child is a subpath of root. and returns the subpath. */
124207
public static String subpath(String root, String child) {
125208
if (child.startsWith(root)) {

lib/src/main/java/com/diffplug/spotless/MoreIterables.java

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2016 DiffPlug
2+
* Copyright 2016-2020 DiffPlug
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -20,6 +20,7 @@
2020
import java.util.ArrayList;
2121
import java.util.Collection;
2222
import java.util.Collections;
23+
import java.util.Comparator;
2324
import java.util.Iterator;
2425
import java.util.List;
2526

@@ -37,18 +38,23 @@ static <T> List<T> toNullHostileList(Iterable<T> input) {
3738
return shallowCopy;
3839
}
3940

40-
/** Sorts "raw" and removes duplicates, throwing on null elements. */
41+
/** Sorts "raw" using {@link Comparator#naturalOrder()} and removes duplicates, throwing on null elements. */
4142
static <T extends Comparable<T>> List<T> toSortedSet(Iterable<T> raw) {
43+
return toSortedSet(raw, Comparator.naturalOrder());
44+
}
45+
46+
/** Sorts "raw" and removes duplicates, throwing on null elements. */
47+
static <T> List<T> toSortedSet(Iterable<T> raw, Comparator<T> comparator) {
4248
List<T> toBeSorted = toNullHostileList(raw);
4349
// sort it
44-
Collections.sort(toBeSorted);
50+
Collections.sort(toBeSorted, comparator);
4551
// remove any duplicates (normally there won't be any)
4652
if (toBeSorted.size() > 1) {
4753
Iterator<T> iter = toBeSorted.iterator();
4854
T last = iter.next();
4955
while (iter.hasNext()) {
5056
T next = iter.next();
51-
if (next.compareTo(last) == 0) {
57+
if (comparator.compare(next, last) == 0) {
5258
iter.remove();
5359
} else {
5460
last = next;
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
/*
2+
* Copyright 2020 DiffPlug
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.diffplug.spotless.npm;
17+
18+
import java.io.File;
19+
20+
class NodeServerLayout {
21+
22+
private final File nodeModulesDir;
23+
private final File packageJsonFile;
24+
private final File serveJsFile;
25+
26+
NodeServerLayout(File buildDir, String stepName) {
27+
this.nodeModulesDir = new File(buildDir, "spotless-node-modules-" + stepName);
28+
this.packageJsonFile = new File(nodeModulesDir, "package.json");
29+
this.serveJsFile = new File(nodeModulesDir, "serve.js");
30+
}
31+
32+
File nodeModulesDir() {
33+
return nodeModulesDir;
34+
}
35+
36+
File packageJsonFile() {
37+
return packageJsonFile;
38+
}
39+
40+
File serveJsFile() {
41+
return serveJsFile;
42+
}
43+
}

lib/src/main/java/com/diffplug/spotless/npm/NpmFormatterStepStateBase.java

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ abstract class NpmFormatterStepStateBase implements Serializable {
4444
private static final long serialVersionUID = 1460749955865959948L;
4545

4646
@SuppressWarnings("unused")
47-
private final FileSignature nodeModulesSignature;
47+
private final FileSignature packageJsonSignature;
4848

4949
@SuppressFBWarnings("SE_TRANSIENT_FIELD_NOT_RESTORED")
5050
public final transient File nodeModulesDir;
@@ -56,22 +56,26 @@ abstract class NpmFormatterStepStateBase implements Serializable {
5656

5757
private final String stepName;
5858

59-
protected NpmFormatterStepStateBase(String stepName, NpmConfig npmConfig, File buildDir, @Nullable File npm) throws IOException {
59+
protected NpmFormatterStepStateBase(String stepName, NpmConfig npmConfig, File buildDir,
60+
@Nullable File npm) throws IOException {
6061
this.stepName = requireNonNull(stepName);
6162
this.npmConfig = requireNonNull(npmConfig);
6263
this.npmExecutable = resolveNpm(npm);
6364

64-
this.nodeModulesDir = prepareNodeServer(buildDir);
65-
this.nodeModulesSignature = FileSignature.signAsList(this.nodeModulesDir);
65+
NodeServerLayout layout = prepareNodeServer(buildDir);
66+
this.nodeModulesDir = layout.nodeModulesDir();
67+
this.packageJsonSignature = FileSignature.signAsList(layout.packageJsonFile());
6668
}
6769

68-
private File prepareNodeServer(File buildDir) throws IOException {
69-
File targetDir = new File(buildDir, "spotless-node-modules-" + stepName);
70-
NpmResourceHelper.assertDirectoryExists(targetDir);
71-
NpmResourceHelper.writeUtf8StringToFile(targetDir, "package.json", this.npmConfig.getPackageJsonContent());
72-
NpmResourceHelper.writeUtf8StringToFile(targetDir, "serve.js", this.npmConfig.getServeScriptContent());
73-
runNpmInstall(targetDir);
74-
return targetDir;
70+
private NodeServerLayout prepareNodeServer(File buildDir) throws IOException {
71+
NodeServerLayout layout = new NodeServerLayout(buildDir, stepName);
72+
NpmResourceHelper.assertDirectoryExists(layout.nodeModulesDir());
73+
NpmResourceHelper.writeUtf8StringToFile(layout.packageJsonFile(),
74+
this.npmConfig.getPackageJsonContent());
75+
NpmResourceHelper
76+
.writeUtf8StringToFile(layout.serveJsFile(), this.npmConfig.getServeScriptContent());
77+
runNpmInstall(layout.nodeModulesDir());
78+
return layout;
7579
}
7680

7781
private void runNpmInstall(File npmProjectDir) throws IOException {

lib/src/main/java/com/diffplug/spotless/npm/NpmResourceHelper.java

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,8 @@ private NpmResourceHelper() {
2828
// no instance required
2929
}
3030

31-
static void writeUtf8StringToFile(File targetDir, String fileName, String stringToWrite) throws IOException {
32-
File packageJsonFile = new File(targetDir, fileName);
33-
Files.write(packageJsonFile.toPath(), stringToWrite.getBytes(StandardCharsets.UTF_8));
31+
static void writeUtf8StringToFile(File file, String stringToWrite) throws IOException {
32+
Files.write(file.toPath(), stringToWrite.getBytes(StandardCharsets.UTF_8));
3433
}
3534

3635
static void writeUtf8StringToOutputStream(String stringToWrite, OutputStream outputStream) throws IOException {

plugin-gradle/CHANGES.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format (
66

77
## [Unreleased]
88
### Added
9+
* Full support for the Gradle buildcache - previously only supported local, now supports remote too. Fixes [#566](https://github.com/diffplug/spotless/issues/566) and [#280](https://github.com/diffplug/spotless/issues/280), via changes in [#621](https://github.com/diffplug/spotless/pull/621) and [#571](https://github.com/diffplug/spotless/pull/571).
910
* `prettier` will now autodetect the parser (and formatter) to use based on the filename, unless you override this using `config()` or `configFile()` with the option `parser` or `filepath`. ([#620](https://github.com/diffplug/spotless/pull/620))
1011
### Fixed
1112
* LineEndings.GIT_ATTRIBUTES is now a bit more efficient, and paves the way for remote build cache support in Gradle. ([#621](https://github.com/diffplug/spotless/pull/621))

plugin-maven/CHANGES.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@ We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format (
44

55
## [Unreleased]
66
### Added
7-
* `prettier` will now autodetect the parser (and formatter) to use based on the filename, unless you override this using `config` or `configFile` with the option `parser` or `filepath`. ([#620](https://github.com/diffplug/spotless/pull/620))
7+
* `prettier` will now autodetect the parser (and formatter) to use based on the filename, unless you override this using `config` or `configFile` with the option `parser` or `filepath` ([#620](https://github.com/diffplug/spotless/pull/620)).
8+
* Huge speed improvement for multi-module projects thanks to improved cross-project classloader caching ([#571](https://github.com/diffplug/spotless/pull/571), fixes [#559](https://github.com/diffplug/spotless/issues/559)).
89

910
## [1.31.3] - 2020-06-17
1011
### Changed

plugin-maven/src/main/java/com/diffplug/spotless/maven/FileLocator.java

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2016 DiffPlug
2+
* Copyright 2016-2020 DiffPlug
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -16,10 +16,13 @@
1616
package com.diffplug.spotless.maven;
1717

1818
import static com.diffplug.common.base.Strings.isNullOrEmpty;
19+
import static java.nio.charset.StandardCharsets.UTF_8;
1920

2021
import java.io.File;
22+
import java.security.MessageDigest;
23+
import java.security.NoSuchAlgorithmException;
24+
import java.util.Base64;
2125
import java.util.Objects;
22-
import java.util.UUID;
2326

2427
import org.codehaus.plexus.resource.ResourceManager;
2528
import org.codehaus.plexus.resource.loader.FileResourceCreationException;
@@ -63,16 +66,29 @@ public File locateFile(String path) {
6366
}
6467
}
6568

66-
private static String tmpOutputFileName(String path) {
67-
String extension = FileUtils.extension(path);
68-
return TMP_RESOURCE_FILE_PREFIX + UUID.randomUUID() + '.' + extension;
69-
}
70-
7169
public File getBaseDir() {
7270
return baseDir;
7371
}
7472

7573
public File getBuildDir() {
7674
return buildDir;
7775
}
76+
77+
private static String tmpOutputFileName(String path) {
78+
String extension = FileUtils.extension(path);
79+
byte[] pathHash = hash(path);
80+
String pathBase64 = Base64.getEncoder().encodeToString(pathHash);
81+
return TMP_RESOURCE_FILE_PREFIX + pathBase64 + '.' + extension;
82+
}
83+
84+
private static byte[] hash(String value) {
85+
MessageDigest messageDigest;
86+
try {
87+
messageDigest = MessageDigest.getInstance("SHA-256");
88+
} catch (NoSuchAlgorithmException e) {
89+
throw new IllegalStateException("SHA-256 digest algorithm not available", e);
90+
}
91+
messageDigest.update(value.getBytes(UTF_8));
92+
return messageDigest.digest();
93+
}
7894
}

0 commit comments

Comments
 (0)