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