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 File fastDir;
60
61 private final String fastMode;
62
63 private final boolean verbose;
64
65 private Log log;
66
67
68
69
70
71
72
73
74
75
76 Cleaner(MavenSession session, final Log log, boolean verbose, File fastDir, String fastMode) {
77 this.session = session;
78
79
80 this.log = log;
81 this.fastDir = fastDir;
82 this.fastMode = fastMode;
83 this.verbose = verbose;
84 }
85
86
87
88
89
90
91
92
93
94
95
96
97
98 public void delete(
99 File basedir, Selector selector, boolean followSymlinks, boolean failOnError, boolean retryOnError)
100 throws IOException {
101 if (!basedir.isDirectory()) {
102 if (!basedir.exists()) {
103 if (log.isDebugEnabled()) {
104 log.debug("Skipping non-existing directory " + basedir);
105 }
106 return;
107 }
108 throw new IOException("Invalid base directory " + basedir);
109 }
110
111 if (log.isInfoEnabled()) {
112 log.info("Deleting " + basedir + (selector != null ? " (" + selector + ")" : ""));
113 }
114
115 File file = followSymlinks ? basedir : basedir.getCanonicalFile();
116
117 if (selector == null && !followSymlinks && fastDir != null && session != null) {
118
119 if (fastDelete(file)) {
120 return;
121 }
122 }
123
124 delete(file, "", selector, followSymlinks, failOnError, retryOnError);
125 }
126
127 private boolean fastDelete(File baseDirFile) {
128 Path baseDir = baseDirFile.toPath();
129 Path fastDir = this.fastDir.toPath();
130
131 if (fastDir.toAbsolutePath().startsWith(baseDir.toAbsolutePath())) {
132 try {
133 String prefix = baseDir.getFileName().toString() + ".";
134 Path tmpDir = Files.createTempDirectory(baseDir.getParent(), prefix);
135 try {
136 Files.move(baseDir, tmpDir, StandardCopyOption.REPLACE_EXISTING);
137 if (session != null) {
138 session.getRepositorySession().getData().set(LAST_DIRECTORY_TO_DELETE, baseDir.toFile());
139 }
140 baseDir = tmpDir;
141 } catch (IOException e) {
142 Files.delete(tmpDir);
143 throw e;
144 }
145 } catch (IOException e) {
146 if (log.isDebugEnabled()) {
147 log.debug("Unable to fast delete directory: ", e);
148 }
149 return false;
150 }
151 }
152
153 try {
154 if (!Files.isDirectory(fastDir)) {
155 Files.createDirectories(fastDir);
156 }
157 } catch (IOException e) {
158 if (log.isDebugEnabled()) {
159 log.debug(
160 "Unable to fast delete directory as the path " + fastDir
161 + " does not point to a directory or cannot be created: ",
162 e);
163 }
164 return false;
165 }
166
167 try {
168 Path tmpDir = Files.createTempDirectory(fastDir, "");
169 Path dstDir = tmpDir.resolve(baseDir.getFileName());
170
171
172
173
174 Files.move(baseDir, dstDir, StandardCopyOption.ATOMIC_MOVE);
175 BackgroundCleaner.delete(this, tmpDir.toFile(), fastMode);
176 return true;
177 } catch (IOException e) {
178 if (log.isDebugEnabled()) {
179 log.debug("Unable to fast delete directory: ", e);
180 }
181 return false;
182 }
183 }
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200 private Result delete(
201 File file,
202 String pathname,
203 Selector selector,
204 boolean followSymlinks,
205 boolean failOnError,
206 boolean retryOnError)
207 throws IOException {
208 Result result = new Result();
209
210 boolean isDirectory = file.isDirectory();
211
212 if (isDirectory) {
213 if (selector == null || selector.couldHoldSelected(pathname)) {
214 if (followSymlinks || !isSymbolicLink(file.toPath())) {
215 File canonical = followSymlinks ? file : file.getCanonicalFile();
216 String[] filenames = canonical.list();
217 if (filenames != null) {
218 String prefix = pathname.length() > 0 ? pathname + File.separatorChar : "";
219 for (int i = filenames.length - 1; i >= 0; i--) {
220 String filename = filenames[i];
221 File child = new File(canonical, filename);
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 (file.exists()) {
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(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 (log.isWarnEnabled()) {
303 log.warn("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 static class BackgroundCleaner extends Thread {
326
327 private static final int NEW = 0;
328 private static final int RUNNING = 1;
329 private static final int STOPPED = 2;
330 private static BackgroundCleaner instance;
331 private final Deque<File> filesToDelete = new ArrayDeque<>();
332 private final Cleaner cleaner;
333 private final String fastMode;
334 private int status = NEW;
335
336 private BackgroundCleaner(Cleaner cleaner, File dir, String fastMode) {
337 super("mvn-background-cleaner");
338 this.cleaner = cleaner;
339 this.fastMode = fastMode;
340 init(cleaner.fastDir, dir);
341 }
342
343 public static void delete(Cleaner cleaner, File dir, String fastMode) {
344 synchronized (BackgroundCleaner.class) {
345 if (instance == null || !instance.doDelete(dir)) {
346 instance = new BackgroundCleaner(cleaner, dir, fastMode);
347 }
348 }
349 }
350
351 static void sessionEnd() {
352 synchronized (BackgroundCleaner.class) {
353 if (instance != null) {
354 instance.doSessionEnd();
355 }
356 }
357 }
358
359 public void run() {
360 while (true) {
361 File basedir = pollNext();
362 if (basedir == null) {
363 break;
364 }
365 try {
366 cleaner.delete(basedir, "", null, false, false, true);
367 } catch (IOException e) {
368
369 }
370 }
371 }
372
373 synchronized void init(File fastDir, File dir) {
374 if (fastDir.isDirectory()) {
375 File[] children = fastDir.listFiles();
376 if (children != null && children.length > 0) {
377 for (File child : children) {
378 doDelete(child);
379 }
380 }
381 }
382 doDelete(dir);
383 }
384
385 synchronized File pollNext() {
386 File basedir = filesToDelete.poll();
387 if (basedir == null) {
388 if (cleaner.session != null) {
389 SessionData data = cleaner.session.getRepositorySession().getData();
390 File lastDir = (File) data.get(LAST_DIRECTORY_TO_DELETE);
391 if (lastDir != null) {
392 data.set(LAST_DIRECTORY_TO_DELETE, null);
393 return lastDir;
394 }
395 }
396 status = STOPPED;
397 notifyAll();
398 }
399 return basedir;
400 }
401
402 synchronized boolean doDelete(File dir) {
403 if (status == STOPPED) {
404 return false;
405 }
406 filesToDelete.add(dir);
407 if (status == NEW && FAST_MODE_BACKGROUND.equals(fastMode)) {
408 status = RUNNING;
409 notifyAll();
410 start();
411 }
412 wrapExecutionListener();
413 return true;
414 }
415
416
417
418
419
420
421
422
423 private void wrapExecutionListener() {
424 ExecutionListener executionListener = cleaner.session.getRequest().getExecutionListener();
425 if (executionListener == null
426 || !Proxy.isProxyClass(executionListener.getClass())
427 || !(Proxy.getInvocationHandler(executionListener) instanceof SpyInvocationHandler)) {
428 ExecutionListener listener = (ExecutionListener) Proxy.newProxyInstance(
429 ExecutionListener.class.getClassLoader(),
430 new Class[] {ExecutionListener.class},
431 new SpyInvocationHandler(executionListener));
432 cleaner.session.getRequest().setExecutionListener(listener);
433 }
434 }
435
436 synchronized void doSessionEnd() {
437 if (status != STOPPED) {
438 if (status == NEW) {
439 start();
440 }
441 if (!FAST_MODE_DEFER.equals(fastMode)) {
442 try {
443 if (cleaner.log.isInfoEnabled()) {
444 cleaner.log.info("Waiting for background file deletion");
445 }
446 while (status != STOPPED) {
447 wait();
448 }
449 } catch (InterruptedException e) {
450
451 }
452 }
453 }
454 }
455 }
456
457 static class SpyInvocationHandler implements InvocationHandler {
458 private final ExecutionListener delegate;
459
460 SpyInvocationHandler(ExecutionListener delegate) {
461 this.delegate = delegate;
462 }
463
464 @Override
465 public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
466 if ("sessionEnded".equals(method.getName())) {
467 BackgroundCleaner.sessionEnd();
468 }
469 if (delegate != null) {
470 return method.invoke(delegate, args);
471 }
472 return null;
473 }
474 }
475 }