ompchamber 1.2.0 → 2.0.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 +131 -147
- package/dist/client/index.html +4 -2
- package/dist/client/static/css/index.5f630421b7.css +2 -0
- package/dist/client/static/js/77.0d81c74cf0.js +139 -0
- package/dist/client/static/js/async/1k.973e8e21fc.js +2 -0
- package/dist/client/static/js/async/{3y.3186ecdd63.js → 3y.3530b824ad.js} +6 -6
- package/dist/client/static/js/async/{q.b453193538.js → 4m.928a1dde85.js} +10 -10
- package/dist/client/static/js/async/66.5dfa29f957.js +93 -0
- package/dist/client/static/js/async/7g.a7814350c3.js +1 -0
- package/dist/client/static/js/async/ar.9303e5f2e8.js +2 -0
- package/dist/client/static/js/async/{cx.9c8268d542.js → cx.3cb0242a3a.js} +1 -1
- package/dist/client/static/js/async/e.93214d622b.js +79 -0
- package/dist/client/static/js/async/g.74ff36419f.js +2 -0
- package/dist/client/static/js/async/{m.2e1ea3db9e.js.LICENSE.txt → g.74ff36419f.js.LICENSE.txt} +5 -0
- package/dist/client/static/js/async/h1.393f066d09.js +1 -0
- package/dist/client/static/js/async/jl.874d352278.js +1 -0
- package/dist/client/static/js/async/{k.d16cd09ae7.js → k.3210286401.js} +1 -1
- package/dist/client/static/js/async/{m.2e1ea3db9e.js → m.eb2e6752e2.js} +3 -3
- package/dist/client/static/js/async/m.eb2e6752e2.js.LICENSE.txt +11 -0
- package/dist/client/static/js/async/mt.0352e976d8.js +1 -0
- package/dist/client/static/js/async/{oc.eb22093f05.js → oc.b73042e8bd.js} +1 -1
- package/dist/client/static/js/async/{og.53f068804c.js → og.8eb577893a.js} +1 -1
- package/dist/client/static/js/async/px.fc6877924b.js +1 -0
- package/dist/client/static/js/async/q.33d6779f98.js +21 -0
- package/dist/client/static/js/async/{q5.88b50ffa95.js → q5.97dc1481b6.js} +1 -1
- package/dist/client/static/js/async/s.0f395cdda5.js +94 -0
- package/dist/client/static/js/async/{tr.2a0d2b7600.js → tr.e878658140.js} +1 -1
- package/dist/client/static/js/async/vt.42416b707d.js +1 -0
- package/dist/client/static/js/async/x1.da8687159b.js +90 -0
- package/dist/client/static/js/async/xe.da8dcc2015.js +2 -0
- package/dist/client/static/js/async/xe.da8dcc2015.js.LICENSE.txt +11 -0
- package/dist/client/static/js/async/{xn.6360379f71.js → xn.6510c0abe2.js} +2 -2
- package/dist/client/static/js/async/yg.35bebd6d0c.js +149 -0
- package/dist/client/static/js/async/yv.d99d829136.js +1 -0
- package/dist/client/static/js/index.c05535dbde.js +38 -0
- package/package.json +3 -3
- package/src/client/components/common/ComposerTextarea.tsx +9 -1
- package/src/client/components/common/MarkdownRenderer.tsx +50 -12
- package/src/client/components/mobile/mobile-right-sidebar/FullEditor.tsx +9 -3
- package/src/client/components/settings/Modal.tsx +11 -2
- package/src/client/components/settings/categories/AppearanceSettings.tsx +7 -30
- package/src/client/components/settings/categories/ProviderSettings.tsx +2 -0
- package/src/client/components/settings/categories/appearance-settings/ThemeCard.tsx +61 -0
- package/src/client/components/settings/categories/appearance-settings/ThemeGrid.tsx +80 -0
- package/src/client/components/settings/categories/chat-settings/TitleSection.tsx +1 -1
- package/src/client/components/settings/categories/provider-settings/AddProviderModal.tsx +43 -6
- package/src/client/components/settings/categories/provider-settings/ModelConfigModal.tsx +54 -54
- package/src/client/components/workspace/SearchPanel.tsx +16 -3
- package/src/client/components/workspace/btw-panel/AskCard.tsx +11 -6
- package/src/client/components/workspace/btw-panel/Messages.tsx +105 -51
- package/src/client/components/workspace/btw-panel/index.tsx +129 -45
- package/src/client/components/workspace/chat-timeline/AiMessageFooter.tsx +30 -14
- package/src/client/components/workspace/chat-timeline/ComposerDock.tsx +10 -1
- package/src/client/components/workspace/chat-timeline/GeneratingIndicator.tsx +8 -2
- package/src/client/components/workspace/chat-timeline/MessageItem.tsx +27 -16
- package/src/client/components/workspace/chat-timeline/MessageList.tsx +11 -0
- package/src/client/components/workspace/chat-timeline/RunFooter.tsx +8 -0
- package/src/client/components/workspace/chat-timeline/chat-input/index.tsx +12 -1
- package/src/client/components/workspace/chat-timeline/index.tsx +7 -2
- package/src/client/components/workspace/chat-timeline/tool-renderers/ask-panel/QuestionBlock.tsx +42 -15
- package/src/client/components/workspace/chat-timeline/tool-renderers/ask-panel/index.tsx +13 -1
- package/src/client/components/workspace/chat-timeline/tool-renderers/extension-dialog/index.tsx +13 -4
- package/src/client/components/workspace/editor/index.tsx +11 -2
- package/src/client/components/workspace/file-explorer/GitRepoDropdown.tsx +15 -41
- package/src/client/components/workspace/file-explorer/index.tsx +86 -16
- package/src/client/components/workspace/git-panel/index.tsx +60 -78
- package/src/client/components/workspace/terminal-panel/Header.tsx +16 -1
- package/src/client/components/workspace/terminal-panel/RealtimeXtermView.tsx +5 -5
- package/src/client/components/workspace/terminal-panel/index.tsx +6 -2
- package/src/client/components/workspace/terminal-panel/mount.ts +2 -2
- package/src/client/data/theme/terminal.ts +50 -52
- package/src/client/hooks/chat/btw/index.ts +139 -18
- package/src/client/hooks/chat/btw/intercept.ts +8 -8
- package/src/client/hooks/chat/btw/mode.ts +111 -16
- package/src/client/hooks/chat/composer/index.ts +10 -4
- package/src/client/hooks/chat/timeline/access-mode.ts +41 -0
- package/src/client/hooks/chat/timeline/ask-frames.test.ts +87 -0
- package/src/client/hooks/chat/timeline/ask-frames.ts +20 -1
- package/src/client/hooks/chat/timeline/auto-scroll.ts +13 -4
- package/src/client/hooks/chat/timeline/index.ts +8 -20
- package/src/client/hooks/chat/timeline/queue.ts +66 -9
- package/src/client/hooks/chat/timeline/scroll.test.ts +260 -0
- package/src/client/hooks/chat/timeline/scroll.ts +93 -20
- package/src/client/hooks/chat/timeline/session-load.ts +7 -1
- package/src/client/hooks/chat/timeline/session-pagination.test.ts +167 -0
- package/src/client/hooks/chat/timeline/session-pagination.ts +37 -11
- package/src/client/hooks/settings/providers.ts +34 -3
- package/src/client/hooks/ui/open-file.ts +7 -1
- package/src/client/hooks/ui/theme.ts +82 -57
- package/src/client/hooks/workspace/file-tabs.ts +5 -1
- package/src/client/hooks/workspace/repo-scope/index.ts +119 -0
- package/src/client/hooks/workspace/repo-scope/store.test.ts +161 -0
- package/src/client/hooks/workspace/repo-scope/store.ts +201 -0
- package/src/client/hooks/workspace/search-stream.ts +24 -5
- package/src/client/main.tsx +6 -0
- package/src/client/tailwind.css +19 -56
- package/src/server/lib/btw/child.server.ts +41 -0
- package/src/server/lib/btw/lifecycle.server.ts +254 -0
- package/src/server/lib/btw/prompt.ts +13 -5
- package/src/server/lib/btw/registry.server.ts +13 -4
- package/src/server/lib/btw/runtime.server.ts +149 -156
- package/src/server/lib/btw/schema.server.ts +27 -0
- package/src/server/lib/btw/service.server.ts +106 -5
- package/src/server/lib/btw/store.server.ts +57 -6
- package/src/server/lib/db/schema.ts +4 -0
- package/src/server/lib/db/settings-store.test.ts +65 -0
- package/src/server/lib/db/settings-store.ts +25 -0
- package/src/server/lib/fs/atomic-write.ts +42 -4
- package/src/server/lib/fs/config-lock.ts +74 -0
- package/src/server/lib/fs/git-ignore.ts +55 -5
- package/src/server/lib/fs/ripgrep.ts +119 -0
- package/src/server/lib/lifecycle/identity.ts +10 -64
- package/src/server/lib/lifecycle/proc/darwin.ts +238 -0
- package/src/server/lib/lifecycle/proc/index.ts +38 -0
- package/src/server/lib/lifecycle/proc/linux.ts +80 -0
- package/src/server/lib/lifecycle/proc/ps.ts +86 -0
- package/src/server/lib/lifecycle/proc/types.ts +33 -0
- package/src/server/lib/lifecycle/proc/win32.ts +53 -0
- package/src/server/lib/models/provider-registry.server.ts +2 -2
- package/src/server/lib/models/registry.server.ts +1 -1
- package/src/server/lib/omp/config/behavior.ts +17 -17
- package/src/server/lib/omp/config/disabled-providers.ts +84 -35
- package/src/server/lib/omp/config/document.ts +128 -0
- package/src/server/lib/omp/config/extensions.ts +11 -10
- package/src/server/lib/omp/config/mcp.ts +4 -57
- package/src/server/lib/omp/config/model-overrides.ts +123 -0
- package/src/server/lib/omp/config/models-config.ts +92 -0
- package/src/server/lib/omp/config/models-validation.ts +159 -0
- package/src/server/lib/omp/config/path-scoped.ts +74 -0
- package/src/server/lib/omp/config/providers.ts +241 -211
- package/src/server/lib/omp/config/roles.ts +6 -14
- package/src/server/lib/omp/core/auth-credentials.ts +1 -1
- package/src/server/lib/omp/rpc/frame-fold.test.ts +124 -0
- package/src/server/lib/omp/rpc/frame-fold.ts +43 -24
- package/src/server/lib/omp/rpc/lifecycle.test.ts +41 -0
- package/src/server/lib/omp/rpc/manager.ts +21 -21
- package/src/server/lib/omp/rpc/pending-ui-dialogs.ts +11 -7
- package/src/server/lib/omp/rpc/session-commands.ts +17 -29
- package/src/server/lib/omp/rpc/session-registry.ts +5 -66
- package/src/server/lib/omp/rpc/web-state.ts +0 -6
- package/src/server/lib/omp/session/auto-title.server.test.ts +109 -11
- package/src/server/lib/omp/session/auto-title.server.ts +95 -20
- package/src/server/lib/omp/session/sidebar-data.server.ts +11 -6
- package/src/server/lib/queue/delivery-dispatch.server.test.ts +162 -0
- package/src/server/lib/queue/delivery-harness.ts +88 -0
- package/src/server/lib/queue/delivery.server.test.ts +78 -149
- package/src/server/lib/queue/delivery.server.ts +105 -13
- package/src/server/lib/queue/store.server.ts +30 -5
- package/src/server/lib/terminal/foreground.test.ts +164 -0
- package/src/server/lib/terminal/foreground.ts +34 -6
- package/src/server/lib/terminal/history.ts +8 -7
- package/src/server/plugins/compress.ts +33 -5
- package/src/server/plugins/ssr.ts +14 -7
- package/src/server/routes/btw/topics.ts +89 -2
- package/src/server/routes/fs/git.ts +5 -6
- package/src/server/routes/fs/search.ts +17 -26
- package/src/server/routes/sessions/queue.ts +17 -4
- package/src/server/routes/settings/index.ts +2 -0
- package/src/server/routes/settings/model-override.ts +69 -0
- package/src/server/routes/settings/provider-models.ts +35 -12
- package/src/server/routes/settings/providers.ts +31 -1
- package/src/server/routes/settings/root.ts +8 -3
- package/src/shared/lib/chat/btw/client.ts +34 -0
- package/src/shared/lib/chat/timeline/tool-phrases.ts +7 -0
- package/src/shared/lib/code/shiki-themes.ts +9 -5
- package/src/shared/lib/fs/file-tree.ts +17 -12
- package/src/shared/lib/markdown/autolink.test.ts +32 -0
- package/src/shared/lib/markdown/autolink.ts +50 -0
- package/src/shared/lib/markdown/document-urls.test.ts +68 -0
- package/src/shared/lib/markdown/document-urls.ts +118 -0
- package/src/shared/lib/markdown/marked.ts +27 -15
- package/src/shared/lib/markdown/mermaid.ts +2 -2
- package/src/shared/lib/models/catalog.ts +1 -1
- package/src/shared/lib/omp/session/schema-migrations.ts +30 -0
- package/src/shared/lib/omp/session/stream-state.server.test.ts +54 -0
- package/src/shared/lib/omp/session/stream-state.server.ts +70 -21
- package/src/shared/lib/settings/client.ts +10 -4
- package/src/shared/lib/settings/diff.test.ts +31 -0
- package/src/shared/lib/settings/diff.ts +37 -0
- package/src/shared/lib/theme/catalog.test.ts +168 -0
- package/src/shared/lib/theme/catalog.ts +98 -0
- package/src/shared/lib/theme/color.ts +88 -0
- package/src/shared/lib/theme/css.ts +70 -0
- package/src/shared/lib/theme/palettes/chamber.ts +61 -0
- package/src/shared/lib/theme/palettes/dark.ts +313 -0
- package/src/shared/lib/theme/palettes/light.ts +313 -0
- package/src/shared/types/btw.ts +83 -4
- package/src/shared/types/index.ts +1 -0
- package/src/shared/types/settings/provider.ts +8 -1
- package/src/shared/types/settings/state.ts +10 -4
- package/src/shared/types/theme.ts +48 -0
- package/dist/client/static/css/index.b938f4a693.css +0 -2
- package/dist/client/static/js/77.8022a800b5.js +0 -139
- package/dist/client/static/js/async/1k.a411fe5b43.js +0 -2
- package/dist/client/static/js/async/66.86544ad741.js +0 -93
- package/dist/client/static/js/async/7g.db82aca7ba.js +0 -1
- package/dist/client/static/js/async/ar.ad5c8822a8.js +0 -2
- package/dist/client/static/js/async/e.6443e739a2.js +0 -79
- package/dist/client/static/js/async/g.ee41bfed7f.js +0 -2
- package/dist/client/static/js/async/h1.d7091cef78.js +0 -1
- package/dist/client/static/js/async/jl.4911fa3383.js +0 -1
- package/dist/client/static/js/async/mt.853be2b5aa.js +0 -1
- package/dist/client/static/js/async/ot.84c71b308f.js +0 -2
- package/dist/client/static/js/async/px.783b6f91f2.js +0 -1
- package/dist/client/static/js/async/q8.7f1b48e9cd.js +0 -21
- package/dist/client/static/js/async/q8.7f1b48e9cd.js.LICENSE.txt +0 -6
- package/dist/client/static/js/async/s.aad2ecebc6.js +0 -94
- package/dist/client/static/js/async/vt.20406a5604.js +0 -1
- package/dist/client/static/js/async/x1.fe8af17016.js +0 -90
- package/dist/client/static/js/async/xn.6360379f71.js.LICENSE.txt +0 -6
- package/dist/client/static/js/async/yg.5a94e1fc8a.js +0 -149
- package/dist/client/static/js/async/yv.2fa22f009a.js +0 -1
- package/dist/client/static/js/index.57b39ffa7f.js +0 -34
- package/src/client/components/workspace/btw-panel/Composer.tsx +0 -181
- package/src/client/components/workspace/btw-panel/attachments.ts +0 -80
- /package/dist/client/static/js/{77.8022a800b5.js.LICENSE.txt → 77.0d81c74cf0.js.LICENSE.txt} +0 -0
- /package/dist/client/static/js/async/{g.ee41bfed7f.js.LICENSE.txt → 1k.973e8e21fc.js.LICENSE.txt} +0 -0
- /package/dist/client/static/js/async/{3y.3186ecdd63.js.LICENSE.txt → 3y.3530b824ad.js.LICENSE.txt} +0 -0
- /package/dist/client/static/js/async/{ot.84c71b308f.js.LICENSE.txt → 4m.928a1dde85.js.LICENSE.txt} +0 -0
- /package/dist/client/static/js/async/{q.b453193538.js.LICENSE.txt → ar.9303e5f2e8.js.LICENSE.txt} +0 -0
- /package/dist/client/static/js/async/{1k.a411fe5b43.js.LICENSE.txt → q.33d6779f98.js.LICENSE.txt} +0 -0
- /package/dist/client/static/js/async/{ar.ad5c8822a8.js.LICENSE.txt → xn.6510c0abe2.js.LICENSE.txt} +0 -0
- /package/dist/client/static/js/{index.57b39ffa7f.js.LICENSE.txt → index.c05535dbde.js.LICENSE.txt} +0 -0
|
@@ -13,53 +13,19 @@
|
|
|
13
13
|
* stranger. Identity therefore comes from the OS command line, so a recycled
|
|
14
14
|
* PID reads back as `mismatched`.
|
|
15
15
|
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
16
|
+
* How the OS is asked is per platform and lives in `./probe` — macOS through
|
|
17
|
+
* `libproc`, Linux through `/proc`, Windows through signal 0 alone, the BSDs
|
|
18
|
+
* through `ps`. This module only turns those answers into the states callers
|
|
19
|
+
* reason about.
|
|
20
|
+
*
|
|
21
|
+
* Where identity cannot be read, it is reported as `unknown` and callers fall
|
|
22
|
+
* back to liveness — never treated as confirmation that the PID is ours.
|
|
19
23
|
*/
|
|
20
24
|
|
|
21
|
-
import
|
|
25
|
+
import { processProbe } from '@/server/lib/lifecycle/proc';
|
|
22
26
|
|
|
23
27
|
export type ProcessState = 'dead' | 'matched' | 'mismatched' | 'unknown';
|
|
24
28
|
|
|
25
|
-
interface ProcessRow {
|
|
26
|
-
pid: number;
|
|
27
|
-
/** `ps` state column — `Z…` means defunct (exited, awaiting reaping). */
|
|
28
|
-
state: string;
|
|
29
|
-
command: string;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
let cachedTable: { at: number; rows: ProcessRow[] } | null = null;
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* One snapshot of the process table, memoized for a startup decision.
|
|
36
|
-
*
|
|
37
|
-
* Every question in a port check happens within milliseconds, so a briefly
|
|
38
|
-
* cached view is strictly better than spawning `ps` per question.
|
|
39
|
-
*/
|
|
40
|
-
function readProcessTable(maxAgeMs = 250): ProcessRow[] {
|
|
41
|
-
if (cachedTable && Date.now() - cachedTable.at < maxAgeMs) return cachedTable.rows;
|
|
42
|
-
const rows: ProcessRow[] = [];
|
|
43
|
-
try {
|
|
44
|
-
const snapshot = Bun.spawnSync({
|
|
45
|
-
cmd: ['ps', '-ax', '-o', 'pid=,stat=,command='],
|
|
46
|
-
stdout: 'pipe',
|
|
47
|
-
stderr: 'ignore',
|
|
48
|
-
});
|
|
49
|
-
if (snapshot.success) {
|
|
50
|
-
for (const line of snapshot.stdout.toString().split('\n')) {
|
|
51
|
-
const match = /^\s*(\d+)\s+(\S+)\s+(.*)$/.exec(line);
|
|
52
|
-
if (!match) continue;
|
|
53
|
-
rows.push({ pid: Number(match[1]), state: match[2], command: match[3].trim() });
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
} catch {
|
|
57
|
-
// No `ps` on PATH: identity falls back to /proc, liveness to signal 0.
|
|
58
|
-
}
|
|
59
|
-
cachedTable = { at: Date.now(), rows };
|
|
60
|
-
return rows;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
29
|
/**
|
|
64
30
|
* Liveness check. Never throws.
|
|
65
31
|
*
|
|
@@ -71,27 +37,7 @@ function readProcessTable(maxAgeMs = 250): ProcessRow[] {
|
|
|
71
37
|
*/
|
|
72
38
|
export function isProcessAlive(pid: number): boolean {
|
|
73
39
|
if (!Number.isFinite(pid) || pid <= 0) return false;
|
|
74
|
-
|
|
75
|
-
process.kill(pid, 0);
|
|
76
|
-
} catch {
|
|
77
|
-
return false;
|
|
78
|
-
}
|
|
79
|
-
const row = readProcessTable().find((entry) => entry.pid === pid);
|
|
80
|
-
return row ? !row.state.startsWith('Z') : true;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
/** Command line for a live PID, or null when this platform cannot report one. */
|
|
84
|
-
function readProcessCommandLine(pid: number): string | null {
|
|
85
|
-
const row = readProcessTable().find((entry) => entry.pid === pid);
|
|
86
|
-
if (row) return row.command.length > 0 ? row.command : null;
|
|
87
|
-
if (process.platform === 'linux') {
|
|
88
|
-
try {
|
|
89
|
-
return fs.readFileSync(`/proc/${pid}/cmdline`, 'utf8').replace(/\0/g, ' ').trim() || null;
|
|
90
|
-
} catch {
|
|
91
|
-
return null;
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
return null;
|
|
40
|
+
return processProbe.isAlive(pid) && !processProbe.isZombie(pid);
|
|
95
41
|
}
|
|
96
42
|
|
|
97
43
|
/**
|
|
@@ -102,7 +48,7 @@ function readProcessCommandLine(pid: number): string | null {
|
|
|
102
48
|
*/
|
|
103
49
|
export function getProcessState(pid: number): ProcessState {
|
|
104
50
|
if (!isProcessAlive(pid)) return 'dead';
|
|
105
|
-
const command =
|
|
51
|
+
const command = processProbe.commandLine(pid);
|
|
106
52
|
if (command === null) return 'unknown';
|
|
107
53
|
return command.toLowerCase().includes('ompchamber') ? 'matched' : 'mismatched';
|
|
108
54
|
}
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* macOS process probe: `libproc` + `sysctl`, no subprocess.
|
|
8
|
+
*
|
|
9
|
+
* `ps -ax` answers the same questions, but it costs a process spawn and a full
|
|
10
|
+
* process-table scan per snapshot — measured at ~32 ms here, against ~0.001 ms
|
|
11
|
+
* for the direct calls — and the CLI's `waitForProcessExit` polls liveness every
|
|
12
|
+
* 100 ms. These are the same APIs `ps` itself uses.
|
|
13
|
+
*
|
|
14
|
+
* `sysctl(KERN_PROC_PID)` is the liveness source rather than
|
|
15
|
+
* `proc_pidinfo(PROC_PIDTBSDINFO)`: it answers for a ZOMBIE (which `proc_pidinfo`
|
|
16
|
+
* refuses, returning 0 — verified) and for root-owned PIDs, so one call covers
|
|
17
|
+
* liveness and the zombie distinction for every PID.
|
|
18
|
+
*
|
|
19
|
+
* Every entry point tolerates failure: a PID that vanished between two calls
|
|
20
|
+
* makes the syscall report zero bytes, which is "not alive", and a sandbox that
|
|
21
|
+
* denies the call degrades to signal 0 rather than throwing.
|
|
22
|
+
*
|
|
23
|
+
* The libraries are opened on the FIRST CALL rather than at module load, so
|
|
24
|
+
* merely importing this module — which happens on every platform, whatever
|
|
25
|
+
* `index.ts` dispatches to — cannot fail. See `bindLibs`.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
import { dlopen, FFIType, ptr } from 'bun:ffi';
|
|
29
|
+
import type { FFIFunction, Library } from 'bun:ffi';
|
|
30
|
+
|
|
31
|
+
import type { ProcessProbe } from '@/server/lib/lifecycle/proc/types';
|
|
32
|
+
|
|
33
|
+
/** `sysctl` MIB pieces. */
|
|
34
|
+
const CTL_KERN = 1;
|
|
35
|
+
const KERN_PROC = 14;
|
|
36
|
+
const KERN_PROC_PID = 1;
|
|
37
|
+
const KERN_PROCARGS2 = 49;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* `struct kinfo_proc` up to its `p_stat` byte: `extern_proc`'s union (16),
|
|
41
|
+
* `p_vmspace` (8), `p_sigacts` (8), `p_flag` (4). Verified against this
|
|
42
|
+
* platform: a running process reads 2, a zombie reads 5, `launchd` reads 2.
|
|
43
|
+
*/
|
|
44
|
+
const KINFO_PROC_BYTES = 648;
|
|
45
|
+
const P_STAT_OFFSET = 36;
|
|
46
|
+
/** Darwin `p_stat` values. */
|
|
47
|
+
const SZOMB = 5;
|
|
48
|
+
const SZOMB_LIKE = new Set([SZOMB, 6 /* SDEAD */]);
|
|
49
|
+
|
|
50
|
+
/** Upper bound on a command line block; the kernel truncates instead. */
|
|
51
|
+
const ARGV_BUFFER_BYTES = 262_144;
|
|
52
|
+
const PATH_BUFFER_BYTES = 4096;
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* The two Darwin libraries and their symbols, opened on the first probe call.
|
|
56
|
+
*
|
|
57
|
+
* `dlopen` at module scope runs on EVERY platform that imports this module —
|
|
58
|
+
* `index.ts` picks a probe at load, but the import itself is what executes the
|
|
59
|
+
* call, so the dispatch cannot prevent it. On Linux the Darwin dylibs do not
|
|
60
|
+
* exist, so the import threw (`Failed to open library "libc.dylib"`) and took
|
|
61
|
+
* down every module transitively importing the probe, tests included. Opening
|
|
62
|
+
* them on first use keeps this module importable everywhere, and a host that
|
|
63
|
+
* cannot open them gets the `null` degradation every method here promises.
|
|
64
|
+
*/
|
|
65
|
+
const LIBC_SYMBOLS = {
|
|
66
|
+
sysctl: { args: [FFIType.ptr, FFIType.u32, FFIType.ptr, FFIType.ptr, FFIType.ptr, FFIType.u64], returns: FFIType.i32 },
|
|
67
|
+
} satisfies Record<string, FFIFunction>;
|
|
68
|
+
|
|
69
|
+
const LIBPROC_SYMBOLS = {
|
|
70
|
+
proc_pidpath: { args: [FFIType.i32, FFIType.ptr, FFIType.u32], returns: FFIType.i32 },
|
|
71
|
+
proc_pidinfo: { args: [FFIType.i32, FFIType.i32, FFIType.u64, FFIType.ptr, FFIType.i32], returns: FFIType.i32 },
|
|
72
|
+
} satisfies Record<string, FFIFunction>;
|
|
73
|
+
|
|
74
|
+
/** The opened Darwin libraries, as used by the probe below. */
|
|
75
|
+
interface DarwinLibs {
|
|
76
|
+
libc: Library<typeof LIBC_SYMBOLS>;
|
|
77
|
+
libproc: Library<typeof LIBPROC_SYMBOLS>;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
let libs: DarwinLibs | null = null;
|
|
81
|
+
let libsAttempted = false;
|
|
82
|
+
|
|
83
|
+
/** Open the libraries once, and remember a host that cannot as unavailable. */
|
|
84
|
+
function bindLibs(): DarwinLibs | null {
|
|
85
|
+
if (!libsAttempted) {
|
|
86
|
+
libsAttempted = true;
|
|
87
|
+
try {
|
|
88
|
+
libs = {
|
|
89
|
+
libc: dlopen('libc.dylib', LIBC_SYMBOLS),
|
|
90
|
+
libproc: dlopen('/usr/lib/libproc.dylib', LIBPROC_SYMBOLS),
|
|
91
|
+
};
|
|
92
|
+
} catch {
|
|
93
|
+
libs = null;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return libs;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** `PROC_PIDTBSDINFO` — the flavor whose reply is a `struct proc_bsdinfo`. */
|
|
100
|
+
const PROC_PIDTBSDINFO = 3;
|
|
101
|
+
/** `sizeof(struct proc_bsdinfo)`, verified against this platform. */
|
|
102
|
+
const BSDINFO_BYTES = 136;
|
|
103
|
+
/** `pbi_pid` — the identity check that guards every offset below. */
|
|
104
|
+
const BSDINFO_PID_OFFSET = 12;
|
|
105
|
+
/** `e_tpgid` — the field `ps -o tpgid=` prints. */
|
|
106
|
+
const BSDINFO_TPGID_OFFSET = 112;
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* `p_stat` for a live PID, or null when the PID does not exist.
|
|
110
|
+
*
|
|
111
|
+
* A zero-length result is the kernel saying "no such process"; the call itself
|
|
112
|
+
* failing (rc !== 0) is reported as null too, and callers then fall back.
|
|
113
|
+
*/
|
|
114
|
+
function procStatus(pid: number): number | null {
|
|
115
|
+
const bound = bindLibs();
|
|
116
|
+
if (bound === null) return null;
|
|
117
|
+
const mib = new Int32Array([CTL_KERN, KERN_PROC, KERN_PROC_PID, pid]);
|
|
118
|
+
const size = new BigUint64Array([BigInt(KINFO_PROC_BYTES)]);
|
|
119
|
+
const buffer = new Uint8Array(KINFO_PROC_BYTES);
|
|
120
|
+
try {
|
|
121
|
+
if (bound.libc.symbols.sysctl(ptr(mib), 4, ptr(buffer), ptr(size), null, 0) !== 0) return null;
|
|
122
|
+
} catch {
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
if (Number(size[0]) === 0) return null;
|
|
126
|
+
return buffer[P_STAT_OFFSET];
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Scratch for `foregroundGroup` below. One module-level buffer, safe because
|
|
131
|
+
* the whole call is synchronous — no await can interleave another caller.
|
|
132
|
+
*/
|
|
133
|
+
const bsdInfo = new Uint8Array(BSDINFO_BYTES);
|
|
134
|
+
const bsdInfoView = new DataView(bsdInfo.buffer);
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Executable path, or null. Also answers for root-owned PIDs (verified: pid 1 →
|
|
138
|
+
* `/sbin/launchd`), which is what makes it the identity fallback when argv is
|
|
139
|
+
* denied.
|
|
140
|
+
*/
|
|
141
|
+
function executablePath(pid: number): string | null {
|
|
142
|
+
const bound = bindLibs();
|
|
143
|
+
if (bound === null) return null;
|
|
144
|
+
const buffer = new Uint8Array(PATH_BUFFER_BYTES);
|
|
145
|
+
try {
|
|
146
|
+
const written = bound.libproc.symbols.proc_pidpath(pid, ptr(buffer), buffer.length);
|
|
147
|
+
return written > 0 ? new TextDecoder().decode(buffer.subarray(0, written)) : null;
|
|
148
|
+
} catch {
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* argv of a live PID via `KERN_PROCARGS2`, or null.
|
|
155
|
+
*
|
|
156
|
+
* The block is laid out as: `argc` (i32), the executable path (NUL-terminated),
|
|
157
|
+
* padding NULs, then `argc` NUL-terminated arguments. Denied for root-owned
|
|
158
|
+
* PIDs, which is why `commandLine` also consults the executable path.
|
|
159
|
+
*/
|
|
160
|
+
function argvOf(pid: number): string[] | null {
|
|
161
|
+
const bound = bindLibs();
|
|
162
|
+
if (bound === null) return null;
|
|
163
|
+
const mib = new Int32Array([CTL_KERN, KERN_PROCARGS2, pid]);
|
|
164
|
+
const size = new BigUint64Array([BigInt(ARGV_BUFFER_BYTES)]);
|
|
165
|
+
const buffer = new Uint8Array(ARGV_BUFFER_BYTES);
|
|
166
|
+
try {
|
|
167
|
+
if (bound.libc.symbols.sysctl(ptr(mib), 3, ptr(buffer), ptr(size), null, 0) !== 0) return null;
|
|
168
|
+
} catch {
|
|
169
|
+
return null;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const length = Number(size[0]);
|
|
173
|
+
if (length <= 4) return null;
|
|
174
|
+
const argc = new DataView(buffer.buffer, buffer.byteOffset, buffer.byteLength).getInt32(0, true);
|
|
175
|
+
let cursor = 4;
|
|
176
|
+
while (cursor < length && buffer[cursor] !== 0) cursor += 1;
|
|
177
|
+
while (cursor < length && buffer[cursor] === 0) cursor += 1;
|
|
178
|
+
|
|
179
|
+
const decoder = new TextDecoder();
|
|
180
|
+
const args: string[] = [];
|
|
181
|
+
for (let index = 0; index < argc && cursor < length; index += 1) {
|
|
182
|
+
let end = cursor;
|
|
183
|
+
while (end < length && buffer[end] !== 0) end += 1;
|
|
184
|
+
args.push(decoder.decode(buffer.subarray(cursor, end)));
|
|
185
|
+
cursor = end + 1;
|
|
186
|
+
}
|
|
187
|
+
return args;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function signalAlive(pid: number): boolean {
|
|
191
|
+
try {
|
|
192
|
+
process.kill(pid, 0);
|
|
193
|
+
return true;
|
|
194
|
+
} catch (error) {
|
|
195
|
+
// EPERM: the PID exists, we may not signal it (root-owned, another user).
|
|
196
|
+
return (error as NodeJS.ErrnoException).code === 'EPERM';
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export const darwinProbe: ProcessProbe = {
|
|
201
|
+
commandLine(pid) {
|
|
202
|
+
const args = argvOf(pid);
|
|
203
|
+
if (args && args.length > 0) return args.join(' ');
|
|
204
|
+
return executablePath(pid);
|
|
205
|
+
},
|
|
206
|
+
isAlive(pid) {
|
|
207
|
+
const status = procStatus(pid);
|
|
208
|
+
if (status !== null) return !SZOMB_LIKE.has(status);
|
|
209
|
+
// The syscall itself failed (sandbox) — liveness alone is all that is left.
|
|
210
|
+
return executablePath(pid) !== null || signalAlive(pid);
|
|
211
|
+
},
|
|
212
|
+
isZombie(pid) {
|
|
213
|
+
const status = procStatus(pid);
|
|
214
|
+
return status !== null && SZOMB_LIKE.has(status);
|
|
215
|
+
},
|
|
216
|
+
/**
|
|
217
|
+
* `e_tpgid` — the field `ps -o tpgid=` prints. `proc_pidinfo` is the cheap
|
|
218
|
+
* route to it (measured ~0.9 µs against ~10 ms for the subprocess).
|
|
219
|
+
*
|
|
220
|
+
* The reply's own `pbi_pid` is checked against the requested pid before
|
|
221
|
+
* `e_tpgid` is trusted: a struct-layout drift on a future macOS would
|
|
222
|
+
* otherwise hand back a garbage group, and a wrong "busy" answer makes the
|
|
223
|
+
* terminal cap refuse an attach. Returning null keeps the caller on `ps`.
|
|
224
|
+
*/
|
|
225
|
+
foregroundGroup(pid) {
|
|
226
|
+
const bound = bindLibs();
|
|
227
|
+
if (bound === null) return null;
|
|
228
|
+
let written = 0;
|
|
229
|
+
try {
|
|
230
|
+
written = bound.libproc.symbols.proc_pidinfo(pid, PROC_PIDTBSDINFO, 0, ptr(bsdInfo), BSDINFO_BYTES);
|
|
231
|
+
} catch {
|
|
232
|
+
return null;
|
|
233
|
+
}
|
|
234
|
+
if (written < BSDINFO_BYTES) return null;
|
|
235
|
+
if (bsdInfoView.getUint32(BSDINFO_PID_OFFSET, true) !== pid) return null;
|
|
236
|
+
return bsdInfoView.getUint32(BSDINFO_TPGID_OFFSET, true);
|
|
237
|
+
},
|
|
238
|
+
};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Which process probe answers on this platform.
|
|
8
|
+
*
|
|
9
|
+
* The choice is by `process.platform`, at module load: macOS gets `libproc`,
|
|
10
|
+
* Linux gets `/proc`, Windows gets the liveness-only probe, and anything else
|
|
11
|
+
* (the BSDs) falls back to `ps`. No caller knows which one it got.
|
|
12
|
+
*
|
|
13
|
+
* macOS and Linux are the native paths because they are the platforms the
|
|
14
|
+
* chamber actually runs on and where a subprocess-free answer exists; the `ps`
|
|
15
|
+
* fallback stays for the platforms that have no other option.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import type { ProcessProbe } from '@/server/lib/lifecycle/proc/types';
|
|
19
|
+
import { darwinProbe } from '@/server/lib/lifecycle/proc/darwin';
|
|
20
|
+
import { linuxProbe } from '@/server/lib/lifecycle/proc/linux';
|
|
21
|
+
import { psProbe } from '@/server/lib/lifecycle/proc/ps';
|
|
22
|
+
import { win32Probe } from '@/server/lib/lifecycle/proc/win32';
|
|
23
|
+
|
|
24
|
+
function selectProbe(): ProcessProbe {
|
|
25
|
+
switch (process.platform) {
|
|
26
|
+
case 'darwin':
|
|
27
|
+
return darwinProbe;
|
|
28
|
+
case 'linux':
|
|
29
|
+
return linuxProbe;
|
|
30
|
+
case 'win32':
|
|
31
|
+
return win32Probe;
|
|
32
|
+
default:
|
|
33
|
+
return psProbe;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** The probe for the running platform. */
|
|
38
|
+
export const processProbe: ProcessProbe = selectProbe();
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Linux process probe: `/proc`, no subprocess and no FFI.
|
|
8
|
+
*
|
|
9
|
+
* `procfs` already holds everything `ps` reads, so this is both faster and
|
|
10
|
+
* immune to a missing binary in minimal containers (where the previous `ps`
|
|
11
|
+
* snapshot simply came back empty and identity degraded to `unknown`).
|
|
12
|
+
*
|
|
13
|
+
* `stat` is parsed from the LAST `)` because field 2 is the executable name in
|
|
14
|
+
* parentheses and may itself contain spaces and parentheses.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import fs from 'fs';
|
|
18
|
+
|
|
19
|
+
import type { ProcessProbe } from '@/server/lib/lifecycle/proc/types';
|
|
20
|
+
|
|
21
|
+
/** Field 3 of `/proc/<pid>/stat`, 1-indexed, once the comm field is stripped. */
|
|
22
|
+
const STATE_FIELD = 0;
|
|
23
|
+
const ZOMBIE_STATES = new Set(['Z', 'X']);
|
|
24
|
+
|
|
25
|
+
function readFile(path: string): string | null {
|
|
26
|
+
try {
|
|
27
|
+
return fs.readFileSync(path, 'utf8');
|
|
28
|
+
} catch {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** `/proc/<pid>/stat` fields after the `(comm)` column, or null. */
|
|
34
|
+
function statTail(pid: number): string[] | null {
|
|
35
|
+
const raw = readFile(`/proc/${pid}/stat`);
|
|
36
|
+
if (raw === null) return null;
|
|
37
|
+
const close = raw.lastIndexOf(')');
|
|
38
|
+
if (close === -1) return null;
|
|
39
|
+
return raw.slice(close + 1).trim().split(/\s+/);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Index of tpgid in `statTail`'s output.
|
|
44
|
+
*
|
|
45
|
+
* `/proc/<pid>/stat` numbers it 8 (1-indexed), and `statTail` has already
|
|
46
|
+
* dropped fields 1 (`pid`) and 2 (`(comm)`) — so the index is 8 - 3 = 5. At 6
|
|
47
|
+
* this read field 9, `flags`, which answered every live process with 0x400000
|
|
48
|
+
* instead of a process group.
|
|
49
|
+
*/
|
|
50
|
+
const TPGID_FIELD = 5;
|
|
51
|
+
|
|
52
|
+
export const linuxProbe: ProcessProbe = {
|
|
53
|
+
commandLine(pid) {
|
|
54
|
+
const raw = readFile(`/proc/${pid}/cmdline`);
|
|
55
|
+
if (raw === null) return null;
|
|
56
|
+
// NUL-separated argv, with a trailing NUL; empty for a kernel thread.
|
|
57
|
+
const args = raw.split('\0').filter((part) => part.length > 0);
|
|
58
|
+
if (args.length > 0) return args.join(' ');
|
|
59
|
+
// Kernel threads have no argv — `comm` is all the identity there is.
|
|
60
|
+
return readFile(`/proc/${pid}/comm`)?.trim() || null;
|
|
61
|
+
},
|
|
62
|
+
isAlive(pid) {
|
|
63
|
+
// A `/proc` entry exists for a zombie too, so liveness is "the directory is
|
|
64
|
+
// there"; `isZombie` is what separates a corpse from a running process.
|
|
65
|
+
return fs.existsSync(`/proc/${pid}`);
|
|
66
|
+
},
|
|
67
|
+
isZombie(pid) {
|
|
68
|
+
const fields = statTail(pid);
|
|
69
|
+
if (fields === null) return false;
|
|
70
|
+
return ZOMBIE_STATES.has(fields[STATE_FIELD]);
|
|
71
|
+
},
|
|
72
|
+
foregroundGroup(pid) {
|
|
73
|
+
// Already read for `isZombie` on the same question, so this costs no extra
|
|
74
|
+
// syscall beyond the file read itself.
|
|
75
|
+
const fields = statTail(pid);
|
|
76
|
+
if (fields === null) return null;
|
|
77
|
+
const tpgid = Number(fields[TPGID_FIELD]);
|
|
78
|
+
return Number.isFinite(tpgid) ? tpgid : null;
|
|
79
|
+
},
|
|
80
|
+
};
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* `ps` fallback for POSIX platforms without a native probe (the BSDs).
|
|
8
|
+
*
|
|
9
|
+
* Kept because it is the only mechanism left that works everywhere a `ps` is
|
|
10
|
+
* installed, and because dropping it would make identity worse on those
|
|
11
|
+
* platforms than it was before the native probes existed. macOS and Linux no
|
|
12
|
+
* longer reach this path.
|
|
13
|
+
*
|
|
14
|
+
* The whole table is read once and cached briefly: every question in a port
|
|
15
|
+
* check happens within milliseconds, so one snapshot answers all of them.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { spawnSync } from 'bun';
|
|
19
|
+
|
|
20
|
+
import type { ProcessProbe } from '@/server/lib/lifecycle/proc/types';
|
|
21
|
+
|
|
22
|
+
interface ProcessRow {
|
|
23
|
+
pid: number;
|
|
24
|
+
/** `ps` state column — `Z…` means defunct (exited, awaiting reaping). */
|
|
25
|
+
state: string;
|
|
26
|
+
command: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const TABLE_MAX_AGE_MS = 250;
|
|
30
|
+
let cached: { at: number; rows: ProcessRow[] } | null = null;
|
|
31
|
+
|
|
32
|
+
function processTable(): ProcessRow[] {
|
|
33
|
+
if (cached && Date.now() - cached.at < TABLE_MAX_AGE_MS) return cached.rows;
|
|
34
|
+
const rows: ProcessRow[] = [];
|
|
35
|
+
try {
|
|
36
|
+
const snapshot = spawnSync({
|
|
37
|
+
cmd: ['ps', '-ax', '-o', 'pid=,stat=,command='],
|
|
38
|
+
stdout: 'pipe',
|
|
39
|
+
stderr: 'ignore',
|
|
40
|
+
});
|
|
41
|
+
if (snapshot.success) {
|
|
42
|
+
for (const line of snapshot.stdout.toString().split('\n')) {
|
|
43
|
+
const match = /^\s*(\d+)\s+(\S+)\s+(.*)$/.exec(line);
|
|
44
|
+
if (!match) continue;
|
|
45
|
+
rows.push({ pid: Number(match[1]), state: match[2], command: match[3].trim() });
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
} catch {
|
|
49
|
+
// No `ps` on PATH: every question degrades to "unknown".
|
|
50
|
+
}
|
|
51
|
+
cached = { at: Date.now(), rows };
|
|
52
|
+
return rows;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function signalAlive(pid: number): boolean {
|
|
56
|
+
try {
|
|
57
|
+
process.kill(pid, 0);
|
|
58
|
+
return true;
|
|
59
|
+
} catch (error) {
|
|
60
|
+
// EPERM: the PID exists, we may not signal it.
|
|
61
|
+
return (error as NodeJS.ErrnoException).code === 'EPERM';
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export const psProbe: ProcessProbe = {
|
|
66
|
+
commandLine(pid) {
|
|
67
|
+
const row = processTable().find((entry) => entry.pid === pid);
|
|
68
|
+
return row && row.command.length > 0 ? row.command : null;
|
|
69
|
+
},
|
|
70
|
+
isAlive(pid) {
|
|
71
|
+
if (!signalAlive(pid)) return false;
|
|
72
|
+
const row = processTable().find((entry) => entry.pid === pid);
|
|
73
|
+
// Absent from the table means no `ps` (or a race) — liveness stands.
|
|
74
|
+
return row ? !row.state.startsWith('Z') : true;
|
|
75
|
+
},
|
|
76
|
+
isZombie(pid) {
|
|
77
|
+
const row = processTable().find((entry) => entry.pid === pid);
|
|
78
|
+
return row ? row.state.startsWith('Z') : false;
|
|
79
|
+
},
|
|
80
|
+
foregroundGroup() {
|
|
81
|
+
// The cached table has no `tpgid` column, and adding one would make every
|
|
82
|
+
// question pay for a field only this one asks. Null keeps the caller on its
|
|
83
|
+
// own `ps -o pid=,tpgid=` path, which is where the BSDs were anyway.
|
|
84
|
+
return null;
|
|
85
|
+
},
|
|
86
|
+
};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* What a platform must be able to answer about a foreign PID, in its own
|
|
8
|
+
* syscall vocabulary. One implementation per OS, selected in `identity.ts`.
|
|
9
|
+
*
|
|
10
|
+
* Every method must degrade rather than throw: a diagnostic question about a
|
|
11
|
+
* PID is never worth failing a request over, and a platform that cannot answer
|
|
12
|
+
* says so (`null` / `false`) so the caller can fall back.
|
|
13
|
+
*/
|
|
14
|
+
export interface ProcessProbe {
|
|
15
|
+
/** Command line of a live PID, or null when unreadable. */
|
|
16
|
+
commandLine(pid: number): string | null;
|
|
17
|
+
/** True when the PID exists. */
|
|
18
|
+
isAlive(pid: number): boolean;
|
|
19
|
+
/** True when the PID is defunct — exited but not yet reaped by its parent.
|
|
20
|
+
* Such a PID still answers signal 0 and holds no port. */
|
|
21
|
+
isZombie(pid: number): boolean;
|
|
22
|
+
/**
|
|
23
|
+
* Foreground process group of the PID's controlling terminal — the value
|
|
24
|
+
* `ps -o tpgid=` prints, including its `0` for "no controlling terminal".
|
|
25
|
+
* Null when this platform (or this call) cannot answer, which is the
|
|
26
|
+
* caller's signal to fall back to `ps`.
|
|
27
|
+
*
|
|
28
|
+
* A PTY's `tpgid` is what tells a shell sitting at its prompt (the group is
|
|
29
|
+
* the shell itself) from one running a command (the group is that command),
|
|
30
|
+
* so it is the exact answer to "is this terminal busy".
|
|
31
|
+
*/
|
|
32
|
+
foregroundGroup(pid: number): number | null;
|
|
33
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Windows process probe.
|
|
8
|
+
*
|
|
9
|
+
* Deliberately the thinnest of the three, because Windows is where the other
|
|
10
|
+
* two mechanisms have no equivalent:
|
|
11
|
+
*
|
|
12
|
+
* - There is no `ps` and no `/proc`.
|
|
13
|
+
* - There is no zombie: a process object lives until its last handle closes,
|
|
14
|
+
* so "exited but not reaped" is not a state that can be observed. `isZombie`
|
|
15
|
+
* is therefore always false rather than a guess.
|
|
16
|
+
* - A foreign process's command line is not reachable without walking its PEB
|
|
17
|
+
* through `ntdll` while it is suspended — untestable here and far more
|
|
18
|
+
* dangerous than the problem it solves. `commandLine` returns null, which
|
|
19
|
+
* `getProcessState` already reports as `unknown`, and callers treat
|
|
20
|
+
* `unknown` as "alive but unverified" — never as confirmation.
|
|
21
|
+
*
|
|
22
|
+
* So identity on Windows is liveness, exactly as before this module existed;
|
|
23
|
+
* the value here is that the behaviour is stated instead of emerging from a
|
|
24
|
+
* `ps` snapshot that silently comes back empty.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import type { ProcessProbe } from '@/server/lib/lifecycle/proc/types';
|
|
28
|
+
|
|
29
|
+
function signalAlive(pid: number): boolean {
|
|
30
|
+
try {
|
|
31
|
+
process.kill(pid, 0);
|
|
32
|
+
return true;
|
|
33
|
+
} catch (error) {
|
|
34
|
+
// EPERM: the PID exists, we may not signal it.
|
|
35
|
+
return (error as NodeJS.ErrnoException).code === 'EPERM';
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export const win32Probe: ProcessProbe = {
|
|
40
|
+
commandLine() {
|
|
41
|
+
return null;
|
|
42
|
+
},
|
|
43
|
+
isAlive: signalAlive,
|
|
44
|
+
isZombie() {
|
|
45
|
+
return false;
|
|
46
|
+
},
|
|
47
|
+
foregroundGroup() {
|
|
48
|
+
// Windows has no process group tied to a console the way a POSIX PTY has
|
|
49
|
+
// `tpgid`, and no `ps` either — the caller's own platform check keeps this
|
|
50
|
+
// from being asked, and null says "unknown" rather than guessing.
|
|
51
|
+
return null;
|
|
52
|
+
},
|
|
53
|
+
};
|
|
@@ -11,8 +11,8 @@
|
|
|
11
11
|
* files and drives the shared utility RPC process.
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
-
import { readDisabledProviders } from '@/server/lib/omp/config/
|
|
15
|
-
import { getModelsConfigPath, readNativeProviders } from '@/server/lib/omp/config/
|
|
14
|
+
import { readDisabledProviders } from '@/server/lib/omp/config/disabled-providers';
|
|
15
|
+
import { getModelsConfigPath, readNativeProviders } from '@/server/lib/omp/config/models-config';
|
|
16
16
|
import { runUtilityCommand, type OmpModel } from '@/server/lib/omp/rpc/utility';
|
|
17
17
|
import { isKenariProvider, removeLegacyKenariModels } from '@/shared/lib/models/provider-cleanup';
|
|
18
18
|
import { formatContextWindow } from '@/shared/lib/code/format';
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
import { getDb } from '@/server/db.server';
|
|
16
16
|
import { PROVIDERS_SETTINGS_KEY } from '@/server/lib/models/provider-registry.server';
|
|
17
17
|
import { readSettingsJson } from '@/server/lib/db/settings-store';
|
|
18
|
-
import { readDisabledProviders } from '@/server/lib/omp/config/
|
|
18
|
+
import { readDisabledProviders } from '@/server/lib/omp/config/disabled-providers';
|
|
19
19
|
import { runUtilityCommand, type OmpModel } from '@/server/lib/omp/rpc/utility';
|
|
20
20
|
import { invalidateModelsCaches } from '@/shared/lib/models/server-cache';
|
|
21
21
|
import type { ModelsData, ProviderItem } from '@/shared/types';
|