disk-space-saviour 0.15.2 → 0.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +20 -0
- package/README.md +74 -1
- package/bin/dss.js +22 -5
- package/package.json +1 -1
- package/src/audit.js +19 -2
- package/src/clean.js +153 -11
- package/src/cli.js +102 -14
- package/src/docker/cli.js +11 -7
- package/src/docker/scan.js +25 -4
- package/src/docker/upperdir.js +44 -0
- package/src/docker/writable.js +90 -13
- package/src/emergency.js +37 -6
- package/src/env/batches.js +20 -0
- package/src/env/cache-roots.js +138 -0
- package/src/env/inspection.js +100 -0
- package/src/env/local.js +314 -96
- package/src/env/parallel.js +23 -0
- package/src/env/platform-index.js +87 -0
- package/src/env/proc.js +13 -1
- package/src/env/process-scripts.js +24 -0
- package/src/env/scope.js +95 -0
- package/src/env/shell.js +146 -108
- package/src/exec.js +79 -8
- package/src/fast-clean.js +39 -0
- package/src/health.js +16 -5
- package/src/index.d.ts +52 -1
- package/src/items.js +9 -3
- package/src/liveness.js +112 -25
- package/src/options.js +9 -1
- package/src/report.js +25 -3
- package/src/rules/toolchains.js +66 -0
- package/src/scan-state.js +239 -0
- package/src/scan.js +240 -19
- package/src/scanners/agents.js +3 -2
- package/src/scanners/common.js +6 -1
- package/src/scanners/global.js +91 -22
- package/src/scanners/projects.js +114 -20
- package/src/scanners/rust.js +7 -1
- package/src/scanners/system.js +7 -6
- package/src/scanners/versions.js +25 -13
- package/src/swap.js +69 -0
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/** Clean completed safe host findings while discovery continues. */
|
|
2
|
+
import { clean } from './clean.js';
|
|
3
|
+
import { scan } from './scan.js';
|
|
4
|
+
|
|
5
|
+
export async function cleanDuringScan(options) {
|
|
6
|
+
const entries = [];
|
|
7
|
+
const health = [];
|
|
8
|
+
let failure;
|
|
9
|
+
const report = await scan({
|
|
10
|
+
...options,
|
|
11
|
+
onScanPhase: async (phase) => {
|
|
12
|
+
try {
|
|
13
|
+
const audit = await clean(phase, {
|
|
14
|
+
...options,
|
|
15
|
+
tier: 'safe',
|
|
16
|
+
priorEntries: entries,
|
|
17
|
+
priorHealth: health,
|
|
18
|
+
});
|
|
19
|
+
entries.splice(0, entries.length, ...audit.entries);
|
|
20
|
+
health.splice(0, health.length, ...audit.health);
|
|
21
|
+
} catch (error) {
|
|
22
|
+
failure = error;
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
});
|
|
27
|
+
if (failure) {
|
|
28
|
+
throw failure;
|
|
29
|
+
}
|
|
30
|
+
if (report.aborted) {
|
|
31
|
+
return { report, audit: null };
|
|
32
|
+
}
|
|
33
|
+
const audit = await clean(report, {
|
|
34
|
+
...options,
|
|
35
|
+
priorEntries: entries,
|
|
36
|
+
priorHealth: health,
|
|
37
|
+
});
|
|
38
|
+
return { report, audit };
|
|
39
|
+
}
|
package/src/health.js
CHANGED
|
@@ -52,8 +52,14 @@ function identity(proc) {
|
|
|
52
52
|
* Processes worth watching: PID 1 and every agent or build tool.
|
|
53
53
|
* @param {object[]} processes output of `env.processes()`
|
|
54
54
|
*/
|
|
55
|
-
export function watchedProcesses(processes) {
|
|
55
|
+
export function watchedProcesses(processes, mountNamespace = null) {
|
|
56
56
|
return processes
|
|
57
|
+
.filter(
|
|
58
|
+
(proc) =>
|
|
59
|
+
!mountNamespace ||
|
|
60
|
+
!proc.mountNamespace ||
|
|
61
|
+
proc.mountNamespace === mountNamespace
|
|
62
|
+
)
|
|
57
63
|
.filter((proc) => proc.pid === 1 || matchingName(proc, WATCHED_NAMES))
|
|
58
64
|
.map(identity);
|
|
59
65
|
}
|
|
@@ -101,9 +107,9 @@ function describe(lost) {
|
|
|
101
107
|
* Per-environment snapshots and re-checks. `records` is the audit view.
|
|
102
108
|
*/
|
|
103
109
|
export class HealthWatch {
|
|
104
|
-
constructor() {
|
|
105
|
-
this.records = [];
|
|
106
|
-
this.byEnv = new Map();
|
|
110
|
+
constructor(records = []) {
|
|
111
|
+
this.records = [...records];
|
|
112
|
+
this.byEnv = new Map(records.map((record) => [record.env, record]));
|
|
107
113
|
}
|
|
108
114
|
|
|
109
115
|
/**
|
|
@@ -118,7 +124,12 @@ export class HealthWatch {
|
|
|
118
124
|
env: item.env,
|
|
119
125
|
envLabel: item.envLabel,
|
|
120
126
|
checks: 0,
|
|
121
|
-
watched: processes
|
|
127
|
+
watched: processes
|
|
128
|
+
? watchedProcesses(
|
|
129
|
+
processes,
|
|
130
|
+
env.kind === 'host' ? env.mountNamespace : null
|
|
131
|
+
)
|
|
132
|
+
: [],
|
|
122
133
|
available: Boolean(processes),
|
|
123
134
|
lost: [],
|
|
124
135
|
stopped: null,
|
package/src/index.d.ts
CHANGED
|
@@ -11,6 +11,25 @@ export interface ScanOptions {
|
|
|
11
11
|
roots?: string[] | string | null;
|
|
12
12
|
/** Directory depth for project search. */
|
|
13
13
|
maxDepth?: number;
|
|
14
|
+
/** Time budget per filesystem environment (`2m`); incomplete findings are kept. */
|
|
15
|
+
scanBudget?: Duration;
|
|
16
|
+
/** Persistent scan/index directory. Library default is false; CLI uses XDG state. */
|
|
17
|
+
cacheDir?: string | false;
|
|
18
|
+
/** Use verified Spotlight/locate hints before the discovery walk. */
|
|
19
|
+
platformIndex?: boolean;
|
|
20
|
+
signal?: AbortSignal;
|
|
21
|
+
onScanProgress?: (progress: {
|
|
22
|
+
env: string;
|
|
23
|
+
scanner: string;
|
|
24
|
+
elapsedMs: number;
|
|
25
|
+
items: number;
|
|
26
|
+
candidates: number;
|
|
27
|
+
filesSized: number;
|
|
28
|
+
directoriesSized: number;
|
|
29
|
+
indexHits: number;
|
|
30
|
+
sizeCacheHits: number;
|
|
31
|
+
status: string;
|
|
32
|
+
}) => void;
|
|
14
33
|
/** Activity window: newer files are kept (`1h`). */
|
|
15
34
|
staleAge?: Duration;
|
|
16
35
|
/** Alias of `staleAge`. */
|
|
@@ -65,7 +84,6 @@ export interface CleanOptions extends ScanOptions {
|
|
|
65
84
|
confirm?: (item: Item) => boolean | Promise<boolean>;
|
|
66
85
|
/** `false` skips writing the audit log file. */
|
|
67
86
|
audit?: boolean;
|
|
68
|
-
signal?: AbortSignal;
|
|
69
87
|
onEntry?: (entry: AuditEntry, item: Item) => void;
|
|
70
88
|
}
|
|
71
89
|
|
|
@@ -100,6 +118,8 @@ export interface Item {
|
|
|
100
118
|
path: string | null;
|
|
101
119
|
paths: string[];
|
|
102
120
|
bytes: number;
|
|
121
|
+
/** Cached size is advisory; cleanup always measures again. */
|
|
122
|
+
sizeEstimated?: boolean;
|
|
103
123
|
/** Merged filesystem bytes, including image data, when measured in Docker. */
|
|
104
124
|
totalBytes?: number;
|
|
105
125
|
/** Immutable image bytes excluded from reclaimable totals. */
|
|
@@ -145,11 +165,36 @@ export interface EnvironmentDescriptor {
|
|
|
145
165
|
chain: Array<{ containerId: string; name: string }>;
|
|
146
166
|
hint?: string | null;
|
|
147
167
|
disk?: Disk | null;
|
|
168
|
+
swap?: {
|
|
169
|
+
total: number;
|
|
170
|
+
used: number;
|
|
171
|
+
free: number;
|
|
172
|
+
holders: Array<{
|
|
173
|
+
pid: number;
|
|
174
|
+
command: string;
|
|
175
|
+
memoryBytes: number;
|
|
176
|
+
compressedBytes: number;
|
|
177
|
+
}>;
|
|
178
|
+
attribution: string;
|
|
179
|
+
advice: string;
|
|
180
|
+
} | null;
|
|
181
|
+
scanStatus?:
|
|
182
|
+
| 'scanning'
|
|
183
|
+
| 'complete'
|
|
184
|
+
| 'goal-reached'
|
|
185
|
+
| 'aborted'
|
|
186
|
+
| 'budget-exceeded'
|
|
187
|
+
| 'error'
|
|
188
|
+
| null;
|
|
189
|
+
layerSource?: string | null;
|
|
148
190
|
}
|
|
149
191
|
|
|
150
192
|
export interface TierTotal {
|
|
151
193
|
items: number;
|
|
152
194
|
bytes: number;
|
|
195
|
+
/** Visible blocked bytes, including data whose writable size is unknown. */
|
|
196
|
+
totalBytes?: number;
|
|
197
|
+
unknownBytes?: number;
|
|
153
198
|
}
|
|
154
199
|
|
|
155
200
|
export interface Report {
|
|
@@ -157,6 +202,9 @@ export interface Report {
|
|
|
157
202
|
tool: 'disk-space-saviour';
|
|
158
203
|
createdAt: string;
|
|
159
204
|
durationMs: number;
|
|
205
|
+
aborted?: boolean;
|
|
206
|
+
authentication?: { hostname: string; uid: number | string; digest: string };
|
|
207
|
+
audit?: { file: string | null; error: string; code?: string };
|
|
160
208
|
host: { env: string; label: string; platform: string };
|
|
161
209
|
options: Record<string, unknown>;
|
|
162
210
|
environments: EnvironmentDescriptor[];
|
|
@@ -188,6 +236,8 @@ export interface AuditEntry {
|
|
|
188
236
|
reason: string | null;
|
|
189
237
|
plannedBytes: number;
|
|
190
238
|
freedBytes: number;
|
|
239
|
+
/** True when authoritative before/after allocation could not be measured. */
|
|
240
|
+
freedEstimated?: boolean;
|
|
191
241
|
durationMs: number;
|
|
192
242
|
backup?: string;
|
|
193
243
|
[key: string]: unknown;
|
|
@@ -211,6 +261,7 @@ export interface AuditLog {
|
|
|
211
261
|
startedAt: string;
|
|
212
262
|
finishedAt: string | null;
|
|
213
263
|
aborted?: boolean;
|
|
264
|
+
report?: Partial<Report>;
|
|
214
265
|
error?: string;
|
|
215
266
|
entries: AuditEntry[];
|
|
216
267
|
freedBytes: number;
|
package/src/items.js
CHANGED
|
@@ -100,11 +100,17 @@ export function tierTotals(items) {
|
|
|
100
100
|
Boolean(item.requiresConfirmation)
|
|
101
101
|
)
|
|
102
102
|
);
|
|
103
|
+
const blocked = dropNested(items.filter((item) => item.blockers.length > 0));
|
|
103
104
|
totals.blocked = {
|
|
104
105
|
items: items.filter((item) => item.blockers.length > 0).length,
|
|
105
|
-
bytes:
|
|
106
|
-
|
|
107
|
-
|
|
106
|
+
bytes: blocked.reduce((sum, item) => sum + item.bytes, 0),
|
|
107
|
+
totalBytes: blocked.reduce(
|
|
108
|
+
(sum, item) => sum + (item.totalBytes ?? item.bytes),
|
|
109
|
+
0
|
|
110
|
+
),
|
|
111
|
+
unknownBytes: blocked
|
|
112
|
+
.filter((item) => item.sizeUnknown)
|
|
113
|
+
.reduce((sum, item) => sum + (item.totalBytes ?? item.bytes), 0),
|
|
108
114
|
};
|
|
109
115
|
return totals;
|
|
110
116
|
}
|
package/src/liveness.js
CHANGED
|
@@ -20,6 +20,46 @@ import { formatDuration } from './units.js';
|
|
|
20
20
|
|
|
21
21
|
const PROC_COMM_LIMIT = 15;
|
|
22
22
|
|
|
23
|
+
function uncertaintyRoots(proc) {
|
|
24
|
+
const roots = processArgs(proc).flatMap((arg) => {
|
|
25
|
+
const value = arg.includes('=') ? arg.slice(arg.indexOf('=') + 1) : arg;
|
|
26
|
+
return value.startsWith('/') ? [value] : [];
|
|
27
|
+
});
|
|
28
|
+
if (proc.name === 'dockerd') {
|
|
29
|
+
roots.push('/var/lib/docker', '/run');
|
|
30
|
+
}
|
|
31
|
+
if (proc.name === 'containerd') {
|
|
32
|
+
roots.push('/var/lib/containerd', '/run/containerd');
|
|
33
|
+
}
|
|
34
|
+
return roots;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function couldAccess(proc, stat) {
|
|
38
|
+
return (
|
|
39
|
+
proc.uid === 0 ||
|
|
40
|
+
typeof proc.uid !== 'number' ||
|
|
41
|
+
!stat ||
|
|
42
|
+
stat.uid === undefined ||
|
|
43
|
+
stat.uid === proc.uid ||
|
|
44
|
+
stat.mode === undefined ||
|
|
45
|
+
(stat.mode & 0o007) !== 0 ||
|
|
46
|
+
// Group bits can also be an ACL mask granting another named identity.
|
|
47
|
+
(stat.mode & 0o070) !== 0
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function ancestors(target, pathApi) {
|
|
52
|
+
const result = [];
|
|
53
|
+
for (
|
|
54
|
+
let dir = pathApi.dirname(target);
|
|
55
|
+
dir !== pathApi.dirname(dir);
|
|
56
|
+
dir = pathApi.dirname(dir)
|
|
57
|
+
) {
|
|
58
|
+
result.push(dir);
|
|
59
|
+
}
|
|
60
|
+
return result;
|
|
61
|
+
}
|
|
62
|
+
|
|
23
63
|
function nameMatches(name, candidate) {
|
|
24
64
|
return (
|
|
25
65
|
name === candidate ||
|
|
@@ -131,6 +171,7 @@ export class LivenessProbe {
|
|
|
131
171
|
this.probeError = false;
|
|
132
172
|
this.realPaths = new Map();
|
|
133
173
|
this.pathOwners = new Map();
|
|
174
|
+
this.pathStats = new Map();
|
|
134
175
|
}
|
|
135
176
|
|
|
136
177
|
async refresh(force = false) {
|
|
@@ -145,6 +186,9 @@ export class LivenessProbe {
|
|
|
145
186
|
(processes === null || this.openPaths === null) &&
|
|
146
187
|
(this.env.platform === 'linux' || this.env.platform === 'darwin');
|
|
147
188
|
this.refreshedAt = this.now();
|
|
189
|
+
this.realPaths.clear();
|
|
190
|
+
this.pathStats.clear();
|
|
191
|
+
this.pathOwners.clear();
|
|
148
192
|
}
|
|
149
193
|
|
|
150
194
|
/** Processes of the last refresh, or null when they could not be listed. */
|
|
@@ -159,18 +203,46 @@ export class LivenessProbe {
|
|
|
159
203
|
* @param {string[]} paths
|
|
160
204
|
*/
|
|
161
205
|
async resolve(paths) {
|
|
162
|
-
if (this.env.unreadableProcesses?.length
|
|
163
|
-
|
|
164
|
-
|
|
206
|
+
if (this.env.unreadableProcesses?.length) {
|
|
207
|
+
const missing = [
|
|
208
|
+
...new Set(
|
|
209
|
+
paths.flatMap((target) => [
|
|
210
|
+
target,
|
|
211
|
+
...ancestors(target, this.env.path),
|
|
212
|
+
])
|
|
213
|
+
),
|
|
214
|
+
].filter((target) => !this.pathStats.has(target));
|
|
215
|
+
const stats = this.env.statMany
|
|
216
|
+
? await this.env.statMany(missing)
|
|
217
|
+
: new Map(
|
|
218
|
+
await Promise.all(
|
|
219
|
+
missing.map(async (target) => [
|
|
220
|
+
target,
|
|
221
|
+
await this.env.stat?.(target),
|
|
222
|
+
])
|
|
223
|
+
)
|
|
224
|
+
);
|
|
225
|
+
for (const target of missing) {
|
|
226
|
+
this.pathStats.set(target, stats.get(target));
|
|
227
|
+
this.pathOwners.set(target, stats.get(target)?.uid);
|
|
165
228
|
}
|
|
166
229
|
}
|
|
167
|
-
if (
|
|
230
|
+
if (!this.env.realPath && !this.env.realPathMany) {
|
|
168
231
|
return;
|
|
169
232
|
}
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
233
|
+
const missing = paths.filter((target) => !this.realPaths.has(target));
|
|
234
|
+
const resolved = this.env.realPathMany
|
|
235
|
+
? await this.env.realPathMany(missing)
|
|
236
|
+
: new Map(
|
|
237
|
+
await Promise.all(
|
|
238
|
+
missing.map(async (target) => [
|
|
239
|
+
target,
|
|
240
|
+
await this.env.realPath(target),
|
|
241
|
+
])
|
|
242
|
+
)
|
|
243
|
+
);
|
|
244
|
+
for (const target of missing) {
|
|
245
|
+
this.realPaths.set(target, resolved.get(target) ?? null);
|
|
174
246
|
}
|
|
175
247
|
}
|
|
176
248
|
|
|
@@ -271,29 +343,25 @@ export class LivenessProbe {
|
|
|
271
343
|
uncertainUsage(paths) {
|
|
272
344
|
for (const proc of this.env.unreadableProcesses ?? []) {
|
|
273
345
|
const args = processArgs(proc);
|
|
274
|
-
const roots =
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
roots.push('/var/lib/docker', '/run');
|
|
280
|
-
}
|
|
281
|
-
if (proc.name === 'containerd') {
|
|
282
|
-
roots.push('/var/lib/containerd', '/run/containerd');
|
|
283
|
-
}
|
|
346
|
+
const roots = uncertaintyRoots(proc);
|
|
347
|
+
const scopedDaemon =
|
|
348
|
+
proc.uid === 0 &&
|
|
349
|
+
['dockerd', 'containerd'].includes(proc.name) &&
|
|
350
|
+
args.length > 0;
|
|
284
351
|
for (const target of paths) {
|
|
285
352
|
const intersects = roots.some(
|
|
286
353
|
(root) =>
|
|
287
354
|
isWithin(root, target, this.env.path) ||
|
|
288
355
|
isWithin(target, root, this.env.path)
|
|
289
356
|
);
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
)
|
|
357
|
+
if (scopedDaemon && !intersects) {
|
|
358
|
+
continue;
|
|
359
|
+
}
|
|
360
|
+
if (!intersects && this.inaccessible(proc, target)) {
|
|
361
|
+
continue;
|
|
362
|
+
}
|
|
363
|
+
const stat = this.pathStats.get(target);
|
|
364
|
+
if (intersects || couldAccess(proc, stat)) {
|
|
297
365
|
return `unreadable process ${proc.name || 'unknown'} (pid ${proc.pid}, uid ${proc.uid ?? 'unknown'}) may use ${target}`;
|
|
298
366
|
}
|
|
299
367
|
}
|
|
@@ -301,6 +369,25 @@ export class LivenessProbe {
|
|
|
301
369
|
return null;
|
|
302
370
|
}
|
|
303
371
|
|
|
372
|
+
inaccessible(proc, target) {
|
|
373
|
+
if (proc.uid === 0 || typeof proc.uid !== 'number') {
|
|
374
|
+
return false;
|
|
375
|
+
}
|
|
376
|
+
return ancestors(target, this.env.path).some((dir) => {
|
|
377
|
+
const stat = this.pathStats.get(dir);
|
|
378
|
+
if (
|
|
379
|
+
!stat ||
|
|
380
|
+
stat.mode === undefined ||
|
|
381
|
+
stat.uid === proc.uid ||
|
|
382
|
+
stat.mode & 0o001
|
|
383
|
+
) {
|
|
384
|
+
return false;
|
|
385
|
+
}
|
|
386
|
+
const groupAccess = stat.mode & 0o010;
|
|
387
|
+
return !groupAccess;
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
|
|
304
391
|
/**
|
|
305
392
|
* First reason the item is busy, or null when it is idle. Items with
|
|
306
393
|
* `checks.perPath` leave open files to `openPathsOf`: the caller skips
|
package/src/options.js
CHANGED
|
@@ -16,7 +16,7 @@ export const DEFAULTS = {
|
|
|
16
16
|
docker: null,
|
|
17
17
|
dockerDepth: 3,
|
|
18
18
|
containers: [],
|
|
19
|
-
scanners: ['
|
|
19
|
+
scanners: ['global', 'projects', 'versions', 'agents', 'system'],
|
|
20
20
|
only: [],
|
|
21
21
|
exclude: [],
|
|
22
22
|
includeVolumes: false,
|
|
@@ -32,6 +32,9 @@ export const DEFAULTS = {
|
|
|
32
32
|
yes: false,
|
|
33
33
|
backupDir: null,
|
|
34
34
|
auditDir: null,
|
|
35
|
+
scanBudget: '2m',
|
|
36
|
+
cacheDir: false,
|
|
37
|
+
platformIndex: true,
|
|
35
38
|
};
|
|
36
39
|
|
|
37
40
|
function toBytes(value) {
|
|
@@ -70,6 +73,10 @@ export function resolveOptions(input = {}) {
|
|
|
70
73
|
}
|
|
71
74
|
}
|
|
72
75
|
const staleAge = input.olderThan ?? merged.staleAge;
|
|
76
|
+
const scanBudgetMs = toMs(merged.scanBudget);
|
|
77
|
+
if (!Number.isFinite(scanBudgetMs) || scanBudgetMs <= 0) {
|
|
78
|
+
throw new Error('scanBudget must be a positive finite duration');
|
|
79
|
+
}
|
|
73
80
|
const roots = toList(merged.roots);
|
|
74
81
|
return {
|
|
75
82
|
...merged,
|
|
@@ -83,6 +90,7 @@ export function resolveOptions(input = {}) {
|
|
|
83
90
|
? null
|
|
84
91
|
: toMs(merged.investigationMaxAge),
|
|
85
92
|
minSizeBytes: toBytes(merged.minSize),
|
|
93
|
+
scanBudgetMs,
|
|
86
94
|
journalKeepBytes: toBytes(merged.journalKeep),
|
|
87
95
|
containerFilter: toList(merged.containers),
|
|
88
96
|
allowDirtyContainers: toList(merged.allowDirtyContainers),
|
package/src/report.js
CHANGED
|
@@ -52,11 +52,11 @@ function itemLine(item, showEnv) {
|
|
|
52
52
|
? ` (needs ${consentFlag(item)})`
|
|
53
53
|
: '';
|
|
54
54
|
const storage = item.sizeUnknown
|
|
55
|
-
?
|
|
55
|
+
? ` (${formatBytes(item.totalBytes ?? 0)} visible; may be in an image layer; freeable bytes unknown, excluded from totals)`
|
|
56
56
|
: item.imageBytes
|
|
57
57
|
? ` (${formatBytes(item.imageBytes)} in image${item.imageRef ? ` ${item.imageRef}` : ''}; 0 B of that data freeable now; remove the stopped container and unused image after the task completes)`
|
|
58
58
|
: '';
|
|
59
|
-
return ` ${pad(formatBytes(item.bytes), 10)} ${env}${item.rule} ${itemLocation(item)}${confirm}${storage}`;
|
|
59
|
+
return ` ${pad(formatBytes(item.bytes), 10)}${item.sizeEstimated ? ' (estimated)' : ''} ${env}${item.rule} ${itemLocation(item)}${confirm}${storage}`;
|
|
60
60
|
}
|
|
61
61
|
|
|
62
62
|
function limited(lines, limit) {
|
|
@@ -102,7 +102,7 @@ function blockedSection(report, options) {
|
|
|
102
102
|
}
|
|
103
103
|
return [
|
|
104
104
|
'',
|
|
105
|
-
`BLOCKED ${plural(items.length, 'item')} (kept)`,
|
|
105
|
+
`BLOCKED ${plural(items.length, 'item')} (kept), ${formatBytes(report.totals.blocked.bytes)} known writable, ${formatBytes(report.totals.blocked.totalBytes ?? report.totals.blocked.bytes)} visible${report.totals.blocked.unknownBytes ? ` (${formatBytes(report.totals.blocked.unknownBytes)} with unknown writable size)` : ''}`,
|
|
106
106
|
...limited(lines, options.limit * 2),
|
|
107
107
|
];
|
|
108
108
|
}
|
|
@@ -318,10 +318,32 @@ export function formatReport(report, options = {}) {
|
|
|
318
318
|
`disk-space-saviour scan of ${host.label} in ${(report.durationMs / 1000).toFixed(1)}s (report only, nothing was deleted)`,
|
|
319
319
|
formatDisk(host.disk),
|
|
320
320
|
];
|
|
321
|
+
if (host.swap) {
|
|
322
|
+
lines.push(
|
|
323
|
+
`Swap: ${formatBytes(host.swap.used)} used of ${formatBytes(host.swap.total)} (managed by macOS)`
|
|
324
|
+
);
|
|
325
|
+
lines.push(host.swap.attribution);
|
|
326
|
+
for (const holder of host.swap.holders) {
|
|
327
|
+
lines.push(
|
|
328
|
+
` ${holder.pid} ${holder.command}: ${formatBytes(holder.memoryBytes)} memory, ${formatBytes(holder.compressedBytes)} compressed`
|
|
329
|
+
);
|
|
330
|
+
}
|
|
331
|
+
lines.push(host.swap.advice);
|
|
332
|
+
}
|
|
321
333
|
for (const tier of TIERS) {
|
|
322
334
|
lines.push(...tierSection(report, tier, { limit }));
|
|
323
335
|
}
|
|
324
336
|
for (const env of report.environments) {
|
|
337
|
+
if (env.layerSource) {
|
|
338
|
+
lines.push(` ${env.label}: layer source ${env.layerSource}`);
|
|
339
|
+
}
|
|
340
|
+
if (env.scanStatus === 'goal-reached') {
|
|
341
|
+
lines.push(
|
|
342
|
+
` ${env.label}: scan stopped after the free-space goal was met`
|
|
343
|
+
);
|
|
344
|
+
} else if (env.scanStatus && env.scanStatus !== 'complete') {
|
|
345
|
+
lines.push(` ${env.label}: scan incomplete (${env.scanStatus})`);
|
|
346
|
+
}
|
|
325
347
|
if (env.hint) {
|
|
326
348
|
lines.push(` ${env.label}: ${env.hint}`);
|
|
327
349
|
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/** Download leftovers of language version managers; installed runtimes are kept. */
|
|
2
|
+
export const TOOLCHAIN_CACHE_RULES = [
|
|
3
|
+
{
|
|
4
|
+
id: 'perlbrew-build',
|
|
5
|
+
ecosystem: 'scripting',
|
|
6
|
+
description: 'perlbrew build trees and source archives',
|
|
7
|
+
paths: [
|
|
8
|
+
'~/perl5/perlbrew/build',
|
|
9
|
+
'~/perl5/perlbrew/dists',
|
|
10
|
+
'~/.perl5/build',
|
|
11
|
+
'~/.perl5/dists',
|
|
12
|
+
],
|
|
13
|
+
aggregate: true,
|
|
14
|
+
busy: ['perlbrew', 'perl', 'make'],
|
|
15
|
+
native: {
|
|
16
|
+
tool: 'perlbrew',
|
|
17
|
+
argv: ['perlbrew', 'clean'],
|
|
18
|
+
cacheRoot: '~/perl5/perlbrew',
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
id: 'sdkman-tmp',
|
|
23
|
+
ecosystem: 'jvm',
|
|
24
|
+
description: 'SDKMAN download leftovers',
|
|
25
|
+
paths: ['~/.sdkman/tmp/*'],
|
|
26
|
+
aggregate: true,
|
|
27
|
+
busy: ['sdk', 'sdkman', 'java', 'curl', 'unzip'],
|
|
28
|
+
native: {
|
|
29
|
+
tool: 'bash',
|
|
30
|
+
init: '~/.sdkman/bin/sdkman-init.sh',
|
|
31
|
+
command: 'sdk flush tmp',
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
id: 'sdkman-archives',
|
|
36
|
+
ecosystem: 'jvm',
|
|
37
|
+
description: 'SDKMAN downloaded archives',
|
|
38
|
+
paths: ['~/.sdkman/archives/*'],
|
|
39
|
+
aggregate: true,
|
|
40
|
+
busy: ['sdk', 'sdkman', 'java', 'curl', 'unzip'],
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
id: 'nvm-cache',
|
|
44
|
+
ecosystem: 'javascript',
|
|
45
|
+
description: 'nvm downloaded Node archives',
|
|
46
|
+
paths: ['~/.nvm/.cache'],
|
|
47
|
+
busy: ['nvm', 'node', 'curl'],
|
|
48
|
+
native: { tool: 'sh', init: '~/.nvm/nvm.sh', command: 'nvm cache clear' },
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
id: 'ruby-gem-cache',
|
|
52
|
+
ecosystem: 'ruby',
|
|
53
|
+
description: 'downloaded Ruby gem archives',
|
|
54
|
+
paths: [
|
|
55
|
+
'~/.rbenv/versions/*/lib/ruby/gems/*/cache/*.gem',
|
|
56
|
+
'~/.rvm/gems/*/cache/*.gem',
|
|
57
|
+
'~/.local/share/gem/ruby/*/cache/*.gem',
|
|
58
|
+
'~/.gem/ruby/*/cache/*.gem',
|
|
59
|
+
'{GEM_HOME}/cache/*.gem',
|
|
60
|
+
'{GEM_PATH}/cache/*.gem',
|
|
61
|
+
],
|
|
62
|
+
aggregate: true,
|
|
63
|
+
fileOnly: true,
|
|
64
|
+
busy: ['gem', 'bundle', 'ruby'],
|
|
65
|
+
},
|
|
66
|
+
];
|