ccdeck 3.29.11 → 3.31.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/README.md +4 -3
- package/bin/agent-dag.js +67 -118
- package/bin/cli/help.js +67 -0
- package/bin/cli/login-item.js +182 -0
- package/bin/cli/one-shot.js +254 -0
- package/bin/cli/package.js +39 -0
- package/bin/cli/pulse.js +98 -0
- package/bin/cli/restart.js +179 -0
- package/bin/cli/screen.js +166 -0
- package/bin/cli/second-start.js +120 -0
- package/bin/cli/startup.js +459 -0
- package/bin/cli/uninstall.js +169 -0
- package/bin/deck.js +112 -1559
- package/dist/web/assets/BrowserWatchModal-n63GjLhv.js +1 -0
- package/dist/web/assets/UsageHistoryModal-DEwBX4Fk.js +1 -0
- package/dist/web/assets/index-CZJf1Cpp.js +299 -0
- package/dist/web/assets/index-D4q1ElXU.css +1 -0
- package/dist/web/index.html +2 -2
- package/hook/hook.js +15 -14
- package/package.json +1 -1
- package/release-notes.json +40 -0
- package/src/server/account-projects.mjs +25 -11
- package/src/server/account-routes.mjs +254 -0
- package/src/server/activity.mjs +3 -3
- package/src/server/append-budget.mjs +185 -0
- package/src/server/append-failures.mjs +147 -0
- package/src/server/args.mjs +37 -5
- package/src/server/atomic-write.mjs +300 -0
- package/src/server/block-notify.mjs +0 -5
- package/src/server/boot-lock.mjs +1 -1
- package/src/server/brand.mjs +7 -6
- package/src/server/browser-react.mjs +10 -2
- package/src/server/browser-watch-decks.mjs +146 -0
- package/src/server/browser-watch-feed.mjs +69 -0
- package/src/server/browser-watch-log.mjs +173 -0
- package/src/server/browser-watch-mtime.mjs +18 -0
- package/src/server/browser-watch-relay.mjs +112 -0
- package/src/server/browser-watch-routes.mjs +172 -0
- package/src/server/browser-watch-seen.mjs +127 -0
- package/src/server/browser-watch-store.mjs +14 -143
- package/src/server/browser-watch.mjs +278 -626
- package/src/server/canonical-path.mjs +150 -0
- package/src/server/ccusage-failure.mjs +64 -0
- package/src/server/ccusage-install.mjs +459 -0
- package/src/server/ccusage-runner.mjs +271 -0
- package/src/server/ccusage.mjs +21 -756
- package/src/server/claude-accounts.mjs +220 -464
- package/src/server/claude-collector.mjs +189 -0
- package/src/server/claude-dir.mjs +7 -3
- package/src/server/claude-identity.mjs +5 -4
- package/src/server/claude-reset-credits.mjs +130 -0
- package/src/server/claude-verdicts.mjs +174 -0
- package/src/server/clear-route.mjs +191 -0
- package/src/server/codex-auth.mjs +4 -4
- package/src/server/codex-dir.mjs +52 -8
- package/src/server/codex-enrichment.mjs +310 -0
- package/src/server/codex-quota.mjs +5 -3
- package/src/server/codex-translate.mjs +407 -0
- package/src/server/codex-usage.mjs +8 -5
- package/src/server/codex-watch.mjs +496 -0
- package/src/server/cswap-auto-loop.mjs +342 -0
- package/src/server/cswap-auto-readers.mjs +303 -0
- package/src/server/cswap-auto.mjs +51 -609
- package/src/server/cswap-install.mjs +69 -265
- package/src/server/cswap-layout.mjs +242 -0
- package/src/server/deck-home.mjs +2 -2
- package/src/server/deck-prefs.mjs +58 -184
- package/src/server/deck-scope.mjs +52 -0
- package/src/server/desktop-update-routes.mjs +75 -0
- package/src/server/detach.mjs +78 -0
- package/src/server/discovery.mjs +298 -0
- package/src/server/event-log.mjs +328 -0
- package/src/server/event-pipeline.mjs +235 -0
- package/src/server/event-ring.mjs +214 -0
- package/src/server/event-routes.mjs +352 -0
- package/src/server/event-sink.mjs +26 -0
- package/src/server/exec-children.mjs +128 -0
- package/src/server/exec-not-found.mjs +213 -0
- package/src/server/exec-output.mjs +23 -0
- package/src/server/exec-spec.mjs +341 -0
- package/src/server/exec.mjs +74 -661
- package/src/server/health-route.mjs +42 -0
- package/src/server/hook-challenge.mjs +37 -0
- package/src/server/http-io.mjs +104 -0
- package/src/server/index.mjs +174 -7620
- package/src/server/install-layout.mjs +506 -0
- package/src/server/installer.mjs +79 -608
- package/src/server/jsonl-chunks.mjs +139 -0
- package/src/server/lan-accounts.mjs +27 -0
- package/src/server/lan-beacon.mjs +457 -0
- package/src/server/lan-call.mjs +239 -0
- package/src/server/lan-copies.mjs +270 -0
- package/src/server/lan-deck.mjs +583 -0
- package/src/server/lan-dials.mjs +161 -0
- package/src/server/lan-engine.mjs +475 -1024
- package/src/server/lan-hearing.mjs +49 -0
- package/src/server/lan-inbound.mjs +100 -0
- package/src/server/lan-invite-offer.mjs +77 -0
- package/src/server/lan-invite.mjs +207 -0
- package/src/server/lan-lines.mjs +73 -0
- package/src/server/lan-manifest.mjs +74 -0
- package/src/server/lan-reach.mjs +82 -69
- package/src/server/lan-requests.mjs +249 -0
- package/src/server/lan-round-record.mjs +45 -0
- package/src/server/lan-round-timer.mjs +62 -0
- package/src/server/lan-routes.mjs +143 -0
- package/src/server/lan-serve.mjs +112 -0
- package/src/server/lan-socket.mjs +243 -925
- package/src/server/lan-sync.mjs +58 -1026
- package/src/server/lan-tailnet-poll.mjs +57 -0
- package/src/server/lan-turns.mjs +80 -0
- package/src/server/lan-wire.mjs +620 -0
- package/src/server/lifecycle.mjs +421 -0
- package/src/server/listen.mjs +110 -0
- package/src/server/live-decks.mjs +138 -0
- package/src/server/load-average.mjs +32 -0
- package/src/server/log-election.mjs +235 -0
- package/src/server/log-replay.mjs +374 -0
- package/src/server/log-writer.mjs +72 -557
- package/src/server/login-service.mjs +3 -2
- package/src/server/macmon.mjs +1 -1
- package/src/server/memory-files.mjs +163 -0
- package/src/server/memory-metrics.mjs +178 -0
- package/src/server/metrics-history.mjs +94 -0
- package/src/server/metrics-run.mjs +100 -0
- package/src/server/music-routes.mjs +134 -0
- package/src/server/network-metrics.mjs +5 -3
- package/src/server/network-sampler.mjs +256 -0
- package/src/server/npm-latest.mjs +385 -0
- package/src/server/npm-upgrade.mjs +352 -0
- package/src/server/npx.mjs +41 -7
- package/src/server/output-watch.mjs +5 -4
- package/src/server/pinned-build.mjs +110 -0
- package/src/server/prefs-give-back.mjs +206 -0
- package/src/server/prefs-path.mjs +14 -0
- package/src/server/prefs-refusal.mjs +86 -0
- package/src/server/prefs-routes.mjs +99 -0
- package/src/server/prefs-set-aside.mjs +86 -0
- package/src/server/prefs-state.mjs +70 -0
- package/src/server/process-command.mjs +104 -0
- package/src/server/process-list.mjs +547 -0
- package/src/server/provider-status.mjs +307 -0
- package/src/server/pulse-line.mjs +132 -0
- package/src/server/purge-key.mjs +7 -7
- package/src/server/quota-cli.mjs +174 -0
- package/src/server/quota-oauth.mjs +190 -0
- package/src/server/quota-shape.mjs +229 -0
- package/src/server/quota-store-resets.mjs +206 -0
- package/src/server/quota.mjs +123 -509
- package/src/server/relay-guard.mjs +3 -3
- package/src/server/request-gates.mjs +441 -0
- package/src/server/restart-note.mjs +180 -0
- package/src/server/retire-sound-hook.mjs +3 -3
- package/src/server/ring-bounds.mjs +215 -0
- package/src/server/running-deck.mjs +1 -1
- package/src/server/running-version.mjs +24 -0
- package/src/server/self-update.mjs +39 -1371
- package/src/server/session-enrichment.mjs +622 -0
- package/src/server/session-read-gate.mjs +49 -0
- package/src/server/session-tracking.mjs +242 -0
- package/src/server/sse-clients.mjs +222 -0
- package/src/server/stale-discovery.mjs +30 -0
- package/src/server/static-serve.mjs +81 -0
- package/src/server/supervisor.mjs +73 -12
- package/src/server/system-metrics.mjs +169 -1935
- package/src/server/term.mjs +11 -249
- package/src/server/thermal-metrics.mjs +520 -0
- package/src/server/thermal-sampler.mjs +204 -0
- package/src/server/token-redact.mjs +123 -0
- package/src/server/transcript-gate.mjs +107 -0
- package/src/server/transcript-scan.mjs +563 -0
- package/src/server/usage-routes.mjs +108 -0
- package/src/server/uv-bootstrap.mjs +1 -1
- package/src/server/wordmark.mjs +127 -0
- package/dist/web/assets/BrowserWatchModal-CyAFTvAY.js +0 -1
- package/dist/web/assets/UsageHistoryModal-KJfli0oa.js +0 -1
- package/dist/web/assets/index-Ck2RAla3.css +0 -1
- package/dist/web/assets/index-jmrcEsCj.js +0 -298
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
// What of a process's command line the Machine panel may show: the arguments,
|
|
2
|
+
// with the executable that carries them taken off the front (commandTail), and
|
|
3
|
+
// then with the shapes a secret takes removed and the rest capped
|
|
4
|
+
// (redactCommand).
|
|
5
|
+
//
|
|
6
|
+
// Moved out of process-list.mjs unchanged. attachDetail there runs both on
|
|
7
|
+
// every candidate's argument vector before it leaves that module, because
|
|
8
|
+
// /api/system/processes answers anything that can reach the loopback port;
|
|
9
|
+
// process-list.mjs re-exports CMD_MAX, redactCommand and commandTail, which is
|
|
10
|
+
// where the suite reaches them.
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* An argument vector with the parts that must not be looked at taken out, and
|
|
14
|
+
* the parts nobody reads cut off.
|
|
15
|
+
*
|
|
16
|
+
* THIS IS A BLOCKLIST AND A BLOCKLIST LEAKS. It is written down here rather
|
|
17
|
+
* than implied, because the alternative — `comm`, which is what this replaced —
|
|
18
|
+
* leaked nothing and told the reader nothing either. What it removes is the
|
|
19
|
+
* shapes a secret actually takes on a command line; what it cannot remove is a
|
|
20
|
+
* secret that looks like an ordinary word, and no rule here pretends otherwise.
|
|
21
|
+
*
|
|
22
|
+
* The cap is the other half, and it is not a nicety. Measured on this machine:
|
|
23
|
+
* one Chrome helper's argv is 906 characters of `--field-trial-handle`,
|
|
24
|
+
* base64 `--gpu-preferences` and shared-memory handles. Forty of those is 36 KB
|
|
25
|
+
* on the wire every four seconds to render a column nobody can read. What
|
|
26
|
+
* identifies a process is its first few arguments — `ng serve admin-portal
|
|
27
|
+
* --port 44440` — and that is what fits.
|
|
28
|
+
*/
|
|
29
|
+
export const CMD_MAX = 180;
|
|
30
|
+
|
|
31
|
+
const SECRET_FLAG =
|
|
32
|
+
/(-{1,2}[\w.-]*(?:token|password|passwd|secret|api[-_]?key|apikey|auth|credential|bearer|cookie|session[-_]?id|private[-_]?key)[\w.-]*)(=|\s+)(\S+)/gi;
|
|
33
|
+
// The shapes that are a secret on their own, with no flag in front of them.
|
|
34
|
+
const BARE_SECRET =
|
|
35
|
+
/\b(?:sk-[A-Za-z0-9_-]{16,}|gh[pousr]_[A-Za-z0-9]{16,}|xox[baprs]-[A-Za-z0-9-]{10,}|AKIA[0-9A-Z]{12,}|eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{5,})/g;
|
|
36
|
+
// user:password@host, in any URL. The password goes; the rest is a location.
|
|
37
|
+
const URL_CREDENTIALS = /\b([a-z][a-z0-9+.-]*:\/\/[^\s:@/]+):[^\s@/]+@/gi;
|
|
38
|
+
|
|
39
|
+
export function redactCommand(argv, home = "") {
|
|
40
|
+
let s = String(argv ?? "").trim();
|
|
41
|
+
if (!s) return "";
|
|
42
|
+
s = s.replace(SECRET_FLAG, (_, flag, sep, value) => `${flag}${sep === "=" ? "=" : " "}\u2022\u2022\u2022`);
|
|
43
|
+
s = s.replace(URL_CREDENTIALS, "$1:\u2022\u2022\u2022@");
|
|
44
|
+
s = s.replace(BARE_SECRET, "\u2022\u2022\u2022");
|
|
45
|
+
// `~` last, so a home path inside a redacted value is already gone. Only a
|
|
46
|
+
// whole path segment, so a user called `con` cannot rewrite the middle of an
|
|
47
|
+
// unrelated word.
|
|
48
|
+
if (home && home.length > 1) s = s.split(home + "/").join("~/").split(home + " ").join("~ ");
|
|
49
|
+
if (s.length > CMD_MAX) s = s.slice(0, CMD_MAX - 1).trimEnd() + "\u2026";
|
|
50
|
+
return s;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* The ARGUMENTS, with the executable that carries them taken off the front.
|
|
55
|
+
*
|
|
56
|
+
* The name column already says `Google Chrome Helper`; repeating
|
|
57
|
+
* `/Applications/Google Chrome.app/Contents/Frameworks/Google Chrome
|
|
58
|
+
* Framework.framework/Versions/152.0.7977.76/Helpers/Google Chrome
|
|
59
|
+
* Helper.app/Contents/MacOS/Google Chrome Helper` beside it spends the entire
|
|
60
|
+
* 180-character budget on a path the reader can already see the end of, and
|
|
61
|
+
* cuts off `--type=renderer`, which is the part that says WHICH helper this is.
|
|
62
|
+
*
|
|
63
|
+
* argv[0] cannot be found by splitting on whitespace — on macOS the executable
|
|
64
|
+
* path routinely contains spaces, and `/Applications/Google Chrome.app/…` would
|
|
65
|
+
* be cut at "Google". The name from the first `ps` call is what resolves it:
|
|
66
|
+
* argv begins with a path whose last segment IS that name.
|
|
67
|
+
*
|
|
68
|
+
* "Last segment" is the whole of the rule, and the first version of this got it
|
|
69
|
+
* wrong in a way only the render showed. A plain `indexOf` cuts at the FIRST
|
|
70
|
+
* occurrence, and a macOS bundle contains the name twice — `…/Helpers/Google
|
|
71
|
+
* Chrome Helper.app/Contents/MacOS/Google Chrome Helper` — so every browser row
|
|
72
|
+
* read `.app/Contents/MacOS/Google Chrome Helper --type=gpu-process`, having
|
|
73
|
+
* spent the budget on the half of the path it was supposed to remove. A plain
|
|
74
|
+
* `lastIndexOf` is wrong the other way: `node /srv/node_modules/x` would cut
|
|
75
|
+
* inside `node_modules`.
|
|
76
|
+
*
|
|
77
|
+
* So: the first occurrence that a path segment actually ends at — preceded by a
|
|
78
|
+
* separator or nothing, followed by whitespace or nothing. `node /srv/app.js
|
|
79
|
+
* --port 3000` leaves `/srv/app.js --port 3000`; `/usr/local/bin/dotnet exec
|
|
80
|
+
* --runtimeconfig …` leaves `exec --runtimeconfig …`, which is what identifies
|
|
81
|
+
* it.
|
|
82
|
+
*
|
|
83
|
+
* When the name is not in argv at all — a process that rewrote argv[0], or a
|
|
84
|
+
* `comm` that Linux truncated at 15 characters — the first whitespace token
|
|
85
|
+
* goes only if it looks like a path, because dropping the first word of
|
|
86
|
+
* something already short would take the only word there was.
|
|
87
|
+
*/
|
|
88
|
+
export function commandTail(argv, name) {
|
|
89
|
+
const s = String(argv ?? "").trim();
|
|
90
|
+
if (!s) return "";
|
|
91
|
+
const n = String(name ?? "").trim();
|
|
92
|
+
if (n) {
|
|
93
|
+
for (let at = s.indexOf(n); at >= 0; at = s.indexOf(n, at + 1)) {
|
|
94
|
+
const before = at === 0 ? "" : s[at - 1];
|
|
95
|
+
const after = s[at + n.length] ?? "";
|
|
96
|
+
if ((before === "" || before === "/" || before === "\\" || /\s/.test(before))
|
|
97
|
+
&& (after === "" || /\s/.test(after))) {
|
|
98
|
+
return s.slice(at + n.length).trim();
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
const first = s.split(/\s+/)[0] ?? "";
|
|
103
|
+
return first.includes("/") ? s.slice(first.length).trim() : s;
|
|
104
|
+
}
|
|
@@ -0,0 +1,547 @@
|
|
|
1
|
+
// The process list: which rows /api/system/processes answers with, and how each
|
|
2
|
+
// platform is asked for them.
|
|
3
|
+
//
|
|
4
|
+
// Moved out of system-metrics.mjs unchanged, because it is the one reading there
|
|
5
|
+
// that never rides the sampler's timers. It is read on demand only — while the
|
|
6
|
+
// process dialog is open — keeps its own one-child-at-a-time guard and its own
|
|
7
|
+
// Windows CPU baseline (#544), and shares nothing with the ambient readings but
|
|
8
|
+
// the command runner. system-metrics.mjs re-exports readProcesses, which is where
|
|
9
|
+
// index.mjs asks for it, and its stopSystemMetrics calls resetProcessList so a
|
|
10
|
+
// reset still clears this list along with everything else.
|
|
11
|
+
import os from "node:os";
|
|
12
|
+
import { run } from "./metrics-run.mjs";
|
|
13
|
+
// What of a command line may leave this module: see process-command.mjs.
|
|
14
|
+
import { CMD_MAX, commandTail, redactCommand } from "./process-command.mjs";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* How far down each ranking the payload reaches.
|
|
18
|
+
*
|
|
19
|
+
* The panel draws eight rows and decides their order on the client (#739), so
|
|
20
|
+
* whichever column it ranks by has to be rankable from the rows it was handed.
|
|
21
|
+
* `ps` returns a CPU-sorted list, and cutting that at eight and then sorting
|
|
22
|
+
* those eight by memory produced a table that was honest about its rows and
|
|
23
|
+
* wrong about its question: the machine's heaviest memory consumer need never
|
|
24
|
+
* have appeared in a CPU top eight at all. Same shape as #492 — a list that is
|
|
25
|
+
* never empty, never errors, and is not the rows being asked for.
|
|
26
|
+
*
|
|
27
|
+
* So what goes over the wire is a candidate SET rather than a ranking.
|
|
28
|
+
* pickCandidates takes this many by CPU and this many by memory and sends the
|
|
29
|
+
* union, which makes the true top eight of either column present by
|
|
30
|
+
* construction. Wide enough that drawing more rows later cannot quietly
|
|
31
|
+
* re-break that, small enough that the whole thing stays a few kilobytes of
|
|
32
|
+
* four-field rows, fetched only while the panel is open.
|
|
33
|
+
*/
|
|
34
|
+
const CANDIDATE_N = 40;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* The rows worth sending, given that the ordering happens on the client.
|
|
38
|
+
*
|
|
39
|
+
* A union of two rankings, deduplicated by object identity rather than by pid:
|
|
40
|
+
* parseGetProcessJson falls back to pid 0 for a row whose `Id` did not parse,
|
|
41
|
+
* more than one row can do that, and a pid-keyed set would drop real processes
|
|
42
|
+
* in order to deduplicate a placeholder.
|
|
43
|
+
*
|
|
44
|
+
* An unknown CPU sorts last here, for the same reason the column prints a dash
|
|
45
|
+
* rather than a zero — a Windows first reading has no percentage yet, and
|
|
46
|
+
* unknown is not idle and is not busiest either. Such a row still reaches the
|
|
47
|
+
* payload through the memory half, which is the reading that does exist on that
|
|
48
|
+
* pass.
|
|
49
|
+
*/
|
|
50
|
+
export function pickCandidates(rows, limit = CANDIDATE_N) {
|
|
51
|
+
const byCpu = [...rows].sort((a, b) => (b.cpu ?? -1) - (a.cpu ?? -1) || b.mem - a.mem);
|
|
52
|
+
const byMem = [...rows].sort((a, b) => b.mem - a.mem || (b.cpu ?? -1) - (a.cpu ?? -1));
|
|
53
|
+
const out = byCpu.slice(0, limit);
|
|
54
|
+
const seen = new Set(out);
|
|
55
|
+
for (const r of byMem.slice(0, limit)) if (!seen.has(r)) out.push(r);
|
|
56
|
+
return out;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* The `ps` argument list, which is not the same list on both Unixes.
|
|
61
|
+
*
|
|
62
|
+
* `-r` was shipped for both and means two different things. On BSD it sorts the
|
|
63
|
+
* output by current CPU, which is the ordering the panel is built around. On
|
|
64
|
+
* Linux procps it is *"restrict the selection to only running processes"* — a
|
|
65
|
+
* filter on state `R`, applied in PID order. A Linux deck therefore listed
|
|
66
|
+
* whatever happened to be on a CPU at the instant of the sample: usually one or
|
|
67
|
+
* two rows on an idle machine, and never the busiest ones, since a process
|
|
68
|
+
* pinning a core while blocked on I/O sits in `D` and one merely burning CPU
|
|
69
|
+
* over time is normally caught in `S`. Nothing errored and nothing was empty,
|
|
70
|
+
* which is why it survived two releases (#492).
|
|
71
|
+
*
|
|
72
|
+
* `--sort=-pcpu` is procps' own way to say what `-r` says on BSD. The column
|
|
73
|
+
* order is deliberately identical on both so one parser reads both, and `comm`
|
|
74
|
+
* stays last so a name containing a space survives intact.
|
|
75
|
+
*
|
|
76
|
+
* Keyed on linux rather than on darwin, because linux is the platform that is
|
|
77
|
+
* wrong: `-r` sorts on every BSD, while `--sort` is a procps long option that
|
|
78
|
+
* would make FreeBSD and OpenBSD exit non-zero. This way the only branch that
|
|
79
|
+
* changes is the one that was broken.
|
|
80
|
+
*
|
|
81
|
+
* Pure and exported for the same reason the parsers are: the command
|
|
82
|
+
* construction is the part that differs per platform, and a fixture cannot
|
|
83
|
+
* prove which flags were passed to produce it.
|
|
84
|
+
*/
|
|
85
|
+
export function psArgs(platform = process.platform) {
|
|
86
|
+
// SEVEN FIELDS, THE SAME SEVEN ON BOTH, and `args` last because it is the one
|
|
87
|
+
// that contains spaces. `etime` is `[[DD-]HH:]MM:SS` on both and `rss` is
|
|
88
|
+
// kibibytes on both, so one parser still reads both — which is why the thread
|
|
89
|
+
// count is fetched separately (attachDetail) rather than as an eighth column:
|
|
90
|
+
// procps spells it `nlwp` and BSD has no keyword for it at all, and one
|
|
91
|
+
// divergent column would have cost the shared parser this list is built on.
|
|
92
|
+
//
|
|
93
|
+
// `args` REPLACES `comm`, which is a change with a cost, and redactCommand is
|
|
94
|
+
// the payment. `comm` was chosen so that argv — and any token on it — could
|
|
95
|
+
// not reach the panel; that made every `node`, `dotnet` and `python` on the
|
|
96
|
+
// machine indistinguishable from every other one, which is most of what a
|
|
97
|
+
// reader opens this list to tell apart. The argument vector is redacted and
|
|
98
|
+
// capped before it leaves this module, and the short name is derived from
|
|
99
|
+
// argv[0] rather than asked for a second time.
|
|
100
|
+
if (platform === "linux") return ["-eo", "pid,pcpu,pmem,rss,etime,user:24,comm", "--sort=-pcpu"];
|
|
101
|
+
// BSD/macOS: `-c` prints the accounting name rather than the argument vector,
|
|
102
|
+
// and `-r` sorts by current CPU.
|
|
103
|
+
return ["-Aceo", "pid,pcpu,pmem,rss,etime,user,comm", "-r"];
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* The second call: how many threads each candidate has, and what it was
|
|
108
|
+
* actually launched with.
|
|
109
|
+
*
|
|
110
|
+
* Two facts, one child, and it has to be a second call for two separate
|
|
111
|
+
* reasons. The thread count has no shared spelling — procps says `nlwp` and BSD
|
|
112
|
+
* has no keyword for it at all, only the `-M` listing — so an eighth column
|
|
113
|
+
* would have cost the single parser psArgs is built around. And `args` cannot
|
|
114
|
+
* sit beside `comm`: only one field in an `-o` list may contain spaces, because
|
|
115
|
+
* only the last one is unambiguous.
|
|
116
|
+
*
|
|
117
|
+
* Scoped to the candidate pids rather than to the machine, which is what makes
|
|
118
|
+
* it cheap: measured here, `ps -M -p` over forty pids is 0.15s against 0.10s
|
|
119
|
+
* for the whole-machine call it follows. `top -stats th,ports` would have
|
|
120
|
+
* answered both in one go and takes 4.25s on an idle machine.
|
|
121
|
+
*/
|
|
122
|
+
export function psDetailArgs(pids, platform = process.platform) {
|
|
123
|
+
const list = pids.join(",");
|
|
124
|
+
// procps: `nlwp` is free here, and `args` is last as ever.
|
|
125
|
+
if (platform === "linux") return ["-o", "pid,nlwp,args", "-p", list];
|
|
126
|
+
// BSD: `-M` lists each process followed by one line per thread, and the
|
|
127
|
+
// process line carries the untruncated argument vector. Counting the lines is
|
|
128
|
+
// the thread count; the process line is the argv. One child, both answers.
|
|
129
|
+
return ["-M", "-p", list];
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* `ps -M` output: a process line, then one line per thread, per pid.
|
|
134
|
+
*
|
|
135
|
+
* The two kinds are told apart by column one. A process line begins with the
|
|
136
|
+
* owning user and a thread line begins with spaces — `ps` leaves USER, TT and
|
|
137
|
+
* COMMAND blank on a thread because a thread has none of its own. So the count
|
|
138
|
+
* is "lines mentioning this pid, less the one that named it", and the argv is
|
|
139
|
+
* whatever the named line ended with.
|
|
140
|
+
*
|
|
141
|
+
* A kernel thread and an exiting process print a bracketed command
|
|
142
|
+
* (`[kworker/0:1]`, `(ccusage)`); that is `ps` reporting a state and it is left
|
|
143
|
+
* exactly as it came.
|
|
144
|
+
*/
|
|
145
|
+
export function parsePsThreadsBsd(text) {
|
|
146
|
+
const out = new Map();
|
|
147
|
+
for (const line of String(text ?? "").split("\n")) {
|
|
148
|
+
if (!line.trim()) continue;
|
|
149
|
+
// USER PID TT %CPU STAT PRI STIME UTIME COMMAND — eight fixed fields, and
|
|
150
|
+
// COMMAND is everything after them.
|
|
151
|
+
const proc = /^(\S+)\s+(\d+)\s+\S+\s+[\d.,]+\s+\S+\s+\S+\s+\S+\s+\S+\s*(.*)$/.exec(line);
|
|
152
|
+
if (proc) {
|
|
153
|
+
if (proc[1] === "USER") continue; // the header
|
|
154
|
+
const pid = Number(proc[2]);
|
|
155
|
+
const row = out.get(pid) ?? { threads: 0, cmd: "" };
|
|
156
|
+
row.cmd = proc[3].trim();
|
|
157
|
+
out.set(pid, row);
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
const thread = /^\s+(\d+)\s/.exec(line);
|
|
161
|
+
if (!thread) continue;
|
|
162
|
+
const pid = Number(thread[1]);
|
|
163
|
+
const row = out.get(pid) ?? { threads: 0, cmd: "" };
|
|
164
|
+
row.threads += 1;
|
|
165
|
+
out.set(pid, row);
|
|
166
|
+
}
|
|
167
|
+
return out;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** `ps -o pid,nlwp,args` output, which is two numbers and then the rest. */
|
|
171
|
+
export function parsePsThreadsProcps(text) {
|
|
172
|
+
const out = new Map();
|
|
173
|
+
for (const line of String(text ?? "").split("\n")) {
|
|
174
|
+
const m = /^\s*(\d+)\s+(\d+)\s*(.*)$/.exec(line);
|
|
175
|
+
if (!m) continue;
|
|
176
|
+
out.set(Number(m[1]), { threads: Number(m[2]), cmd: m[3].trim() });
|
|
177
|
+
}
|
|
178
|
+
return out;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* The pieces of a `ps` ELAPSED field, which is `[[DD-]HH:]MM:SS` on both Unixes.
|
|
183
|
+
*
|
|
184
|
+
* Returns seconds, or null for anything that is not that shape — a header line
|
|
185
|
+
* that slipped through, a locale doing something unexpected. Null prints as a
|
|
186
|
+
* dash rather than as "0s", because a process that has been up for no time and
|
|
187
|
+
* a process whose uptime could not be read are different facts.
|
|
188
|
+
*/
|
|
189
|
+
export function elapsedSeconds(text) {
|
|
190
|
+
const m = /^(?:(\d+)-)?(?:(\d+):)?(\d+):(\d+)$/.exec(String(text ?? "").trim());
|
|
191
|
+
if (!m) return null;
|
|
192
|
+
const [, d, h, min, sec] = m;
|
|
193
|
+
return (Number(d ?? 0) * 86400) + (Number(h ?? 0) * 3600) + (Number(min) * 60) + Number(sec);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// The command column — argv without its executable, redacted and capped before
|
|
197
|
+
// it leaves this module (#807): process-command.mjs.
|
|
198
|
+
export { CMD_MAX, commandTail, redactCommand };
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Rows out of `ps -o pid,pcpu,pmem,comm`, in that column order on both Unixes —
|
|
202
|
+
* see psArgs for how each platform is asked for it.
|
|
203
|
+
*
|
|
204
|
+
* `pcpu` is a percentage of ONE core on both, so a multi-threaded process runs
|
|
205
|
+
* past 100 and that is information rather than an error: 157 is one and a half
|
|
206
|
+
* cores. cpuFromDeltas puts the Windows column on this same scale.
|
|
207
|
+
*
|
|
208
|
+
* There is no row limit in the query because neither `ps` has one and `run`
|
|
209
|
+
* deliberately never inherits a shell, so there is no `| head` to pipe into.
|
|
210
|
+
*
|
|
211
|
+
* And there is none by default here either, which is a change: the loop used to
|
|
212
|
+
* stop at eight, and stopping at eight is what made the memory ranking a lie
|
|
213
|
+
* once the panel could ask for one (#739). Selection belongs to pickCandidates,
|
|
214
|
+
* which cannot rank a column out of rows this function has already thrown away.
|
|
215
|
+
* The cost is a regex over every line of `ps` — a few hundred on a busy
|
|
216
|
+
* machine, once every four seconds and only while the panel is open — against a
|
|
217
|
+
* string that has already been read and allocated. `limit` stays for callers
|
|
218
|
+
* that do want a truncation, which is now only the tests.
|
|
219
|
+
*/
|
|
220
|
+
export function parsePsProcesses(text, limit = Infinity) {
|
|
221
|
+
const lines = String(text ?? "").trim().split("\n");
|
|
222
|
+
const out = [];
|
|
223
|
+
for (const line of lines.slice(1)) { // drop the header row
|
|
224
|
+
// Both separators, for the reason swapFromSysctl gives: C_LOCALE is the
|
|
225
|
+
// fix, and this is what keeps a stripped environment from emptying the
|
|
226
|
+
// panel. `%CPU` and `%MEM` are percentages printed with %.1f, so a comma in
|
|
227
|
+
// either can only be the decimal point.
|
|
228
|
+
//
|
|
229
|
+
// Seven fields, and only the last of them may contain a space — which is
|
|
230
|
+
// why `comm` is last and why `args` is not here at all (psDetailArgs).
|
|
231
|
+
// `rss` is an integer of kibibytes, `etime` and `user` cannot contain
|
|
232
|
+
// whitespace, so the shape stays unambiguous with three more columns in it.
|
|
233
|
+
const m = /^\s*(\d+)\s+([\d.,]+)\s+([\d.,]+)\s+(\d+)\s+(\S+)\s+(\S+)\s+(.+?)\s*$/.exec(line);
|
|
234
|
+
if (!m) continue;
|
|
235
|
+
const num = v => Number(v.replace(",", "."));
|
|
236
|
+
out.push({
|
|
237
|
+
pid: Number(m[1]),
|
|
238
|
+
cpu: num(m[2]),
|
|
239
|
+
mem: num(m[3]),
|
|
240
|
+
// RSS IS NOT WHAT ACTIVITY MONITOR CALLS MEMORY, and on macOS it is not
|
|
241
|
+
// close. Measured here against `top`'s MEM on the same processes at the
|
|
242
|
+
// same moment: `dotnet` 37 MB against 534, `rider` 1300 against 3833,
|
|
243
|
+
// one Brave renderer 240 against 153 — ratios from 0.02 to 1.57. The
|
|
244
|
+
// column Activity Monitor draws is `phys_footprint`, which no
|
|
245
|
+
// unprivileged one-shot command reports; `top -stats mem` has it and
|
|
246
|
+
// takes 4.25 seconds on an idle machine, against 0.10 for this call.
|
|
247
|
+
// So the figure is resident set size, the panel says so, and it is the
|
|
248
|
+
// same measurement on all three platforms rather than a different one
|
|
249
|
+
// per OS. On Linux and Windows it is also the figure their own tools
|
|
250
|
+
// show.
|
|
251
|
+
rssBytes: Number(m[4]) * 1024,
|
|
252
|
+
uptimeSec: elapsedSeconds(m[5]),
|
|
253
|
+
user: m[6],
|
|
254
|
+
name: m[7],
|
|
255
|
+
});
|
|
256
|
+
if (out.length >= limit) break;
|
|
257
|
+
}
|
|
258
|
+
return out;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Rows out of `Get-Process`.
|
|
263
|
+
*
|
|
264
|
+
* NOT `Win32_PerfFormattedData_PerfProc_Process`, which is what this used and
|
|
265
|
+
* which is not a class you may assume exists. It is published by perflib, and
|
|
266
|
+
* perflib is deregistered often enough to matter — a corporate image, a bad
|
|
267
|
+
* in-place upgrade, a half-run `lodctr`. On a machine reported from the field
|
|
268
|
+
* the class was simply absent (`Get-CimInstance: Invalid class`), and `typeperf`
|
|
269
|
+
* failed identically, which places the fault below WMI rather than in it. WMI
|
|
270
|
+
* was only mirroring what perflib had stopped publishing.
|
|
271
|
+
*
|
|
272
|
+
* `Get-Process` reads through NtQuerySystemInformation instead, so it depends on
|
|
273
|
+
* nothing that can be unregistered. The cost is that its `CPU` is total
|
|
274
|
+
* processor SECONDS since the process started, not a rate — so a percentage has
|
|
275
|
+
* to be derived from two readings, exactly the way the machine-wide figure is
|
|
276
|
+
* already derived from two tick samples. Reliability is worth one extra poll:
|
|
277
|
+
* an instant number from a class that may not exist is worth nothing at all.
|
|
278
|
+
*/
|
|
279
|
+
export function parseGetProcessJson(json, totalMem) {
|
|
280
|
+
let rows;
|
|
281
|
+
try { rows = typeof json === "string" ? JSON.parse(json) : json; }
|
|
282
|
+
catch { return []; }
|
|
283
|
+
if (!rows) return [];
|
|
284
|
+
if (!Array.isArray(rows)) rows = [rows];
|
|
285
|
+
return rows
|
|
286
|
+
.filter(r => r && r.ProcessName)
|
|
287
|
+
.map(r => ({
|
|
288
|
+
pid: Number(r.Id) || 0,
|
|
289
|
+
name: String(r.ProcessName),
|
|
290
|
+
// Null rather than 0 when the process denies the read: a system process
|
|
291
|
+
// we cannot query has an unknown CPU time, and calling that zero would
|
|
292
|
+
// rank it as idle.
|
|
293
|
+
cpuSec: typeof r.CPU === "number" ? r.CPU : null,
|
|
294
|
+
mem: totalMem > 0
|
|
295
|
+
? Math.round((Number(r.WorkingSetPrivate) || 0) / totalMem * 1000) / 10
|
|
296
|
+
: 0,
|
|
297
|
+
// The same three optional fields the POSIX branch attaches, so one client
|
|
298
|
+
// shape covers both. Absent rather than zero when the value did not come
|
|
299
|
+
// back: `StartTime` throws for a process this session may not open, and
|
|
300
|
+
// `Threads.Count` is undefined on a row that failed to project.
|
|
301
|
+
//
|
|
302
|
+
// `typeof` rather than `Number.isFinite(Number(…))` on the uptime, and the
|
|
303
|
+
// difference is the whole of that rule: `Number(null)` is 0, and 0 is
|
|
304
|
+
// finite, so the projection's own `else{$null}` — the branch it takes for
|
|
305
|
+
// every process whose StartTime this session may not read — arrived as an
|
|
306
|
+
// uptime of ZERO SECONDS. Every protected process on the machine was
|
|
307
|
+
// reported to the panel as having started this instant, which is the
|
|
308
|
+
// reading the comment above says must never be invented. A genuine zero
|
|
309
|
+
// is still kept: `[int]` of a sub-second TimeSpan is 0, and a process that
|
|
310
|
+
// started this instant is a real answer when it is the real answer. Same
|
|
311
|
+
// guard the CPU field two lines up already used, for the same reason.
|
|
312
|
+
...(Number.isFinite(Number(r.Threads)) && Number(r.Threads) > 0 ? { threads: Number(r.Threads) } : {}),
|
|
313
|
+
...(typeof r.StartedAt === "number" && Number.isFinite(r.StartedAt) ? { uptimeSec: r.StartedAt } : {}),
|
|
314
|
+
rssBytes: Number(r.WorkingSet) || 0,
|
|
315
|
+
}));
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Turn two `Get-Process` readings into a percentage per process.
|
|
320
|
+
*
|
|
321
|
+
* `prev` maps pid to the cpuSec of the previous reading. A pid absent from it —
|
|
322
|
+
* a process that started since — has no delta and reports null rather than a
|
|
323
|
+
* number invented from its whole lifetime, which would rank a freshly spawned
|
|
324
|
+
* compiler as though it had been burning a core since boot.
|
|
325
|
+
*
|
|
326
|
+
* Per core, NOT per machine, because that is what the column beside it means:
|
|
327
|
+
* `ps -o pcpu` is a percentage of one core on both Unixes and is reported
|
|
328
|
+
* unmodified, so a row reading 157 there is a process using one and a half
|
|
329
|
+
* cores. This used to divide by the core count and clamp to 100 on the reasoning
|
|
330
|
+
* that Unix reported 0-100 — it does not, and never did, so the normalisation
|
|
331
|
+
* corrected a scale that already matched and introduced the mismatch it was
|
|
332
|
+
* written to prevent: on a 12-core machine six busy cores read 600 on macOS and
|
|
333
|
+
* 50 on Windows (#493). One CPU-second burned per wall-second is 100 here, on
|
|
334
|
+
* every platform.
|
|
335
|
+
*
|
|
336
|
+
* Core count is deliberately not a parameter any more. The aggregate meter's
|
|
337
|
+
* 0-100 convention (see cpuPercent in system-metrics.mjs) is a different
|
|
338
|
+
* question with a different answer, and the only way this drifts back is if a core count is in reach.
|
|
339
|
+
*
|
|
340
|
+
* THE ROW IS CARRIED, NOT REBUILT, and that is the half this used to get wrong
|
|
341
|
+
* (#954). It listed the four fields it knew about — pid, cpu, mem, name — so
|
|
342
|
+
* `threads`, `uptimeSec` and `rssBytes` were silently dropped on their way
|
|
343
|
+
* through, and this is the only step the Windows reading takes: readProcessesNow
|
|
344
|
+
* returns straight out of the win32 branch, on the other side of the
|
|
345
|
+
* `attachDetail` call that puts those three back on POSIX. Every Windows row
|
|
346
|
+
* therefore reached the panel with three of its seven columns missing, always,
|
|
347
|
+
* and the memory header — which sorts on `rssBytes` and treats `undefined` as a
|
|
348
|
+
* missing reading — reordered nothing when clicked, because every row was
|
|
349
|
+
* missing it. Invisible to anyone developing on macOS or Linux.
|
|
350
|
+
*
|
|
351
|
+
* `cpuSec` is the one field deliberately removed: it is the raw counter this
|
|
352
|
+
* function exists to turn into a rate, the POSIX rows never carry one, and
|
|
353
|
+
* shipping it would put two spellings of the same quantity on the wire.
|
|
354
|
+
*/
|
|
355
|
+
export function cpuFromDeltas(rows, prev, elapsedMs, limit = Infinity) {
|
|
356
|
+
const secs = elapsedMs / 1000;
|
|
357
|
+
const out = rows.map(r => {
|
|
358
|
+
const before = prev instanceof Map ? prev.get(r.pid) : undefined;
|
|
359
|
+
let cpu = null;
|
|
360
|
+
if (r.cpuSec != null && before != null && secs > 0) {
|
|
361
|
+
const d = r.cpuSec - before;
|
|
362
|
+
// A counter that went backwards means the pid was reused by a different
|
|
363
|
+
// process; report nothing rather than a negative or a wild number.
|
|
364
|
+
if (d >= 0) cpu = Math.round((d / secs) * 1000) / 10;
|
|
365
|
+
}
|
|
366
|
+
const { cpuSec: _raw, ...rest } = r;
|
|
367
|
+
return { ...rest, cpu };
|
|
368
|
+
});
|
|
369
|
+
// Until the second reading lands there is no CPU to sort on, so the list is
|
|
370
|
+
// ordered by memory — which is a real answer to "what is this machine doing",
|
|
371
|
+
// not a placeholder.
|
|
372
|
+
const haveCpu = out.some(r => r.cpu != null);
|
|
373
|
+
out.sort(haveCpu
|
|
374
|
+
? (a, b) => (b.cpu ?? -1) - (a.cpu ?? -1)
|
|
375
|
+
: (a, b) => b.mem - a.mem);
|
|
376
|
+
return out.slice(0, limit);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/** Previous Windows reading, so the next one can be a rate. Cleared with the
|
|
380
|
+
* rest of the sampler state — see resetProcessList. */
|
|
381
|
+
let prevProcCpu = null;
|
|
382
|
+
let prevProcAt = 0;
|
|
383
|
+
|
|
384
|
+
/** The run producing the next list, and the last one that finished. */
|
|
385
|
+
let procInFlight = null;
|
|
386
|
+
let procLast = null; // { at, read: { procs, total }, detail }
|
|
387
|
+
let procInFlightDetail = false;
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* How old a finished reading may be and still answer a caller.
|
|
391
|
+
*
|
|
392
|
+
* Well under ProcessListModal's PROC_POLL_MS of 4000, so the dialog this exists
|
|
393
|
+
* for never once gets a cached list; long enough that a second tab, a second
|
|
394
|
+
* browser, or anything else arriving between two of those polls is handed the
|
|
395
|
+
* list the first tab is already looking at rather than starting its own child.
|
|
396
|
+
*/
|
|
397
|
+
const PROC_MIN_GAP_MS = 1_500;
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* The process list, on demand only — never on the ambient timer, and never more
|
|
401
|
+
* than one child at a time.
|
|
402
|
+
*
|
|
403
|
+
* #544: /api/system/processes is a GET with no cache, no dedupe and no
|
|
404
|
+
* throttle, so the number of `powershell.exe Get-Process` children — about six
|
|
405
|
+
* seconds each — was whatever the caller asked for. That is the cheap half of
|
|
406
|
+
* the problem. The expensive half is that concurrent readers also overwrote
|
|
407
|
+
* each other's baseline: cpuFromDeltas needs the PREVIOUS reading's cpuSec per
|
|
408
|
+
* pid, prevProcCpu/prevProcAt are one shared pair, and two readers each stored
|
|
409
|
+
* theirs over the other's, so the CPU column came back computed against a
|
|
410
|
+
* baseline that belonged to somebody else's reading. That is a wrong number on
|
|
411
|
+
* screen, not merely wasted work, and it needed no attacker at all: one
|
|
412
|
+
* Get-Process takes longer than the panel's four-second poll, so a single tab
|
|
413
|
+
* on Windows already overlapped itself.
|
|
414
|
+
*
|
|
415
|
+
* One in-flight run fixes both, because one reader means one baseline. Callers
|
|
416
|
+
* that arrive while a run is going share its promise; callers that arrive just
|
|
417
|
+
* after one finished are served that reading.
|
|
418
|
+
*/
|
|
419
|
+
export async function readProcesses(platform = process.platform, detail = false) {
|
|
420
|
+
const now = Date.now();
|
|
421
|
+
// A cached reading serves a caller that wants LESS than it holds, never one
|
|
422
|
+
// that wants more: the panel is happy with a detailed reading, and the modal
|
|
423
|
+
// opening onto a plain one would show four empty columns for up to a poll.
|
|
424
|
+
if (procLast && now - procLast.at < PROC_MIN_GAP_MS && (procLast.detail || !detail)) return procLast.read;
|
|
425
|
+
if (procInFlight && (procInFlightDetail || !detail)) return procInFlight;
|
|
426
|
+
// Only a real reading is remembered. Every failure inside readProcessesNow —
|
|
427
|
+
// a spawn that never started, a non-zero exit, the timeout — resolves to an
|
|
428
|
+
// empty array, and no machine has nothing running on it, so an empty list is
|
|
429
|
+
// a failure by construction. Serving one for the next 1.5s would turn a
|
|
430
|
+
// single hiccup into a blank panel that outlives it; the next caller retries
|
|
431
|
+
// instead. The in-flight share still applies, so a burst arriving during a
|
|
432
|
+
// failing read is one failing child, not a burst of them.
|
|
433
|
+
procInFlightDetail = detail;
|
|
434
|
+
procInFlight = readProcessesNow(platform, detail)
|
|
435
|
+
.then(read => {
|
|
436
|
+
if (read.procs.length) procLast = { at: Date.now(), read, detail };
|
|
437
|
+
return read;
|
|
438
|
+
})
|
|
439
|
+
.finally(() => { procInFlight = null; procInFlightDetail = false; });
|
|
440
|
+
return procInFlight;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
/**
|
|
444
|
+
* The one-liner the Windows process list is read with.
|
|
445
|
+
*
|
|
446
|
+
* Threads and StartTime ride along on the call that was already being made —
|
|
447
|
+
* `Get-Process` has both, so the two columns cost nothing here. `WorkingSet64`
|
|
448
|
+
* joins `PrivateMemorySize64` rather than replacing it: the percentage has
|
|
449
|
+
* always been computed from private bytes and moving it would change a number
|
|
450
|
+
* that is on screen today, while the new column wants the resident figure Task
|
|
451
|
+
* Manager itself shows.
|
|
452
|
+
*
|
|
453
|
+
* NOT `-IncludeUserName`: it requires an elevated session, and no part of this
|
|
454
|
+
* deck may ask for one. The user column is simply absent on Windows, which is
|
|
455
|
+
* the same rule the thermal section keeps.
|
|
456
|
+
*
|
|
457
|
+
* A named export for the reason WIN_THERMAL_PS is one, and #954 is why it
|
|
458
|
+
* became one: three of the seven columns this projection is built to fill were
|
|
459
|
+
* being dropped after it, and every assertion in the suite was made against a
|
|
460
|
+
* payload somebody typed. A constant can be handed to the real powershell.exe
|
|
461
|
+
* on the Windows leg of the matrix, which is the only place the claim "Windows
|
|
462
|
+
* really does answer with these seven keys" can be measured rather than
|
|
463
|
+
* reasoned about.
|
|
464
|
+
*/
|
|
465
|
+
export const WIN_PROCESS_PS =
|
|
466
|
+
"Get-Process | Select-Object Id,ProcessName,CPU,@{n='Threads';e={$_.Threads.Count}},@{n='StartedAt';e={if($_.StartTime){[int]((Get-Date)-$_.StartTime).TotalSeconds}else{$null}}},@{n='WorkingSet';e={$_.WorkingSet64}},@{n='WorkingSetPrivate';e={$_.PrivateMemorySize64}} | ConvertTo-Json -Compress";
|
|
467
|
+
|
|
468
|
+
async function readProcessesNow(platform, detail = false) {
|
|
469
|
+
if (platform === "win32") {
|
|
470
|
+
const out = await run("powershell.exe", [
|
|
471
|
+
"-NoProfile", "-NonInteractive", "-Command", WIN_PROCESS_PS,
|
|
472
|
+
], 6_000);
|
|
473
|
+
// The same shape the POSIX branch below returns, and not a bare array: the
|
|
474
|
+
// caller reads `read.procs.length` to decide whether this was a real
|
|
475
|
+
// reading, so an array meant a TypeError rather than an empty list. Every
|
|
476
|
+
// Windows failure — a non-zero exit, a spawn that never started, the 6s
|
|
477
|
+
// deadline this module's own comment says the runtime sits right on —
|
|
478
|
+
// therefore answered `/api/system/processes` with a 500 and a logged stack,
|
|
479
|
+
// every four seconds for as long as the machine panel was open.
|
|
480
|
+
if (!out) return { procs: [], total: 0 };
|
|
481
|
+
const rows = parseGetProcessJson(out.trim(), os.totalmem());
|
|
482
|
+
const now = Date.now();
|
|
483
|
+
const ranked = cpuFromDeltas(rows, prevProcCpu, now - prevProcAt);
|
|
484
|
+
prevProcCpu = new Map(rows.filter(r => r.cpuSec != null).map(r => [r.pid, r.cpuSec]));
|
|
485
|
+
prevProcAt = now;
|
|
486
|
+
return { procs: pickCandidates(ranked), total: ranked.length };
|
|
487
|
+
}
|
|
488
|
+
const out = await run("ps", psArgs(platform), 4_000);
|
|
489
|
+
if (!out) return { procs: [], total: 0 };
|
|
490
|
+
const all = parsePsProcesses(out);
|
|
491
|
+
const procs = pickCandidates(all);
|
|
492
|
+
if (detail) await attachDetail(procs, platform);
|
|
493
|
+
return { procs, total: all.length };
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
/**
|
|
497
|
+
* Fill in threads and the command tail, for the candidates and no further.
|
|
498
|
+
*
|
|
499
|
+
* AFTER pickCandidates, deliberately: this is a per-pid listing, and asking it
|
|
500
|
+
* about every process on the machine would be several hundred pids to answer a
|
|
501
|
+
* question about forty. The candidates are what any surface can draw, so they
|
|
502
|
+
* are what gets the second child.
|
|
503
|
+
*
|
|
504
|
+
* A failure here is not a failure of the reading. `ps -M` can lose a race with
|
|
505
|
+
* a process that exited between the two calls, a hardened environment can
|
|
506
|
+
* refuse it, and neither is a reason to blank a list whose CPU and memory
|
|
507
|
+
* columns are already correct. The fields are simply absent, and the columns
|
|
508
|
+
* that read them print a dash — the same rule the rest of this module keeps:
|
|
509
|
+
* an unknown is never rendered as a zero.
|
|
510
|
+
*/
|
|
511
|
+
async function attachDetail(procs, platform) {
|
|
512
|
+
if (!procs.length) return;
|
|
513
|
+
const pids = procs.map(p => p.pid).filter(n => Number.isInteger(n) && n > 0);
|
|
514
|
+
if (!pids.length) return;
|
|
515
|
+
let out;
|
|
516
|
+
try { out = await run("ps", psDetailArgs(pids, platform), 4_000); }
|
|
517
|
+
catch { return; }
|
|
518
|
+
if (!out) return;
|
|
519
|
+
const detail = platform === "linux" ? parsePsThreadsProcps(out) : parsePsThreadsBsd(out);
|
|
520
|
+
const home = os.homedir?.() ?? "";
|
|
521
|
+
for (const p of procs) {
|
|
522
|
+
const d = detail.get(p.pid);
|
|
523
|
+
if (!d) continue;
|
|
524
|
+
// Zero threads is not a reading. Every process has at least one, so a zero
|
|
525
|
+
// here means the listing named the pid and gave no thread lines for it —
|
|
526
|
+
// a race with an exit — and a `0` in that column would be a claim.
|
|
527
|
+
if (d.threads > 0) p.threads = d.threads;
|
|
528
|
+
// REDACTED BEFORE IT LEAVES THIS MODULE, not on the way to the screen.
|
|
529
|
+
// /api/system/processes is served to anything that can reach the loopback
|
|
530
|
+
// port, and a token that only the client hides is a token that shipped.
|
|
531
|
+
const cmd = redactCommand(commandTail(d.cmd, p.name), home);
|
|
532
|
+
if (cmd) p.cmd = cmd;
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
/**
|
|
537
|
+
* Forget the Windows baseline and the last list, for stopSystemMetrics.
|
|
538
|
+
*
|
|
539
|
+
* The last list goes with the baseline it was computed against. A sampler that
|
|
540
|
+
* stopped and started again must not answer the first caller with a reading
|
|
541
|
+
* from before the stop.
|
|
542
|
+
*/
|
|
543
|
+
export function resetProcessList() {
|
|
544
|
+
prevProcCpu = null;
|
|
545
|
+
prevProcAt = 0;
|
|
546
|
+
procLast = null;
|
|
547
|
+
}
|