blob: f9cdfc008d63f7847b2cca05d540e075448e1385 [file] [log] [blame]
// 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
}