@@ -29,6 +29,7 @@ import (
29
29
"runtime"
30
30
"strings"
31
31
"syscall"
32
+ "time"
32
33
33
34
"github.com/pkg/errors"
34
35
"github.com/rs/xid"
@@ -291,6 +292,7 @@ func (e *Executor) Config(loadDotEnv bool) error {
291
292
}
292
293
293
294
func (e * Executor ) CleanupTemporaryDirectories () {
295
+ go cleanupStaleTemporaryDirectories (e .Logger )
294
296
if e .iniDir != "" {
295
297
os .RemoveAll (e .iniDir )
296
298
}
@@ -299,6 +301,82 @@ func (e *Executor) CleanupTemporaryDirectories() {
299
301
}
300
302
}
301
303
304
+ // The Symfony CLI used to leak temporary directories until v5.10.8. The bug is
305
+ // fixed but because directories names are random they are not going to be
306
+ // reused and thus are not going to be cleaned up. And because they might be
307
+ // in-use by running servers we can't simply delete the parent directory. This
308
+ // is why we make our best to find the oldest directories and remove then,
309
+ // cleaning the directory little by little.
310
+ func cleanupStaleTemporaryDirectories (mainLogger zerolog.Logger ) {
311
+ parentDirectory := filepath .Join (util .GetHomeDir (), "tmp" )
312
+ mainLogger = mainLogger .With ().Str ("dir" , parentDirectory ).Logger ()
313
+
314
+ if len (parentDirectory ) < 6 {
315
+ mainLogger .Warn ().Msg ("temporary dir path looks too short" )
316
+ return
317
+ }
318
+
319
+ mainLogger .Debug ().Msg ("Starting temporary directory cleanup..." )
320
+ dir , err := os .Open (parentDirectory )
321
+ if err != nil {
322
+ mainLogger .Warn ().Err (err ).Msg ("Failed to open temporary directory" )
323
+ return
324
+ }
325
+ defer dir .Close ()
326
+
327
+ // the duration after which we consider temporary directories as
328
+ // stale and can be removed
329
+ cutoff := time .Now ().Add (- 7 * 24 * time .Hour )
330
+
331
+ for {
332
+ // we might have a lof of entries so we need to work in batches
333
+ entries , err := dir .Readdirnames (30 )
334
+ if err == io .EOF {
335
+ mainLogger .Debug ().Msg ("Cleaning is done..." )
336
+ return
337
+ }
338
+ if err != nil {
339
+ mainLogger .Warn ().Err (err ).Msg ("Failed to read entries" )
340
+ return
341
+ }
342
+
343
+ for _ , entry := range entries {
344
+ logger := mainLogger .With ().Str ("entry" , entry ).Logger ()
345
+
346
+ // we generate temporary directory names with
347
+ // `xid.New().String()` which is always 20 char long
348
+ if len (entry ) != 20 {
349
+ logger .Debug ().Msg ("found an entry that is not from us" )
350
+ continue
351
+ } else if _ , err := xid .FromString (entry ); err != nil {
352
+ logger .Debug ().Err (err ).Msg ("found an entry that is not from us" )
353
+ continue
354
+ }
355
+
356
+ entryPath := filepath .Join (parentDirectory , entry )
357
+ file , err := os .Open (entryPath )
358
+ if err != nil {
359
+ logger .Warn ().Err (err ).Msg ("failed to read entry" )
360
+ continue
361
+ } else if fi , err := file .Stat (); err != nil {
362
+ logger .Warn ().Err (err ).Msg ("failed to read entry" )
363
+ continue
364
+ } else if ! fi .IsDir () {
365
+ logger .Warn ().Err (err ).Msg ("entry is not a directory" )
366
+ continue
367
+ } else if fi .ModTime ().After (cutoff ) {
368
+ logger .Debug ().Any ("cutoff" , cutoff ).Msg ("entry is more recent than cutoff, keeping it for now" )
369
+ continue
370
+ }
371
+
372
+ logger .Debug ().Str ("entry" , entry ).Msg ("entry matches the criterias, removing it" )
373
+ if err := os .RemoveAll (entryPath ); err != nil {
374
+ logger .Warn ().Err (err ).Msg ("failed to remove entry" )
375
+ }
376
+ }
377
+ }
378
+ }
379
+
302
380
// Find composer depending on the configuration
303
381
func (e * Executor ) findComposer (extraBin string ) (string , error ) {
304
382
if scriptDir , err := e .DetectScriptDir (); err == nil {
0 commit comments