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