1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19 package org.apache.maven.plugins.clean;
20
21 import java.io.File;
22 import java.io.IOException;
23 import java.nio.file.Files;
24 import java.nio.file.LinkOption;
25 import java.nio.file.Path;
26 import java.nio.file.StandardCopyOption;
27 import java.nio.file.attribute.BasicFileAttributes;
28 import java.util.ArrayDeque;
29 import java.util.Deque;
30 import java.util.function.BiConsumer;
31 import java.util.function.Consumer;
32 import java.util.stream.Stream;
33
34 import org.apache.maven.api.Event;
35 import org.apache.maven.api.EventType;
36 import org.apache.maven.api.Listener;
37 import org.apache.maven.api.Session;
38 import org.apache.maven.api.SessionData;
39 import org.apache.maven.api.plugin.Log;
40 import org.codehaus.plexus.util.Os;
41
42 import static org.apache.maven.plugins.clean.CleanMojo.FAST_MODE_BACKGROUND;
43 import static org.apache.maven.plugins.clean.CleanMojo.FAST_MODE_DEFER;
44
45
46
47
48
49
50 class Cleaner {
51
52 private static final boolean ON_WINDOWS = Os.isFamily(Os.FAMILY_WINDOWS);
53
54 private static final SessionData.Key<Path> LAST_DIRECTORY_TO_DELETE =
55 SessionData.key(Path.class, Cleaner.class.getName() + ".lastDirectoryToDelete");
56
57
58
59
60 private final Session session;
61
62 private final Logger logDebug;
63
64 private final Logger logInfo;
65
66 private final Logger logVerbose;
67
68 private final Logger logWarn;
69
70 private final Path fastDir;
71
72 private final String fastMode;
73
74
75
76
77
78
79
80
81
82
83 Cleaner(Session session, Log log, boolean verbose, Path fastDir, String fastMode) {
84 logDebug = (log == null || !log.isDebugEnabled()) ? null : logger(log::debug, log::debug);
85
86 logInfo = (log == null || !log.isInfoEnabled()) ? null : logger(log::info, log::info);
87
88 logWarn = (log == null || !log.isWarnEnabled()) ? null : logger(log::warn, log::warn);
89
90 logVerbose = verbose ? logInfo : logDebug;
91
92 this.session = session;
93 this.fastDir = fastDir;
94 this.fastMode = fastMode;
95 }
96
97 private Logger logger(Consumer<CharSequence> l1, BiConsumer<CharSequence, Throwable> l2) {
98 return new Logger() {
99 @Override
100 public void log(CharSequence message) {
101 l1.accept(message);
102 }
103
104 @Override
105 public void log(CharSequence message, Throwable t) {
106 l2.accept(message, t);
107 }
108 };
109 }
110
111
112
113
114
115
116
117
118
119
120
121
122
123 public void delete(
124 Path basedir, Selector selector, boolean followSymlinks, boolean failOnError, boolean retryOnError)
125 throws IOException {
126 if (!Files.isDirectory(basedir)) {
127 if (!Files.exists(basedir)) {
128 if (logDebug != null) {
129 logDebug.log("Skipping non-existing directory " + basedir);
130 }
131 return;
132 }
133 throw new IOException("Invalid base directory " + basedir);
134 }
135
136 if (logInfo != null) {
137 logInfo.log("Deleting " + basedir + (selector != null ? " (" + selector + ")" : ""));
138 }
139
140 Path file = followSymlinks ? basedir : getCanonicalPath(basedir);
141
142 if (selector == null && !followSymlinks && fastDir != null && session != null) {
143
144 if (fastDelete(file)) {
145 return;
146 }
147 }
148
149 delete(file, "", selector, followSymlinks, failOnError, retryOnError);
150 }
151
152 private boolean fastDelete(Path baseDir) {
153 Path fastDir = this.fastDir;
154
155 if (fastDir.toAbsolutePath().startsWith(baseDir.toAbsolutePath())) {
156 try {
157 String prefix = baseDir.getFileName().toString() + ".";
158 Path tmpDir = Files.createTempDirectory(baseDir.getParent(), prefix);
159 try {
160 Files.move(baseDir, tmpDir, StandardCopyOption.REPLACE_EXISTING);
161 if (session != null) {
162 session.getData().set(LAST_DIRECTORY_TO_DELETE, baseDir);
163 }
164 baseDir = tmpDir;
165 } catch (IOException e) {
166 Files.delete(tmpDir);
167 throw e;
168 }
169 } catch (IOException e) {
170 if (logDebug != null) {
171 logDebug.log("Unable to fast delete directory", e);
172 }
173 return false;
174 }
175 }
176
177 try {
178 if (!Files.isDirectory(fastDir)) {
179 Files.createDirectories(fastDir);
180 }
181 } catch (IOException e) {
182 if (logDebug != null) {
183 logDebug.log(
184 "Unable to fast delete directory as the path " + fastDir
185 + " does not point to a directory or cannot be created",
186 e);
187 }
188 return false;
189 }
190
191 try {
192 Path tmpDir = Files.createTempDirectory(fastDir, "");
193 Path dstDir = tmpDir.resolve(baseDir.getFileName());
194
195
196
197
198 Files.move(baseDir, dstDir, StandardCopyOption.ATOMIC_MOVE);
199 BackgroundCleaner.delete(this, tmpDir, fastMode);
200 return true;
201 } catch (IOException e) {
202 if (logDebug != null) {
203 logDebug.log("Unable to fast delete directory", e);
204 }
205 return false;
206 }
207 }
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224 private Result delete(
225 Path file,
226 String pathname,
227 Selector selector,
228 boolean followSymlinks,
229 boolean failOnError,
230 boolean retryOnError)
231 throws IOException {
232 Result result = new Result();
233
234 boolean isDirectory = Files.isDirectory(file);
235
236 if (isDirectory) {
237 if (selector == null || selector.couldHoldSelected(pathname)) {
238 final boolean isSymlink = isSymbolicLink(file);
239 Path canonical = followSymlinks ? file : getCanonicalPath(file);
240 if (followSymlinks || !isSymlink) {
241 String prefix = !pathname.isEmpty() ? pathname + File.separatorChar : "";
242 try (Stream<Path> children = Files.list(canonical)) {
243 for (Path child : children.toList()) {
244 result.update(delete(
245 child,
246 prefix + child.getFileName(),
247 selector,
248 followSymlinks,
249 failOnError,
250 retryOnError));
251 }
252 }
253 } else if (logDebug != null) {
254 logDebug.log("Not recursing into symlink " + file);
255 }
256 } else if (logDebug != null) {
257 logDebug.log("Not recursing into directory without included files " + file);
258 }
259 }
260
261 if (!result.excluded && (selector == null || selector.isSelected(pathname))) {
262 if (logVerbose != null) {
263 if (isDirectory) {
264 logVerbose.log("Deleting directory " + file);
265 } else if (Files.exists(file)) {
266 logVerbose.log("Deleting file " + file);
267 } else {
268 logVerbose.log("Deleting dangling symlink " + file);
269 }
270 }
271 result.failures += delete(file, failOnError, retryOnError);
272 } else {
273 result.excluded = true;
274 }
275
276 return result;
277 }
278
279 private static Path getCanonicalPath(Path path) {
280 try {
281 return path.toRealPath();
282 } catch (IOException e) {
283 return getCanonicalPath(path.getParent()).resolve(path.getFileName());
284 }
285 }
286
287 private boolean isSymbolicLink(Path path) throws IOException {
288 BasicFileAttributes attrs = Files.readAttributes(path, BasicFileAttributes.class, LinkOption.NOFOLLOW_LINKS);
289 return attrs.isSymbolicLink()
290
291 || (attrs.isDirectory() && attrs.isOther());
292 }
293
294
295
296
297
298
299
300
301
302
303
304 private int delete(Path file, boolean failOnError, boolean retryOnError) throws IOException {
305 IOException failure = delete(file);
306 if (failure != null) {
307
308 if (retryOnError) {
309 if (ON_WINDOWS) {
310
311 System.gc();
312 }
313
314 final int[] delays = {50, 250, 750};
315 for (int delay : delays) {
316 try {
317 Thread.sleep(delay);
318 } catch (InterruptedException e) {
319 throw new IOException(e);
320 }
321 failure = delete(file);
322 if (failure == null) {
323 break;
324 }
325 }
326 }
327
328 if (Files.exists(file)) {
329 if (failOnError) {
330 throw new IOException("Failed to delete " + file, failure);
331 } else {
332 if (logWarn != null) {
333 logWarn.log("Failed to delete " + file, failure);
334 }
335 return 1;
336 }
337 }
338 }
339
340 return 0;
341 }
342
343 private static IOException delete(Path file) {
344 try {
345 Files.deleteIfExists(file);
346 } catch (IOException e) {
347 return e;
348 }
349 return null;
350 }
351
352 private static class Result {
353
354 private int failures;
355
356 private boolean excluded;
357
358 public void update(Result result) {
359 failures += result.failures;
360 excluded |= result.excluded;
361 }
362 }
363
364 private interface Logger {
365
366 void log(CharSequence message);
367
368 void log(CharSequence message, Throwable t);
369 }
370
371 private static class BackgroundCleaner extends Thread {
372
373 private static BackgroundCleaner instance;
374
375 private final Deque<Path> filesToDelete = new ArrayDeque<>();
376
377 private final Cleaner cleaner;
378
379 private final String fastMode;
380
381 private static final int NEW = 0;
382 private static final int RUNNING = 1;
383 private static final int STOPPED = 2;
384
385 private int status = NEW;
386
387 public static void delete(Cleaner cleaner, Path dir, String fastMode) {
388 synchronized (BackgroundCleaner.class) {
389 if (instance == null || !instance.doDelete(dir)) {
390 instance = new BackgroundCleaner(cleaner, dir, fastMode);
391 }
392 }
393 }
394
395 static void sessionEnd() {
396 synchronized (BackgroundCleaner.class) {
397 if (instance != null) {
398 instance.doSessionEnd();
399 }
400 }
401 }
402
403 private BackgroundCleaner(Cleaner cleaner, Path dir, String fastMode) {
404 super("mvn-background-cleaner");
405 this.cleaner = cleaner;
406 this.fastMode = fastMode;
407 init(cleaner.fastDir, dir);
408 }
409
410 public void run() {
411 while (true) {
412 Path basedir = pollNext();
413 if (basedir == null) {
414 break;
415 }
416 try {
417 cleaner.delete(basedir, "", null, false, false, true);
418 } catch (IOException e) {
419
420 }
421 }
422 }
423
424 synchronized void init(Path fastDir, Path dir) {
425 if (Files.isDirectory(fastDir)) {
426 try {
427 try (Stream<Path> children = Files.list(fastDir)) {
428 children.forEach(this::doDelete);
429 }
430 } catch (IOException e) {
431 throw new RuntimeException(e);
432 }
433 }
434 doDelete(dir);
435 }
436
437 synchronized Path pollNext() {
438 Path basedir = filesToDelete.poll();
439 if (basedir == null) {
440 if (cleaner.session != null) {
441 SessionData data = cleaner.session.getData();
442 Path lastDir = data.get(LAST_DIRECTORY_TO_DELETE);
443 if (lastDir != null) {
444 data.set(LAST_DIRECTORY_TO_DELETE, null);
445 return lastDir;
446 }
447 }
448 status = STOPPED;
449 notifyAll();
450 }
451 return basedir;
452 }
453
454 synchronized boolean doDelete(Path dir) {
455 if (status == STOPPED) {
456 return false;
457 }
458 filesToDelete.add(dir);
459 if (status == NEW && FAST_MODE_BACKGROUND.equals(fastMode)) {
460 status = RUNNING;
461 notifyAll();
462 start();
463 }
464 wrapExecutionListener();
465 return true;
466 }
467
468
469
470
471
472
473
474
475 private void wrapExecutionListener() {
476 synchronized (CleanerListener.class) {
477 if (cleaner.session.getListeners().stream().noneMatch(l -> l instanceof CleanerListener)) {
478 cleaner.session.registerListener(new CleanerListener());
479 }
480 }
481 }
482
483 synchronized void doSessionEnd() {
484 if (status != STOPPED) {
485 if (status == NEW) {
486 start();
487 }
488 if (!FAST_MODE_DEFER.equals(fastMode)) {
489 try {
490 if (cleaner.logInfo != null) {
491 cleaner.logInfo.log("Waiting for background file deletion");
492 }
493 while (status != STOPPED) {
494 wait();
495 }
496 } catch (InterruptedException e) {
497
498 }
499 }
500 }
501 }
502 }
503
504 static class CleanerListener implements Listener {
505 @Override
506 public void onEvent(Event event) {
507 if (event.getType() == EventType.SESSION_ENDED) {
508 BackgroundCleaner.sessionEnd();
509 }
510 }
511 }
512 }