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