package system import ( "os" "syscall" "time" "github.com/containers/storage/pkg/mount" "github.com/pkg/errors" ) // EnsureRemoveAll wraps `os.RemoveAll` to check for specific errors that can // often be remedied. // Only use `EnsureRemoveAll` if you really want to make every effort to remove // a directory. // // Because of the way `os.Remove` (and by extension `os.RemoveAll`) works, there // can be a race between reading directory entries and then actually attempting // to remove everything in the directory. // These types of errors do not need to be returned since it's ok for the dir to // be gone we can just retry the remove operation. // // This should not return a `os.ErrNotExist` kind of error under any circumstances func EnsureRemoveAll(dir string) error { notExistErr := make(map[string]bool) // track retries exitOnErr := make(map[string]int) maxRetry := 5 // Attempt to unmount anything beneath this dir first mount.RecursiveUnmount(dir) for { err := os.RemoveAll(dir) if err == nil { return err } pe, ok := err.(*os.PathError) if !ok { return err } if os.IsNotExist(err) { if notExistErr[pe.Path] { return err } notExistErr[pe.Path] = true // There is a race where some subdir can be removed but after the parent // dir entries have been read. // So the path could be from `os.Remove(subdir)` // If the reported non-existent path is not the passed in `dir` we // should just retry, but otherwise return with no error. if pe.Path == dir { return nil } continue } if pe.Err != syscall.EBUSY { return err } if mounted, _ := mount.Mounted(pe.Path); mounted { if e := mount.Unmount(pe.Path); e != nil { if mounted, _ := mount.Mounted(pe.Path); mounted { return errors.Wrapf(e, "error while removing %s", dir) } } } if exitOnErr[pe.Path] == maxRetry { return err } exitOnErr[pe.Path]++ time.Sleep(100 * time.Millisecond) } }