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