1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
package assets
import (
"context"
"fmt"
"os"
"path/filepath"
"sort"
"strings"
"time"
)
type UnitRef struct {
Backend string
Name string
Exec []string
Env []string
}
type UnitLister interface {
LoadedUnits(ctx context.Context) ([]UnitRef, error)
}
type UnitListerFunc func(ctx context.Context) ([]UnitRef, error)
func (f UnitListerFunc) LoadedUnits(ctx context.Context) ([]UnitRef, error) { return f(ctx) }
type GCPolicy struct {
Keep []string
Window time.Duration
Now time.Time
}
type GCKept struct {
Dir string `json:"dir"`
Reason string `json:"reason"`
}
type GCReport struct {
Root string `json:"root"`
Removed []string `json:"removed"`
Kept []GCKept `json:"kept"`
}
func Collect(ctx context.Context, root string, listers []UnitLister, p GCPolicy) (GCReport, error) {
rep := GCReport{Root: root, Removed: []string{}, Kept: []GCKept{}}
if !filepath.IsAbs(root) {
return rep, fmt.Errorf("assets: gc root %q is not absolute", root)
}
if p.Now.IsZero() {
p.Now = time.Now()
}
var units []UnitRef
for _, l := range listers {
u, err := l.LoadedUnits(ctx)
if err != nil {
return rep, fmt.Errorf("assets: gc refuses: the loaded units cannot be read: %w", err)
}
units = append(units, u...)
}
entries, err := os.ReadDir(root)
if os.IsNotExist(err) {
return rep, nil
}
if err != nil {
return rep, err
}
keep := map[string]bool{}
for _, k := range p.Keep {
keep[k] = true
}
sort.Slice(entries, func(i, j int) bool { return entries[i].Name() < entries[j].Name() })
for _, e := range entries {
name := e.Name()
scratch := strings.HasPrefix(name, tmpPrefix) || strings.HasPrefix(name, stalePrefix)
if !e.IsDir() || (!digestName.MatchString(name) && !scratch) {
continue
}
dir := filepath.Join(root, name)
if keep[name] {
rep.Kept = append(rep.Kept, GCKept{Dir: dir, Reason: "the current or previous binary's asset set"})
continue
}
if u, ok := referencedBy(dir, units); ok {
rep.Kept = append(rep.Kept, GCKept{Dir: dir, Reason: fmt.Sprintf("named by loaded %s unit %s", u.Backend, u.Name)})
continue
}
info, err := os.Lstat(dir)
if err != nil {
return rep, err
}
if age := p.Now.Sub(info.ModTime()); age < p.Window {
rep.Kept = append(rep.Kept, GCKept{Dir: dir, Reason: fmt.Sprintf("used %s ago, inside the %s rollback window", age.Round(time.Second), p.Window)})
continue
}
if err := removeTree(dir); err != nil {
return rep, err
}
os.Remove(filepath.Join(root, lockPrefix+name))
rep.Removed = append(rep.Removed, dir)
}
return rep, nil
}
func IsDevsup(argv []string) bool {
return len(argv) >= 3 && argv[1] == "internal" && argv[2] == "devsup"
}
func DevCollect(ctx context.Context, root string, devsups []int, listers []UnitLister, p GCPolicy) (GCReport, error) {
if len(devsups) > 0 {
return GCReport{Root: root, Removed: []string{}, Kept: []GCKept{}},
fmt.Errorf("assets: dev gc refuses while a devsup of this user runs (pids %v); the per-user cache is shared by every dev instance, run dev down first", devsups)
}
return Collect(ctx, root, listers, p)
}
func referencedBy(dir string, units []UnitRef) (UnitRef, bool) {
for _, u := range units {
for _, s := range u.Exec {
if names(s, dir) {
return u, true
}
}
for _, s := range u.Env {
if _, v, ok := strings.Cut(s, "="); ok && names(v, dir) {
return u, true
}
}
}
return UnitRef{}, false
}
func names(s, dir string) bool {
for _, part := range strings.FieldsFunc(s, func(r rune) bool { return r == ':' || r == '=' || r == ' ' || r == ',' }) {
c := filepath.Clean(part)
if c == dir || strings.HasPrefix(c, dir+string(filepath.Separator)) {
return true
}
}
return false
}