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,138 @@
|
|
|
1
|
+
/** Resolve cache configuration once per environment, using only that user's tools. */
|
|
2
|
+
export const CACHE_VARIABLES = [
|
|
3
|
+
'LOCALAPPDATA',
|
|
4
|
+
'APPDATA',
|
|
5
|
+
'TMPDIR',
|
|
6
|
+
'XDG_CACHE_HOME',
|
|
7
|
+
'CARGO_HOME',
|
|
8
|
+
'RUSTUP_HOME',
|
|
9
|
+
'GRADLE_USER_HOME',
|
|
10
|
+
'DENO_DIR',
|
|
11
|
+
'BUN_INSTALL_CACHE_DIR',
|
|
12
|
+
'GOMODCACHE',
|
|
13
|
+
'GOCACHE',
|
|
14
|
+
'GOPATH',
|
|
15
|
+
'NVM_DIR',
|
|
16
|
+
'SDKMAN_DIR',
|
|
17
|
+
'PERLBREW_ROOT',
|
|
18
|
+
'RBENV_ROOT',
|
|
19
|
+
'GEM_HOME',
|
|
20
|
+
'GEM_PATH',
|
|
21
|
+
'npm_config_cache',
|
|
22
|
+
'NPM_CONFIG_CACHE',
|
|
23
|
+
'PIP_CACHE_DIR',
|
|
24
|
+
'YARN_CACHE_FOLDER',
|
|
25
|
+
'PNPM_STORE_DIR',
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
const ROOTS = [
|
|
29
|
+
['~/.cargo', 'CARGO_HOME'],
|
|
30
|
+
['~/.rustup', 'RUSTUP_HOME'],
|
|
31
|
+
['~/.gradle', 'GRADLE_USER_HOME'],
|
|
32
|
+
['~/.nvm', 'NVM_DIR'],
|
|
33
|
+
['~/.sdkman', 'SDKMAN_DIR'],
|
|
34
|
+
['~/.perl5', 'PERLBREW_ROOT'],
|
|
35
|
+
['~/perl5/perlbrew', 'PERLBREW_ROOT'],
|
|
36
|
+
['~/.rbenv', 'RBENV_ROOT'],
|
|
37
|
+
['~/.bun/install/cache', 'BUN_INSTALL_CACHE_DIR'],
|
|
38
|
+
['~/.cache/deno', 'DENO_DIR'],
|
|
39
|
+
['~/.npm', 'npm_config_cache', 'NPM_CONFIG_CACHE'],
|
|
40
|
+
['~/.cache/pip', 'PIP_CACHE_DIR'],
|
|
41
|
+
['~/.cache/yarn', 'YARN_CACHE_FOLDER'],
|
|
42
|
+
['~/.local/share/pnpm/store', 'PNPM_STORE_DIR'],
|
|
43
|
+
['~/go/pkg/mod', 'GOMODCACHE'],
|
|
44
|
+
['~/.cache/go-build', 'GOCACHE'],
|
|
45
|
+
['~/Library/Caches/go-build', 'GOCACHE'],
|
|
46
|
+
['{LOCALAPPDATA}/go-build', 'GOCACHE'],
|
|
47
|
+
['~/Library/Caches/deno', 'DENO_DIR'],
|
|
48
|
+
['{LOCALAPPDATA}/deno', 'DENO_DIR'],
|
|
49
|
+
['{LOCALAPPDATA}/npm-cache', 'npm_config_cache', 'NPM_CONFIG_CACHE'],
|
|
50
|
+
['~/Library/Caches/pip', 'PIP_CACHE_DIR'],
|
|
51
|
+
['{LOCALAPPDATA}/pip/Cache', 'PIP_CACHE_DIR'],
|
|
52
|
+
['~/Library/Caches/Yarn', 'YARN_CACHE_FOLDER'],
|
|
53
|
+
['{LOCALAPPDATA}/Yarn/Cache', 'YARN_CACHE_FOLDER'],
|
|
54
|
+
['~/Library/pnpm/store', 'PNPM_STORE_DIR'],
|
|
55
|
+
['~/.pnpm-store', 'PNPM_STORE_DIR'],
|
|
56
|
+
['{LOCALAPPDATA}/pnpm/store', 'PNPM_STORE_DIR'],
|
|
57
|
+
['~/.cache', 'XDG_CACHE_HOME'],
|
|
58
|
+
];
|
|
59
|
+
|
|
60
|
+
export function configuredPattern(env, home, pattern) {
|
|
61
|
+
if (home !== env.currentHome) {
|
|
62
|
+
return pattern;
|
|
63
|
+
}
|
|
64
|
+
for (const [prefix, ...keys] of ROOTS) {
|
|
65
|
+
if (pattern !== prefix && !pattern.startsWith(`${prefix}/`)) {
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
const root = keys
|
|
69
|
+
.map((key) => env.vars?.[key])
|
|
70
|
+
.find((value) => value && env.path.isAbsolute(value));
|
|
71
|
+
if (root) {
|
|
72
|
+
return env.path.join(root, pattern.slice(prefix.length));
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return pattern;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function applyQuery(vars, keys, values, pathApi) {
|
|
79
|
+
for (const key of keys) {
|
|
80
|
+
if (
|
|
81
|
+
!vars[key] &&
|
|
82
|
+
typeof values[key] === 'string' &&
|
|
83
|
+
pathApi.isAbsolute(values[key])
|
|
84
|
+
) {
|
|
85
|
+
vars[key] = values[key];
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export async function configureCacheRoots(env, homes) {
|
|
91
|
+
if (env.variables) {
|
|
92
|
+
Object.assign(env.vars, await env.variables(CACHE_VARIABLES));
|
|
93
|
+
}
|
|
94
|
+
if (!homes.includes(env.currentHome)) {
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
const vars = env.vars;
|
|
98
|
+
if (!vars.GOMODCACHE && vars.GOPATH) {
|
|
99
|
+
vars.GOMODCACHE = env.path.join(
|
|
100
|
+
vars.GOPATH.split(env.platform === 'win32' ? ';' : ':')[0],
|
|
101
|
+
'pkg/mod'
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
const queries = [
|
|
105
|
+
[
|
|
106
|
+
'go',
|
|
107
|
+
['go', 'env', '-json', 'GOMODCACHE', 'GOCACHE'],
|
|
108
|
+
['GOMODCACHE', 'GOCACHE'],
|
|
109
|
+
],
|
|
110
|
+
['npm', ['npm', 'config', 'get', 'cache'], ['npm_config_cache']],
|
|
111
|
+
['pip', ['pip', 'cache', 'dir'], ['PIP_CACHE_DIR']],
|
|
112
|
+
['yarn', ['yarn', 'cache', 'dir'], ['YARN_CACHE_FOLDER']],
|
|
113
|
+
['pnpm', ['pnpm', 'store', 'path'], ['PNPM_STORE_DIR']],
|
|
114
|
+
];
|
|
115
|
+
for (const [tool, argv, keys] of queries) {
|
|
116
|
+
if (
|
|
117
|
+
keys.every((key) => vars[key]) ||
|
|
118
|
+
(tool === 'npm' && vars.NPM_CONFIG_CACHE) ||
|
|
119
|
+
!(await env.which(tool))
|
|
120
|
+
) {
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
const result = await env.run(argv, { timeoutMs: 5000 });
|
|
124
|
+
if (result.code !== 0) {
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
let values;
|
|
128
|
+
try {
|
|
129
|
+
values =
|
|
130
|
+
tool === 'go'
|
|
131
|
+
? JSON.parse(result.stdout)
|
|
132
|
+
: { [keys[0]]: result.stdout.trim().split('\n').at(-1) };
|
|
133
|
+
} catch {
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
applyQuery(vars, keys, values, env.path);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { OPEN_PATHS_SCRIPT } from './process-scripts.js';
|
|
2
|
+
import { trace } from '../exec.js';
|
|
3
|
+
|
|
4
|
+
export function parseProcessProbe(result) {
|
|
5
|
+
const gids = new Map(
|
|
6
|
+
result.stderr
|
|
7
|
+
.split('\n')
|
|
8
|
+
.filter((line) => line.startsWith('DSS_GID|'))
|
|
9
|
+
.map((line) => {
|
|
10
|
+
const [, pid, gid] = line.split('|');
|
|
11
|
+
return [Number(pid), gid ? Number(gid) : null];
|
|
12
|
+
})
|
|
13
|
+
);
|
|
14
|
+
return {
|
|
15
|
+
paths: new Set(
|
|
16
|
+
result.stdout
|
|
17
|
+
.split('\n')
|
|
18
|
+
.filter((line) => line.startsWith('/'))
|
|
19
|
+
.map((line) => line.replace(/ \(deleted\)$/, ''))
|
|
20
|
+
),
|
|
21
|
+
unreadable: result.stderr
|
|
22
|
+
.split('\n')
|
|
23
|
+
.filter((line) => line.startsWith('DSS_UNREADABLE|'))
|
|
24
|
+
.map((line) => {
|
|
25
|
+
const [, pid, uid, name, ...command] = line.split('|');
|
|
26
|
+
return {
|
|
27
|
+
pid: Number(pid),
|
|
28
|
+
uid: uid ? Number(uid) : null,
|
|
29
|
+
gid: gids.get(Number(pid)) ?? null,
|
|
30
|
+
name,
|
|
31
|
+
command: command.join('|'),
|
|
32
|
+
};
|
|
33
|
+
}),
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Root without ptrace can often inspect a dumpable process as its own UID/GID. */
|
|
38
|
+
export async function retryProcessOwners(executor, evidence) {
|
|
39
|
+
const groups = new Map();
|
|
40
|
+
for (const proc of evidence.unreadable) {
|
|
41
|
+
if (
|
|
42
|
+
!Number.isInteger(proc.uid) ||
|
|
43
|
+
!Number.isInteger(proc.gid) ||
|
|
44
|
+
proc.uid === 0
|
|
45
|
+
) {
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
const key = `${proc.uid}:${proc.gid}`;
|
|
49
|
+
groups.set(key, [...(groups.get(key) ?? []), proc]);
|
|
50
|
+
}
|
|
51
|
+
const unresolved = new Map(
|
|
52
|
+
evidence.unreadable.map((proc) => [proc.pid, proc])
|
|
53
|
+
);
|
|
54
|
+
for (const [identity, procs] of groups) {
|
|
55
|
+
const [uid, gid] = identity.split(':');
|
|
56
|
+
trace(
|
|
57
|
+
'process inspection as owning uid/gid',
|
|
58
|
+
identity,
|
|
59
|
+
procs.map((proc) => proc.pid)
|
|
60
|
+
);
|
|
61
|
+
const result = await executor.run([
|
|
62
|
+
'setpriv',
|
|
63
|
+
`--reuid=${uid}`,
|
|
64
|
+
`--regid=${gid}`,
|
|
65
|
+
'--clear-groups',
|
|
66
|
+
'sh',
|
|
67
|
+
'-c',
|
|
68
|
+
OPEN_PATHS_SCRIPT,
|
|
69
|
+
'dss',
|
|
70
|
+
...procs.map((proc) => `/proc/${proc.pid}`),
|
|
71
|
+
]);
|
|
72
|
+
const retried = parseProcessProbe(result);
|
|
73
|
+
for (const target of retried.paths) {
|
|
74
|
+
evidence.paths.add(target);
|
|
75
|
+
}
|
|
76
|
+
if (result.code === 0 || retried.unreadable.length) {
|
|
77
|
+
for (const proc of procs) {
|
|
78
|
+
unresolved.delete(proc.pid);
|
|
79
|
+
}
|
|
80
|
+
for (const proc of retried.unreadable) {
|
|
81
|
+
unresolved.set(proc.pid, proc);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return { paths: evidence.paths, unreadable: [...unresolved.values()] };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function inspectionHint(unreadable) {
|
|
89
|
+
if (!unreadable.length) {
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
const identities = unreadable
|
|
93
|
+
.slice(0, 5)
|
|
94
|
+
.map(
|
|
95
|
+
(proc) =>
|
|
96
|
+
`${proc.name || 'unknown'} pid ${proc.pid} uid ${proc.uid ?? '?'} gid ${proc.gid ?? '?'}`
|
|
97
|
+
)
|
|
98
|
+
.join('; ');
|
|
99
|
+
return `${unreadable.length} processes are unreadable (${identities}). Activity uncertainty is scoped to daemon storage and accessible paths. Inspect as the owning UID/GID (setpriv), or grant CAP_SYS_PTRACE for read-only inspection; root alone may lack that capability.`;
|
|
100
|
+
}
|