| // Copyright 2015 The Vanadium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style |
| // license that can be found in the LICENSE file. |
| |
| package impl |
| |
| import ( |
| "fmt" |
| "os" |
| "path/filepath" |
| "strings" |
| "time" |
| |
| "v.io/v23/context" |
| "v.io/v23/services/device" |
| "v.io/v23/verror" |
| "v.io/x/ref/services/device/internal/errors" |
| ) |
| |
| // This file contains the various routines that the device manager uses |
| // to tidy up its persisted but no longer necessary state. |
| |
| const aboutOneDay = time.Hour * 24 |
| |
| func oldEnoughToTidy(fi os.FileInfo, now time.Time) bool { |
| return fi.ModTime().Add(aboutOneDay).Before(now) |
| } |
| |
| // AutomaticTidyingInterval defaults to 1 day. |
| // Settable for tests. |
| var AutomaticTidyingInterval = time.Hour * 24 |
| |
| func shouldDelete(idir, suffix string, now time.Time) (bool, error) { |
| fi, err := os.Stat(filepath.Join(idir, suffix)) |
| if err != nil { |
| return false, err |
| } |
| |
| return oldEnoughToTidy(fi, now), nil |
| } |
| |
| // Exposed for replacability in tests. |
| var MockableNow = time.Now |
| |
| // shouldDeleteInstallation returns true if the tidying policy holds |
| // for this installation. |
| func shouldDeleteInstallation(idir string, now time.Time) (bool, error) { |
| return shouldDelete(idir, device.InstallationStateUninstalled.String(), now) |
| } |
| |
| // shouldDeleteInstance returns true if the tidying policy holds |
| // that the instance should be deleted. |
| func shouldDeleteInstance(idir string, now time.Time) (bool, error) { |
| return shouldDelete(idir, device.InstanceStateDeleted.String(), now) |
| } |
| |
| type pthError struct { |
| pth string |
| err error |
| } |
| |
| func pruneDeletedInstances(ctx *context.T, root string, now time.Time) error { |
| paths, err := filepath.Glob(filepath.Join(root, "app*", "installation*", "instances", "instance*")) |
| if err != nil { |
| return err |
| } |
| |
| allerrors := make([]pthError, 0) |
| |
| for _, pth := range paths { |
| state, err := getInstanceState(pth) |
| if err != nil { |
| allerrors = append(allerrors, pthError{pth, err}) |
| continue |
| } |
| if state != device.InstanceStateDeleted { |
| continue |
| } |
| |
| shouldDelete, err := shouldDeleteInstance(pth, now) |
| if err != nil { |
| allerrors = append(allerrors, pthError{pth, err}) |
| continue |
| } |
| |
| if shouldDelete { |
| if err := suidHelper.deleteFileTree(ctx, pth, nil, nil); err != nil { |
| allerrors = append(allerrors, pthError{pth, err}) |
| } |
| } |
| } |
| return processErrors(ctx, allerrors) |
| } |
| |
| func processErrors(ctx *context.T, allerrors []pthError) error { |
| if len(allerrors) > 0 { |
| errormessages := make([]string, 0, len(allerrors)) |
| for _, ep := range allerrors { |
| errormessages = append(errormessages, fmt.Sprintf("path: %s failed: %v", ep.pth, ep.err)) |
| } |
| return verror.New(errors.ErrOperationFailed, ctx, "Some older instances could not be deleted: %s", strings.Join(errormessages, ", ")) |
| } |
| return nil |
| } |
| |
| func pruneUninstalledInstallations(ctx *context.T, root string, now time.Time) error { |
| // Read all the Uninstalled installations into a map. |
| installationPaths, err := filepath.Glob(filepath.Join(root, "app*", "installation*")) |
| if err != nil { |
| return err |
| } |
| pruneCandidates := make(map[string]struct{}, len(installationPaths)) |
| for _, p := range installationPaths { |
| state, err := getInstallationState(p) |
| if err != nil { |
| return err |
| } |
| |
| if state != device.InstallationStateUninstalled { |
| continue |
| } |
| |
| pruneCandidates[p] = struct{}{} |
| } |
| |
| instancePaths, err := filepath.Glob(filepath.Join(root, "app*", "installation*", "instances", "instance*", "installation")) |
| if err != nil { |
| return err |
| } |
| |
| allerrors := make([]pthError, 0) |
| |
| // Filter out installations that are still owned by an instance. Note |
| // that pruneUninstalledInstallations runs after |
| // pruneDeletedInstances so that freshly-pruned Instances will not |
| // retain the Installation. |
| for _, idir := range instancePaths { |
| installPath, err := os.Readlink(idir) |
| if err != nil { |
| allerrors = append(allerrors, pthError{idir, err}) |
| continue |
| } |
| |
| if _, ok := pruneCandidates[installPath]; ok { |
| delete(pruneCandidates, installPath) |
| } |
| } |
| |
| // All remaining entries in pruneCandidates are not referenced by |
| // any instance. |
| for pth, _ := range pruneCandidates { |
| shouldDelete, err := shouldDeleteInstallation(pth, now) |
| if err != nil { |
| allerrors = append(allerrors, pthError{pth, err}) |
| continue |
| } |
| |
| if shouldDelete { |
| if err := suidHelper.deleteFileTree(ctx, pth, nil, nil); err != nil { |
| allerrors = append(allerrors, pthError{pth, err}) |
| } |
| } |
| } |
| return processErrors(ctx, allerrors) |
| } |
| |
| // pruneOldLogs removes logs more than a day old. Symlinks (the |
| // cannonical log file name) the (newest) log files that they point to |
| // are preserved. |
| func pruneOldLogs(ctx *context.T, root string, now time.Time) error { |
| logPaths, err := filepath.Glob(filepath.Join(root, "app*", "installation*", "instances", "instance*", "logs", "*")) |
| if err != nil { |
| return err |
| } |
| |
| pruneCandidates := make(map[string]struct{}, len(logPaths)) |
| for _, p := range logPaths { |
| pruneCandidates[p] = struct{}{} |
| } |
| |
| allerrors := make([]pthError, 0) |
| for p, _ := range pruneCandidates { |
| fi, err := os.Stat(p) |
| if err != nil { |
| allerrors = append(allerrors, pthError{p, err}) |
| delete(pruneCandidates, p) |
| continue |
| } |
| |
| if fi.Mode()&os.ModeSymlink != 0 { |
| delete(pruneCandidates, p) |
| target, err := os.Readlink(p) |
| if err != nil { |
| allerrors = append(allerrors, pthError{p, err}) |
| continue |
| } |
| delete(pruneCandidates, target) |
| continue |
| } |
| |
| if !oldEnoughToTidy(fi, now) { |
| delete(pruneCandidates, p) |
| } |
| } |
| |
| for pth, _ := range pruneCandidates { |
| if err := suidHelper.deleteFileTree(ctx, pth, nil, nil); err != nil { |
| allerrors = append(allerrors, pthError{pth, err}) |
| } |
| } |
| return processErrors(ctx, allerrors) |
| } |
| |
| // tidyHarness runs device manager cleanup operations |
| func tidyHarness(ctx *context.T, root string) error { |
| now := MockableNow() |
| |
| if err := pruneDeletedInstances(ctx, root, now); err != nil { |
| return err |
| } |
| |
| if err := pruneUninstalledInstallations(ctx, root, now); err != nil { |
| return err |
| } |
| |
| return pruneOldLogs(ctx, root, now) |
| } |
| |
| // tidyDaemon runs in a Go routine, processing requests to tidy |
| // or tidying on a schedule. |
| func tidyDaemon(ctx *context.T, c <-chan tidyRequests, root string) { |
| for { |
| select { |
| case req, ok := <-c: |
| if !ok { |
| return |
| } |
| req.bc <- tidyHarness(req.ctx, root) |
| case <-time.After(AutomaticTidyingInterval): |
| if err := tidyHarness(nil, root); err != nil { |
| ctx.Errorf("tidyDaemon failed to tidy: %v", err) |
| } |
| } |
| |
| } |
| } |
| |
| type tidyRequests struct { |
| ctx *context.T |
| bc chan<- error |
| } |
| |
| func newTidyingDaemon(ctx *context.T, root string) chan<- tidyRequests { |
| c := make(chan tidyRequests) |
| go tidyDaemon(ctx, c, root) |
| return c |
| } |