@orion-agents/orion-code 0.3.0 → 0.3.2
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 +85 -0
- package/README.md +45 -18
- package/README.zh-CN.md +39 -18
- package/dist/commands/model-command-handlers.d.ts.map +1 -1
- package/dist/commands/model-command-handlers.js +59 -14
- package/dist/commands/model-command-handlers.js.map +1 -1
- package/dist/commands/types.d.ts +6 -0
- package/dist/commands/types.d.ts.map +1 -1
- package/dist/commands/types.js.map +1 -1
- package/dist/commands/workflow-command-handlers.d.ts +1 -1
- package/dist/commands/workflow-command-handlers.d.ts.map +1 -1
- package/dist/commands/workflow-command-handlers.js +42 -3
- package/dist/commands/workflow-command-handlers.js.map +1 -1
- package/dist/commands/workflow-commands.d.ts.map +1 -1
- package/dist/commands/workflow-commands.js +2 -2
- package/dist/commands/workflow-commands.js.map +1 -1
- package/dist/core/tool-artifacts.d.ts +2 -0
- package/dist/core/tool-artifacts.d.ts.map +1 -1
- package/dist/core/tool-artifacts.js +27 -1
- package/dist/core/tool-artifacts.js.map +1 -1
- package/dist/runtime/agent-runtime-controller.d.ts +16 -2
- package/dist/runtime/agent-runtime-controller.d.ts.map +1 -1
- package/dist/runtime/agent-runtime-controller.js +116 -8
- package/dist/runtime/agent-runtime-controller.js.map +1 -1
- package/dist/runtime/agent-runtime-protocol.d.ts +7 -0
- package/dist/runtime/agent-runtime-protocol.d.ts.map +1 -1
- package/dist/runtime/agent-runtime-protocol.js.map +1 -1
- package/dist/runtime/agent-runtime-runner.d.ts +8 -0
- package/dist/runtime/agent-runtime-runner.d.ts.map +1 -1
- package/dist/runtime/durable-tool-receipt-reader.d.ts +32 -0
- package/dist/runtime/durable-tool-receipt-reader.d.ts.map +1 -0
- package/dist/runtime/durable-tool-receipt-reader.js +130 -0
- package/dist/runtime/durable-tool-receipt-reader.js.map +1 -0
- package/dist/runtime/legacy-thread-materializer.d.ts +14 -1
- package/dist/runtime/legacy-thread-materializer.d.ts.map +1 -1
- package/dist/runtime/legacy-thread-materializer.js +44 -5
- package/dist/runtime/legacy-thread-materializer.js.map +1 -1
- package/dist/runtime/orion-session-runner.d.ts +4 -0
- package/dist/runtime/orion-session-runner.d.ts.map +1 -1
- package/dist/runtime/orion-session-runner.js +15 -0
- package/dist/runtime/orion-session-runner.js.map +1 -1
- package/dist/runtime/plan-review.d.ts +38 -0
- package/dist/runtime/plan-review.d.ts.map +1 -0
- package/dist/runtime/plan-review.js +173 -0
- package/dist/runtime/plan-review.js.map +1 -0
- package/dist/runtime/product-bootstrap.d.ts.map +1 -1
- package/dist/runtime/product-bootstrap.js +28 -6
- package/dist/runtime/product-bootstrap.js.map +1 -1
- package/dist/runtime/product-orion-runtime.d.ts.map +1 -1
- package/dist/runtime/product-orion-runtime.js +14 -35
- package/dist/runtime/product-orion-runtime.js.map +1 -1
- package/dist/runtime/protocol/runtime-protocol-v1.d.ts +73 -0
- package/dist/runtime/protocol/runtime-protocol-v1.d.ts.map +1 -1
- package/dist/runtime/protocol/runtime-protocol-v1.js +37 -0
- package/dist/runtime/protocol/runtime-protocol-v1.js.map +1 -1
- package/dist/runtime/release-receipts.d.ts +8 -3
- package/dist/runtime/release-receipts.d.ts.map +1 -1
- package/dist/runtime/release-receipts.js +88 -4
- package/dist/runtime/release-receipts.js.map +1 -1
- package/dist/runtime/session-composer-control.d.ts +136 -0
- package/dist/runtime/session-composer-control.d.ts.map +1 -0
- package/dist/runtime/session-composer-control.js +532 -0
- package/dist/runtime/session-composer-control.js.map +1 -0
- package/dist/runtime/thread-event-store.d.ts +62 -9
- package/dist/runtime/thread-event-store.d.ts.map +1 -1
- package/dist/runtime/thread-event-store.js +363 -31
- package/dist/runtime/thread-event-store.js.map +1 -1
- package/dist/runtime/thread-projection.d.ts +17 -0
- package/dist/runtime/thread-projection.d.ts.map +1 -1
- package/dist/runtime/thread-projection.js +87 -0
- package/dist/runtime/thread-projection.js.map +1 -1
- package/dist/runtime/thread-session-index.d.ts +84 -0
- package/dist/runtime/thread-session-index.d.ts.map +1 -0
- package/dist/runtime/thread-session-index.js +503 -0
- package/dist/runtime/thread-session-index.js.map +1 -0
- package/dist/runtime/thread-session-view.d.ts +31 -13
- package/dist/runtime/thread-session-view.d.ts.map +1 -1
- package/dist/runtime/thread-session-view.js +116 -61
- package/dist/runtime/thread-session-view.js.map +1 -1
- package/dist/runtime/thread-ui-adapter.d.ts.map +1 -1
- package/dist/runtime/thread-ui-adapter.js +18 -58
- package/dist/runtime/thread-ui-adapter.js.map +1 -1
- package/dist/runtime/tool-detail-repository.d.ts.map +1 -1
- package/dist/runtime/tool-detail-repository.js +23 -19
- package/dist/runtime/tool-detail-repository.js.map +1 -1
- package/dist/runtime/tool-receipt-validator.d.ts +21 -0
- package/dist/runtime/tool-receipt-validator.d.ts.map +1 -0
- package/dist/runtime/tool-receipt-validator.js +157 -0
- package/dist/runtime/tool-receipt-validator.js.map +1 -0
- package/dist/runtime/ui-events.d.ts +6 -1
- package/dist/runtime/ui-events.d.ts.map +1 -1
- package/dist/runtime/ui-events.js.map +1 -1
- package/dist/services/redaction.d.ts +4 -0
- package/dist/services/redaction.d.ts.map +1 -1
- package/dist/services/redaction.js +63 -0
- package/dist/services/redaction.js.map +1 -1
- package/dist/services/session-index.d.ts +16 -0
- package/dist/services/session-index.d.ts.map +1 -1
- package/dist/services/session-index.js +45 -36
- package/dist/services/session-index.js.map +1 -1
- package/dist/services/session-storage.d.ts +36 -0
- package/dist/services/session-storage.d.ts.map +1 -1
- package/dist/services/session-storage.js +80 -7
- package/dist/services/session-storage.js.map +1 -1
- package/dist/services/workspace-registry.d.ts +42 -0
- package/dist/services/workspace-registry.d.ts.map +1 -0
- package/dist/services/workspace-registry.js +222 -0
- package/dist/services/workspace-registry.js.map +1 -0
- package/dist/web/errors.d.ts +6 -0
- package/dist/web/errors.d.ts.map +1 -0
- package/dist/web/errors.js +13 -0
- package/dist/web/errors.js.map +1 -0
- package/dist/web/event-hub.d.ts +4 -0
- package/dist/web/event-hub.d.ts.map +1 -1
- package/dist/web/event-hub.js +81 -11
- package/dist/web/event-hub.js.map +1 -1
- package/dist/web/file-read-service.d.ts +64 -0
- package/dist/web/file-read-service.d.ts.map +1 -0
- package/dist/web/file-read-service.js +409 -0
- package/dist/web/file-read-service.js.map +1 -0
- package/dist/web/git-read-model-service.d.ts +85 -0
- package/dist/web/git-read-model-service.d.ts.map +1 -0
- package/dist/web/git-read-model-service.js +657 -0
- package/dist/web/git-read-model-service.js.map +1 -0
- package/dist/web/protocol.d.ts +205 -1
- package/dist/web/protocol.d.ts.map +1 -1
- package/dist/web/protocol.js +229 -1
- package/dist/web/protocol.js.map +1 -1
- package/dist/web/review-service.d.ts +38 -0
- package/dist/web/review-service.d.ts.map +1 -0
- package/dist/web/review-service.js +74 -0
- package/dist/web/review-service.js.map +1 -0
- package/dist/web/server.d.ts.map +1 -1
- package/dist/web/server.js +312 -29
- package/dist/web/server.js.map +1 -1
- package/dist/web/terminal-manager.d.ts +156 -0
- package/dist/web/terminal-manager.d.ts.map +1 -0
- package/dist/web/terminal-manager.js +760 -0
- package/dist/web/terminal-manager.js.map +1 -0
- package/dist/web/terminal-server.d.ts +8 -0
- package/dist/web/terminal-server.d.ts.map +1 -0
- package/dist/web/terminal-server.js +275 -0
- package/dist/web/terminal-server.js.map +1 -0
- package/dist/web/workbench-controller.d.ts +71 -18
- package/dist/web/workbench-controller.d.ts.map +1 -1
- package/dist/web/workbench-controller.js +1187 -178
- package/dist/web/workbench-controller.js.map +1 -1
- package/dist/web-client/assets/DiffViewer-eaQNsnCh.js +5 -0
- package/dist/web-client/assets/FilesPanel-DYs1MhoM.js +2 -0
- package/dist/web-client/assets/GitPanel-CnU4XVr7.js +1 -0
- package/dist/web-client/assets/ReviewPanel-BDxG670T.js +1 -0
- package/dist/web-client/assets/TerminalPanel-DLuoa74B.css +1 -0
- package/dist/web-client/assets/TerminalPanel-Q5mSSsoK.js +23 -0
- package/dist/web-client/assets/index-BWV5sIvw.css +1 -0
- package/dist/web-client/assets/index-VVUKj6FE.js +17 -0
- package/dist/web-client/index.html +2 -2
- package/docs/architecture/agent-mode-permission-contract.md +87 -0
- package/docs/architecture/v0.3.0-web-api.yaml +44 -13
- package/docs/architecture/v0.3.1-web-api.yaml +2338 -0
- package/docs/architecture/v0.3.2-web-api.yaml +3029 -0
- package/docs/migration/v0.3.0-to-v0.3.1.md +88 -0
- package/docs/migration/v0.3.1-to-v0.3.2.md +94 -0
- package/docs/plan/v0.3.0-web-workbench-plan.md +58 -7
- package/docs/plan/v0.3.1-web-workbench-professional-shell-plan.md +848 -0
- package/docs/plan/v0.3.2-web-workbench-layout-and-composer-plan.md +869 -0
- package/docs/readme.md +20 -0
- package/docs/test/v0.3.1-web-workbench-e2e-plan.md +140 -0
- package/docs/test/v0.3.2-web-workbench-e2e-plan.md +100 -0
- package/npm-shrinkwrap.json +185 -354
- package/package.json +21 -4
- package/dist/web-client/assets/index-BAEO71Gp.css +0 -1
- package/dist/web-client/assets/index-DSkFy7gM.js +0 -13
|
@@ -0,0 +1,760 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.TerminalManagerV1 = void 0;
|
|
4
|
+
const crypto_1 = require("crypto");
|
|
5
|
+
const child_process_1 = require("child_process");
|
|
6
|
+
const fs_1 = require("fs");
|
|
7
|
+
const path_1 = require("path");
|
|
8
|
+
const errors_1 = require("./errors");
|
|
9
|
+
const MAX_TERMINALS = 4;
|
|
10
|
+
const MAX_WORKSPACE_TERMINALS = 2;
|
|
11
|
+
const MAX_SCROLLBACK_BYTES = 2 * 1024 * 1024;
|
|
12
|
+
const MAX_INPUT_BYTES = 64 * 1024;
|
|
13
|
+
const MAX_OUTPUT_FRAME_CHARS = 8000;
|
|
14
|
+
const OUTPUT_COALESCE_MS = 8;
|
|
15
|
+
const DEFAULT_TICKET_TTL_MS = 15000;
|
|
16
|
+
const DEFAULT_IDLE_TIMEOUT_MS = 30 * 60 * 1000;
|
|
17
|
+
const DEFAULT_FORCE_KILL_DELAY_MS = 1000;
|
|
18
|
+
const DEFAULT_SHUTDOWN_GRACE_MS = 1000;
|
|
19
|
+
const DEFAULT_FORCE_KILL_GRACE_MS = 1000;
|
|
20
|
+
const PROCESS_REAP_POLL_MS = 25;
|
|
21
|
+
/** Owns explicit-user PTYs; terminal bytes never enter Workbench SSE or durable storage. */
|
|
22
|
+
class TerminalManagerV1 {
|
|
23
|
+
constructor(options) {
|
|
24
|
+
this.terminals = new Map();
|
|
25
|
+
this.tickets = new Map();
|
|
26
|
+
this.closed = false;
|
|
27
|
+
this.resolveWorkspace = options.resolveWorkspace;
|
|
28
|
+
this.getActiveContext = options.getActiveContext;
|
|
29
|
+
this.backend = options.backend;
|
|
30
|
+
this.now = options.now ?? Date.now;
|
|
31
|
+
this.ticketTtlMs = boundedPositive(options.ticketTtlMs ?? DEFAULT_TICKET_TTL_MS, 'ticketTtlMs');
|
|
32
|
+
this.idleTimeoutMs = boundedPositive(options.idleTimeoutMs ?? DEFAULT_IDLE_TIMEOUT_MS, 'idleTimeoutMs');
|
|
33
|
+
this.forceKillDelayMs = boundedPositive(options.forceKillDelayMs ?? DEFAULT_FORCE_KILL_DELAY_MS, 'forceKillDelayMs');
|
|
34
|
+
this.shutdownGraceMs = boundedPositive(options.shutdownGraceMs ?? DEFAULT_SHUTDOWN_GRACE_MS, 'shutdownGraceMs');
|
|
35
|
+
this.forceKillGraceMs = boundedPositive(options.forceKillGraceMs ?? DEFAULT_FORCE_KILL_GRACE_MS, 'forceKillGraceMs');
|
|
36
|
+
this.onWorkspaceMutationHint = options.onWorkspaceMutationHint;
|
|
37
|
+
}
|
|
38
|
+
get available() {
|
|
39
|
+
if (this.backend)
|
|
40
|
+
return true;
|
|
41
|
+
try {
|
|
42
|
+
loadNodePtyBackend();
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
list(workspaceId) {
|
|
50
|
+
return Object.freeze([...this.terminals.values()]
|
|
51
|
+
.filter(entry => entry.workspaceId === workspaceId)
|
|
52
|
+
.sort((left, right) => left.createdAtMs - right.createdAtMs)
|
|
53
|
+
.map(entry => this.metadata(entry)));
|
|
54
|
+
}
|
|
55
|
+
create(input) {
|
|
56
|
+
this.assertOpen();
|
|
57
|
+
const active = this.getActiveContext();
|
|
58
|
+
if (active.workspaceId !== input.workspaceId ||
|
|
59
|
+
active.contextRevision !== input.expectedContextRevision) {
|
|
60
|
+
throw new errors_1.WebWorkbenchError(409, 'The active Context changed before terminal creation.', 'context_revision_conflict');
|
|
61
|
+
}
|
|
62
|
+
const workspace = this.resolveWorkspace(input.workspaceId);
|
|
63
|
+
if (!workspace) {
|
|
64
|
+
throw new errors_1.WebWorkbenchError(404, 'Workspace was not found.', 'workspace_not_found');
|
|
65
|
+
}
|
|
66
|
+
const canonicalWorkspace = canonicalDirectory(workspace);
|
|
67
|
+
const running = [...this.terminals.values()].filter(entry => entry.state === 'running');
|
|
68
|
+
if (running.length >= MAX_TERMINALS) {
|
|
69
|
+
throw new errors_1.WebWorkbenchError(409, 'Terminal limit reached.', 'terminal_limit_reached');
|
|
70
|
+
}
|
|
71
|
+
if (running.filter(entry => entry.workspaceId === input.workspaceId).length >=
|
|
72
|
+
MAX_WORKSPACE_TERMINALS) {
|
|
73
|
+
throw new errors_1.WebWorkbenchError(409, 'Workspace terminal limit reached.', 'terminal_workspace_limit_reached');
|
|
74
|
+
}
|
|
75
|
+
const cols = boundedDimension(input.cols ?? 100, 2, 400, 'cols');
|
|
76
|
+
const rows = boundedDimension(input.rows ?? 30, 1, 200, 'rows');
|
|
77
|
+
const shell = resolveShell();
|
|
78
|
+
const backend = this.backend ?? loadNodePtyBackend();
|
|
79
|
+
let process;
|
|
80
|
+
try {
|
|
81
|
+
process = backend.spawn(shell, shellArguments(shell), {
|
|
82
|
+
name: 'xterm-256color',
|
|
83
|
+
cols,
|
|
84
|
+
rows,
|
|
85
|
+
cwd: canonicalWorkspace,
|
|
86
|
+
env: terminalEnvironment(shell),
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
throw new errors_1.WebWorkbenchError(503, 'The native terminal backend could not start a shell.', 'terminal_backend_unavailable');
|
|
91
|
+
}
|
|
92
|
+
const id = (0, crypto_1.randomUUID)();
|
|
93
|
+
const now = this.now();
|
|
94
|
+
const entry = {};
|
|
95
|
+
Object.assign(entry, {
|
|
96
|
+
id,
|
|
97
|
+
workspaceId: input.workspaceId,
|
|
98
|
+
cwd: canonicalWorkspace,
|
|
99
|
+
shell,
|
|
100
|
+
title: (0, path_1.basename)(shell),
|
|
101
|
+
createdAtMs: now,
|
|
102
|
+
process,
|
|
103
|
+
state: 'running',
|
|
104
|
+
cols,
|
|
105
|
+
rows,
|
|
106
|
+
lastActiveAtMs: now,
|
|
107
|
+
nextSequence: 1,
|
|
108
|
+
frames: [],
|
|
109
|
+
frameBytes: 0,
|
|
110
|
+
pendingOutput: '',
|
|
111
|
+
dataSubscription: process.onData(data => this.appendOutput(entry, data)),
|
|
112
|
+
exitSubscription: process.onExit(event => this.handleExit(entry, event)),
|
|
113
|
+
removeAfterExit: false,
|
|
114
|
+
outputPaused: false,
|
|
115
|
+
ownedProcesses: new Map(),
|
|
116
|
+
});
|
|
117
|
+
rememberProcessTree(entry);
|
|
118
|
+
this.terminals.set(id, entry);
|
|
119
|
+
this.scheduleIdle(entry);
|
|
120
|
+
const ticket = this.issueTicket(id);
|
|
121
|
+
return Object.freeze({
|
|
122
|
+
terminal: this.metadata(entry),
|
|
123
|
+
ticket: ticket.value,
|
|
124
|
+
ticketExpiresAt: new Date(ticket.expiresAt).toISOString(),
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
issueAttachTicket(input) {
|
|
128
|
+
this.assertActiveContext(input.workspaceId, input.expectedContextRevision);
|
|
129
|
+
const entry = this.requireTerminal(input.terminalId);
|
|
130
|
+
if (entry.workspaceId !== input.workspaceId) {
|
|
131
|
+
throw new errors_1.WebWorkbenchError(404, 'Terminal was not found.', 'terminal_not_found');
|
|
132
|
+
}
|
|
133
|
+
if (entry.state !== 'running') {
|
|
134
|
+
throw new errors_1.WebWorkbenchError(409, 'Terminal is no longer running.', 'terminal_not_running');
|
|
135
|
+
}
|
|
136
|
+
const ticket = this.issueTicket(entry.id);
|
|
137
|
+
return Object.freeze({
|
|
138
|
+
ticket: ticket.value,
|
|
139
|
+
ticketExpiresAt: new Date(ticket.expiresAt).toISOString(),
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
attach(input) {
|
|
143
|
+
this.assertOpen();
|
|
144
|
+
if (!Number.isSafeInteger(input.afterSequence) || input.afterSequence < 0) {
|
|
145
|
+
throw new errors_1.WebWorkbenchError(400, 'Terminal sequence is invalid.', 'terminal_sequence_invalid');
|
|
146
|
+
}
|
|
147
|
+
const entry = this.requireTerminal(input.terminalId);
|
|
148
|
+
this.consumeTicket(entry.id, input.ticket);
|
|
149
|
+
this.flushPendingOutput(entry, true);
|
|
150
|
+
if (entry.outputPaused)
|
|
151
|
+
this.setProcessOutputPaused(entry, false);
|
|
152
|
+
entry.subscriber?.onReplaced();
|
|
153
|
+
const token = (0, crypto_1.randomUUID)();
|
|
154
|
+
entry.subscriber = {
|
|
155
|
+
token,
|
|
156
|
+
onFrame: input.onFrame,
|
|
157
|
+
onExit: input.onExit,
|
|
158
|
+
onReplaced: input.onReplaced,
|
|
159
|
+
};
|
|
160
|
+
this.touch(entry);
|
|
161
|
+
const earliest = entry.frames[0]?.sequence ?? entry.nextSequence;
|
|
162
|
+
const latest = entry.nextSequence - 1;
|
|
163
|
+
const gap = input.afterSequence < earliest - 1
|
|
164
|
+
? Object.freeze({
|
|
165
|
+
type: 'gap',
|
|
166
|
+
earliestSequence: earliest,
|
|
167
|
+
latestSequence: latest,
|
|
168
|
+
})
|
|
169
|
+
: undefined;
|
|
170
|
+
const replay = entry.frames
|
|
171
|
+
.filter(frame => frame.sequence > input.afterSequence)
|
|
172
|
+
.map(frame => Object.freeze({ type: 'output', sequence: frame.sequence, data: frame.data }));
|
|
173
|
+
return Object.freeze({
|
|
174
|
+
terminal: this.metadata(entry),
|
|
175
|
+
replay: Object.freeze(replay),
|
|
176
|
+
...(gap ? { gap } : {}),
|
|
177
|
+
write: (data) => {
|
|
178
|
+
this.assertConnection(entry, token);
|
|
179
|
+
if (typeof data !== 'string' || Buffer.byteLength(data, 'utf8') > MAX_INPUT_BYTES) {
|
|
180
|
+
throw new errors_1.WebWorkbenchError(400, 'Terminal input is too large.', 'terminal_input_invalid');
|
|
181
|
+
}
|
|
182
|
+
entry.process.write(data);
|
|
183
|
+
this.touch(entry);
|
|
184
|
+
if (/[\r\n]/u.test(data))
|
|
185
|
+
this.onWorkspaceMutationHint?.(entry.workspaceId);
|
|
186
|
+
},
|
|
187
|
+
resize: (cols, rows) => {
|
|
188
|
+
this.assertConnection(entry, token);
|
|
189
|
+
const nextCols = boundedDimension(cols, 2, 400, 'cols');
|
|
190
|
+
const nextRows = boundedDimension(rows, 1, 200, 'rows');
|
|
191
|
+
entry.process.resize(nextCols, nextRows);
|
|
192
|
+
entry.cols = nextCols;
|
|
193
|
+
entry.rows = nextRows;
|
|
194
|
+
this.touch(entry);
|
|
195
|
+
},
|
|
196
|
+
setOutputPaused: (paused) => {
|
|
197
|
+
this.assertConnection(entry, token);
|
|
198
|
+
if (typeof paused !== 'boolean') {
|
|
199
|
+
throw new errors_1.WebWorkbenchError(400, 'Terminal output flow state is invalid.', 'terminal_flow_invalid');
|
|
200
|
+
}
|
|
201
|
+
this.setProcessOutputPaused(entry, paused);
|
|
202
|
+
},
|
|
203
|
+
dispose: () => {
|
|
204
|
+
if (entry.subscriber?.token !== token)
|
|
205
|
+
return;
|
|
206
|
+
entry.subscriber = undefined;
|
|
207
|
+
if (entry.outputPaused)
|
|
208
|
+
this.setProcessOutputPaused(entry, false);
|
|
209
|
+
},
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
closeTerminal(input) {
|
|
213
|
+
this.assertActiveContext(input.workspaceId, input.expectedContextRevision);
|
|
214
|
+
const entry = this.requireTerminal(input.terminalId);
|
|
215
|
+
if (entry.workspaceId !== input.workspaceId) {
|
|
216
|
+
throw new errors_1.WebWorkbenchError(404, 'Terminal was not found.', 'terminal_not_found');
|
|
217
|
+
}
|
|
218
|
+
entry.removeAfterExit = true;
|
|
219
|
+
if (entry.state === 'running') {
|
|
220
|
+
entry.state = 'closing';
|
|
221
|
+
this.terminateEntry(entry);
|
|
222
|
+
}
|
|
223
|
+
else {
|
|
224
|
+
if (isProcessTreeAlive(entry)) {
|
|
225
|
+
forceKillProcessTree(entry);
|
|
226
|
+
this.scheduleReap(entry);
|
|
227
|
+
}
|
|
228
|
+
else {
|
|
229
|
+
this.removeEntry(entry);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
return this.metadata(entry);
|
|
233
|
+
}
|
|
234
|
+
closeWorkspace(workspaceId) {
|
|
235
|
+
for (const entry of [...this.terminals.values()]) {
|
|
236
|
+
if (entry.workspaceId !== workspaceId)
|
|
237
|
+
continue;
|
|
238
|
+
entry.removeAfterExit = true;
|
|
239
|
+
if (entry.state === 'running') {
|
|
240
|
+
entry.state = 'closing';
|
|
241
|
+
this.terminateEntry(entry);
|
|
242
|
+
}
|
|
243
|
+
else {
|
|
244
|
+
if (isProcessTreeAlive(entry)) {
|
|
245
|
+
forceKillProcessTree(entry);
|
|
246
|
+
this.scheduleReap(entry);
|
|
247
|
+
}
|
|
248
|
+
else {
|
|
249
|
+
this.removeEntry(entry);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
async shutdown() {
|
|
255
|
+
if (this.shutdownPromise)
|
|
256
|
+
return this.shutdownPromise;
|
|
257
|
+
this.closed = true;
|
|
258
|
+
this.shutdownPromise = this.performShutdown();
|
|
259
|
+
return this.shutdownPromise;
|
|
260
|
+
}
|
|
261
|
+
async performShutdown() {
|
|
262
|
+
this.tickets.clear();
|
|
263
|
+
const entries = [...this.terminals.values()];
|
|
264
|
+
for (const entry of entries) {
|
|
265
|
+
entry.removeAfterExit = true;
|
|
266
|
+
if (entry.state === 'running')
|
|
267
|
+
entry.state = 'closing';
|
|
268
|
+
if (isProcessTreeAlive(entry))
|
|
269
|
+
this.terminateEntry(entry);
|
|
270
|
+
else
|
|
271
|
+
this.removeEntry(entry);
|
|
272
|
+
}
|
|
273
|
+
await this.reapStoppedEntries(entries, this.shutdownGraceMs);
|
|
274
|
+
const survivors = entries.filter(entry => this.terminals.get(entry.id) === entry && isProcessTreeAlive(entry));
|
|
275
|
+
for (const entry of survivors)
|
|
276
|
+
forceKillProcessTree(entry);
|
|
277
|
+
await this.reapStoppedEntries(survivors, this.forceKillGraceMs);
|
|
278
|
+
const orphans = survivors.filter(entry => this.terminals.get(entry.id) === entry && isProcessTreeAlive(entry));
|
|
279
|
+
if (orphans.length > 0) {
|
|
280
|
+
throw new errors_1.WebWorkbenchError(503, 'One or more terminal process trees survived Host shutdown.', 'terminal_shutdown_incomplete');
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
async reapStoppedEntries(entries, timeoutMs) {
|
|
284
|
+
const deadline = Date.now() + timeoutMs;
|
|
285
|
+
for (;;) {
|
|
286
|
+
let alive = false;
|
|
287
|
+
for (const entry of entries) {
|
|
288
|
+
if (this.terminals.get(entry.id) !== entry)
|
|
289
|
+
continue;
|
|
290
|
+
if (isProcessTreeAlive(entry)) {
|
|
291
|
+
alive = true;
|
|
292
|
+
continue;
|
|
293
|
+
}
|
|
294
|
+
this.removeEntry(entry);
|
|
295
|
+
}
|
|
296
|
+
if (!alive || Date.now() >= deadline)
|
|
297
|
+
return;
|
|
298
|
+
await delay(Math.min(PROCESS_REAP_POLL_MS, Math.max(1, deadline - Date.now())));
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
terminateEntry(entry) {
|
|
302
|
+
terminateProcessTree(entry);
|
|
303
|
+
if (entry.forceKillTimer)
|
|
304
|
+
clearTimeout(entry.forceKillTimer);
|
|
305
|
+
entry.forceKillTimer = setTimeout(() => {
|
|
306
|
+
entry.forceKillTimer = undefined;
|
|
307
|
+
if (this.terminals.get(entry.id) !== entry || !isProcessTreeAlive(entry)) {
|
|
308
|
+
if (entry.removeAfterExit && this.terminals.get(entry.id) === entry) {
|
|
309
|
+
this.removeEntry(entry);
|
|
310
|
+
}
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
forceKillProcessTree(entry);
|
|
314
|
+
this.scheduleReap(entry);
|
|
315
|
+
}, this.forceKillDelayMs);
|
|
316
|
+
entry.forceKillTimer.unref();
|
|
317
|
+
}
|
|
318
|
+
scheduleReap(entry) {
|
|
319
|
+
if (entry.reapTimer || this.terminals.get(entry.id) !== entry)
|
|
320
|
+
return;
|
|
321
|
+
const check = () => {
|
|
322
|
+
entry.reapTimer = undefined;
|
|
323
|
+
if (this.terminals.get(entry.id) !== entry)
|
|
324
|
+
return;
|
|
325
|
+
if (!isProcessTreeAlive(entry)) {
|
|
326
|
+
if (entry.removeAfterExit)
|
|
327
|
+
this.removeEntry(entry);
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
forceKillProcessTree(entry);
|
|
331
|
+
entry.reapTimer = setTimeout(check, PROCESS_REAP_POLL_MS);
|
|
332
|
+
entry.reapTimer.unref();
|
|
333
|
+
};
|
|
334
|
+
entry.reapTimer = setTimeout(check, PROCESS_REAP_POLL_MS);
|
|
335
|
+
entry.reapTimer.unref();
|
|
336
|
+
}
|
|
337
|
+
setProcessOutputPaused(entry, paused) {
|
|
338
|
+
if (entry.outputPaused === paused)
|
|
339
|
+
return;
|
|
340
|
+
try {
|
|
341
|
+
if (paused)
|
|
342
|
+
entry.process.pause();
|
|
343
|
+
else
|
|
344
|
+
entry.process.resume();
|
|
345
|
+
entry.outputPaused = paused;
|
|
346
|
+
}
|
|
347
|
+
catch {
|
|
348
|
+
throw new errors_1.WebWorkbenchError(503, 'Terminal output flow control failed.', 'terminal_flow_failed');
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
appendOutput(entry, data) {
|
|
352
|
+
if (entry.state !== 'running' && entry.state !== 'closing')
|
|
353
|
+
return;
|
|
354
|
+
entry.pendingOutput += data;
|
|
355
|
+
this.flushPendingOutput(entry, data.length >= MAX_OUTPUT_FRAME_CHARS);
|
|
356
|
+
if (entry.pendingOutput && !entry.outputFlushTimer) {
|
|
357
|
+
entry.outputFlushTimer = setTimeout(() => {
|
|
358
|
+
entry.outputFlushTimer = undefined;
|
|
359
|
+
this.flushPendingOutput(entry, true);
|
|
360
|
+
}, OUTPUT_COALESCE_MS);
|
|
361
|
+
entry.outputFlushTimer.unref();
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
flushPendingOutput(entry, force) {
|
|
365
|
+
let emitted = false;
|
|
366
|
+
while (entry.pendingOutput.length > 0 &&
|
|
367
|
+
(force || entry.pendingOutput.length >= MAX_OUTPUT_FRAME_CHARS)) {
|
|
368
|
+
let end = Math.min(entry.pendingOutput.length, MAX_OUTPUT_FRAME_CHARS);
|
|
369
|
+
if (end < entry.pendingOutput.length &&
|
|
370
|
+
end > 0 &&
|
|
371
|
+
isHighSurrogate(entry.pendingOutput.charCodeAt(end - 1)) &&
|
|
372
|
+
isLowSurrogate(entry.pendingOutput.charCodeAt(end))) {
|
|
373
|
+
end -= 1;
|
|
374
|
+
}
|
|
375
|
+
if (!force &&
|
|
376
|
+
end === entry.pendingOutput.length &&
|
|
377
|
+
isHighSurrogate(entry.pendingOutput.charCodeAt(end - 1))) {
|
|
378
|
+
break;
|
|
379
|
+
}
|
|
380
|
+
if (end <= 0)
|
|
381
|
+
break;
|
|
382
|
+
const value = entry.pendingOutput.slice(0, end);
|
|
383
|
+
entry.pendingOutput = entry.pendingOutput.slice(end);
|
|
384
|
+
const frame = Object.freeze({
|
|
385
|
+
sequence: entry.nextSequence++,
|
|
386
|
+
data: value,
|
|
387
|
+
bytes: Buffer.byteLength(value, 'utf8'),
|
|
388
|
+
});
|
|
389
|
+
entry.frames.push(frame);
|
|
390
|
+
entry.frameBytes += frame.bytes;
|
|
391
|
+
while (entry.frameBytes > MAX_SCROLLBACK_BYTES && entry.frames.length > 0) {
|
|
392
|
+
const removed = entry.frames.shift();
|
|
393
|
+
entry.frameBytes -= removed?.bytes ?? 0;
|
|
394
|
+
}
|
|
395
|
+
entry.subscriber?.onFrame(Object.freeze({ type: 'output', sequence: frame.sequence, data: frame.data }));
|
|
396
|
+
emitted = true;
|
|
397
|
+
}
|
|
398
|
+
if (!entry.pendingOutput && entry.outputFlushTimer) {
|
|
399
|
+
clearTimeout(entry.outputFlushTimer);
|
|
400
|
+
entry.outputFlushTimer = undefined;
|
|
401
|
+
}
|
|
402
|
+
if (emitted)
|
|
403
|
+
this.touch(entry);
|
|
404
|
+
}
|
|
405
|
+
handleExit(entry, event) {
|
|
406
|
+
if (this.terminals.get(entry.id) !== entry)
|
|
407
|
+
return;
|
|
408
|
+
this.flushPendingOutput(entry, true);
|
|
409
|
+
entry.state = 'exited';
|
|
410
|
+
entry.exitCode = event.exitCode;
|
|
411
|
+
entry.signal = event.signal;
|
|
412
|
+
entry.outputPaused = false;
|
|
413
|
+
if (entry.idleTimer)
|
|
414
|
+
clearTimeout(entry.idleTimer);
|
|
415
|
+
if (entry.forceKillTimer)
|
|
416
|
+
clearTimeout(entry.forceKillTimer);
|
|
417
|
+
entry.subscriber?.onExit(Object.freeze({
|
|
418
|
+
type: 'exit',
|
|
419
|
+
exitCode: event.exitCode,
|
|
420
|
+
...(event.signal !== undefined ? { signal: event.signal } : {}),
|
|
421
|
+
}));
|
|
422
|
+
entry.subscriber = undefined;
|
|
423
|
+
this.onWorkspaceMutationHint?.(entry.workspaceId);
|
|
424
|
+
if (isProcessTreeAlive(entry)) {
|
|
425
|
+
forceKillProcessTree(entry);
|
|
426
|
+
this.scheduleReap(entry);
|
|
427
|
+
}
|
|
428
|
+
else if (entry.removeAfterExit) {
|
|
429
|
+
this.removeEntry(entry);
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
issueTicket(terminalId) {
|
|
433
|
+
for (const [key, record] of this.tickets) {
|
|
434
|
+
if (record.terminalId === terminalId)
|
|
435
|
+
this.tickets.delete(key);
|
|
436
|
+
}
|
|
437
|
+
const value = (0, crypto_1.randomBytes)(32).toString('base64url');
|
|
438
|
+
const digest = ticketDigest(value);
|
|
439
|
+
const expiresAt = this.now() + this.ticketTtlMs;
|
|
440
|
+
this.tickets.set(terminalId, { terminalId, digest, expiresAt });
|
|
441
|
+
return Object.freeze({ value, expiresAt });
|
|
442
|
+
}
|
|
443
|
+
consumeTicket(terminalId, value) {
|
|
444
|
+
const record = this.tickets.get(terminalId);
|
|
445
|
+
const digest = ticketDigest(typeof value === 'string' ? value : '');
|
|
446
|
+
if (!record ||
|
|
447
|
+
record.expiresAt < this.now() ||
|
|
448
|
+
record.digest.length !== digest.length ||
|
|
449
|
+
!(0, crypto_1.timingSafeEqual)(record.digest, digest)) {
|
|
450
|
+
throw new errors_1.WebWorkbenchError(403, 'Terminal stream ticket is invalid.', 'terminal_ticket_invalid');
|
|
451
|
+
}
|
|
452
|
+
this.tickets.delete(terminalId);
|
|
453
|
+
}
|
|
454
|
+
metadata(entry) {
|
|
455
|
+
return Object.freeze({
|
|
456
|
+
id: entry.id,
|
|
457
|
+
workspaceId: entry.workspaceId,
|
|
458
|
+
title: entry.title,
|
|
459
|
+
shell: (0, path_1.basename)(entry.shell),
|
|
460
|
+
state: entry.state,
|
|
461
|
+
cols: entry.cols,
|
|
462
|
+
rows: entry.rows,
|
|
463
|
+
connected: Boolean(entry.subscriber),
|
|
464
|
+
createdAt: new Date(entry.createdAtMs).toISOString(),
|
|
465
|
+
lastActiveAt: new Date(entry.lastActiveAtMs).toISOString(),
|
|
466
|
+
earliestSequence: entry.frames[0]?.sequence ?? entry.nextSequence,
|
|
467
|
+
lastSequence: entry.nextSequence - 1,
|
|
468
|
+
...(entry.exitCode !== undefined ? { exitCode: entry.exitCode } : {}),
|
|
469
|
+
...(entry.signal !== undefined ? { signal: entry.signal } : {}),
|
|
470
|
+
});
|
|
471
|
+
}
|
|
472
|
+
assertConnection(entry, token) {
|
|
473
|
+
if (entry.subscriber?.token !== token) {
|
|
474
|
+
throw new errors_1.WebWorkbenchError(409, 'Terminal connection was replaced.', 'terminal_replaced');
|
|
475
|
+
}
|
|
476
|
+
if (entry.state !== 'running') {
|
|
477
|
+
throw new errors_1.WebWorkbenchError(409, 'Terminal is no longer running.', 'terminal_not_running');
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
assertActiveContext(workspaceId, contextRevision) {
|
|
481
|
+
this.assertOpen();
|
|
482
|
+
const active = this.getActiveContext();
|
|
483
|
+
if (active.workspaceId !== workspaceId || active.contextRevision !== contextRevision) {
|
|
484
|
+
throw new errors_1.WebWorkbenchError(409, 'The active Context changed before terminal mutation.', 'context_revision_conflict');
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
requireTerminal(id) {
|
|
488
|
+
const entry = this.terminals.get(id);
|
|
489
|
+
if (!entry)
|
|
490
|
+
throw new errors_1.WebWorkbenchError(404, 'Terminal was not found.', 'terminal_not_found');
|
|
491
|
+
return entry;
|
|
492
|
+
}
|
|
493
|
+
touch(entry) {
|
|
494
|
+
entry.lastActiveAtMs = this.now();
|
|
495
|
+
this.scheduleIdle(entry);
|
|
496
|
+
}
|
|
497
|
+
scheduleIdle(entry) {
|
|
498
|
+
if (entry.idleTimer)
|
|
499
|
+
clearTimeout(entry.idleTimer);
|
|
500
|
+
entry.idleTimer = setTimeout(() => {
|
|
501
|
+
if (entry.state !== 'running')
|
|
502
|
+
return;
|
|
503
|
+
if (this.now() - entry.lastActiveAtMs < this.idleTimeoutMs) {
|
|
504
|
+
this.scheduleIdle(entry);
|
|
505
|
+
return;
|
|
506
|
+
}
|
|
507
|
+
entry.state = 'closing';
|
|
508
|
+
this.terminateEntry(entry);
|
|
509
|
+
}, this.idleTimeoutMs);
|
|
510
|
+
entry.idleTimer.unref();
|
|
511
|
+
}
|
|
512
|
+
removeEntry(entry) {
|
|
513
|
+
if (entry.idleTimer)
|
|
514
|
+
clearTimeout(entry.idleTimer);
|
|
515
|
+
if (entry.forceKillTimer)
|
|
516
|
+
clearTimeout(entry.forceKillTimer);
|
|
517
|
+
if (entry.reapTimer)
|
|
518
|
+
clearTimeout(entry.reapTimer);
|
|
519
|
+
if (entry.outputFlushTimer)
|
|
520
|
+
clearTimeout(entry.outputFlushTimer);
|
|
521
|
+
entry.dataSubscription.dispose();
|
|
522
|
+
entry.exitSubscription.dispose();
|
|
523
|
+
entry.subscriber = undefined;
|
|
524
|
+
this.terminals.delete(entry.id);
|
|
525
|
+
this.tickets.delete(entry.id);
|
|
526
|
+
}
|
|
527
|
+
assertOpen() {
|
|
528
|
+
if (this.closed) {
|
|
529
|
+
throw new errors_1.WebWorkbenchError(503, 'Terminal manager is closed.', 'terminal_unavailable');
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
exports.TerminalManagerV1 = TerminalManagerV1;
|
|
534
|
+
function loadNodePtyBackend() {
|
|
535
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
536
|
+
const nodePty = require('node-pty');
|
|
537
|
+
return Object.freeze({
|
|
538
|
+
spawn: (file, args, options) => nodePty.spawn(file, [...args], { ...options, env: { ...options.env } }),
|
|
539
|
+
});
|
|
540
|
+
}
|
|
541
|
+
function resolveShell() {
|
|
542
|
+
const configured = process.env.SHELL;
|
|
543
|
+
const candidates = [
|
|
544
|
+
configured,
|
|
545
|
+
process.platform === 'win32' ? process.env.COMSPEC : '/bin/zsh',
|
|
546
|
+
'/bin/bash',
|
|
547
|
+
];
|
|
548
|
+
for (const candidate of candidates) {
|
|
549
|
+
if (!candidate || !(0, path_1.isAbsolute)(candidate) || !(0, fs_1.existsSync)(candidate))
|
|
550
|
+
continue;
|
|
551
|
+
try {
|
|
552
|
+
const canonical = (0, fs_1.realpathSync)(candidate);
|
|
553
|
+
const stat = (0, fs_1.statSync)(canonical);
|
|
554
|
+
if (stat.isFile() && (stat.mode & 0o111) !== 0)
|
|
555
|
+
return canonical;
|
|
556
|
+
}
|
|
557
|
+
catch {
|
|
558
|
+
// Try the next bounded shell candidate.
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
throw new errors_1.WebWorkbenchError(503, 'No interactive shell is available.', 'terminal_shell_unavailable');
|
|
562
|
+
}
|
|
563
|
+
function shellArguments(shell) {
|
|
564
|
+
if (process.platform === 'win32')
|
|
565
|
+
return [];
|
|
566
|
+
return (0, path_1.basename)(shell) === 'zsh' ? ['-f'] : ['--noprofile', '--norc'];
|
|
567
|
+
}
|
|
568
|
+
function terminalEnvironment(shell) {
|
|
569
|
+
const env = {
|
|
570
|
+
TERM: 'xterm-256color',
|
|
571
|
+
COLORTERM: 'truecolor',
|
|
572
|
+
SHELL: shell,
|
|
573
|
+
};
|
|
574
|
+
for (const key of ['PATH', 'HOME', 'USER', 'LOGNAME', 'LANG', 'LC_ALL', 'LC_CTYPE', 'TMPDIR']) {
|
|
575
|
+
const value = process.env[key];
|
|
576
|
+
if (value)
|
|
577
|
+
env[key] = value;
|
|
578
|
+
}
|
|
579
|
+
return env;
|
|
580
|
+
}
|
|
581
|
+
function canonicalDirectory(path) {
|
|
582
|
+
try {
|
|
583
|
+
const canonical = (0, fs_1.realpathSync)((0, path_1.resolve)(path));
|
|
584
|
+
if (!(0, fs_1.statSync)(canonical).isDirectory())
|
|
585
|
+
throw new Error('not a directory');
|
|
586
|
+
return canonical;
|
|
587
|
+
}
|
|
588
|
+
catch {
|
|
589
|
+
throw new errors_1.WebWorkbenchError(400, 'Workspace directory is unavailable.');
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
function ticketDigest(ticket) {
|
|
593
|
+
return (0, crypto_1.createHash)('sha256').update(ticket).digest();
|
|
594
|
+
}
|
|
595
|
+
function boundedPositive(value, name) {
|
|
596
|
+
if (!Number.isSafeInteger(value) || value < 1) {
|
|
597
|
+
throw new Error(`${name} must be a positive integer`);
|
|
598
|
+
}
|
|
599
|
+
return value;
|
|
600
|
+
}
|
|
601
|
+
function boundedDimension(value, min, max, name) {
|
|
602
|
+
if (!Number.isSafeInteger(value) || value < min || value > max) {
|
|
603
|
+
throw new errors_1.WebWorkbenchError(400, `${name} must be an integer from ${min} through ${max}.`);
|
|
604
|
+
}
|
|
605
|
+
return value;
|
|
606
|
+
}
|
|
607
|
+
function isHighSurrogate(value) {
|
|
608
|
+
return value >= 0xd800 && value <= 0xdbff;
|
|
609
|
+
}
|
|
610
|
+
function isLowSurrogate(value) {
|
|
611
|
+
return value >= 0xdc00 && value <= 0xdfff;
|
|
612
|
+
}
|
|
613
|
+
function terminateProcessTree(entry) {
|
|
614
|
+
signalProcessTree(entry, 'SIGTERM');
|
|
615
|
+
}
|
|
616
|
+
function forceKillProcessTree(entry) {
|
|
617
|
+
signalProcessTree(entry, 'SIGKILL');
|
|
618
|
+
}
|
|
619
|
+
function signalProcessTree(entry, signal) {
|
|
620
|
+
if (process.platform === 'win32') {
|
|
621
|
+
try {
|
|
622
|
+
entry.process.kill(signal);
|
|
623
|
+
}
|
|
624
|
+
catch {
|
|
625
|
+
// The process may already have exited.
|
|
626
|
+
}
|
|
627
|
+
return;
|
|
628
|
+
}
|
|
629
|
+
const rows = rememberProcessTree(entry);
|
|
630
|
+
let groupSignalled = false;
|
|
631
|
+
const rootIdentity = entry.ownedProcesses.get(entry.process.pid);
|
|
632
|
+
if (rootIdentity && rows.get(entry.process.pid)?.identity === rootIdentity) {
|
|
633
|
+
try {
|
|
634
|
+
process.kill(-entry.process.pid, signal);
|
|
635
|
+
groupSignalled = true;
|
|
636
|
+
}
|
|
637
|
+
catch {
|
|
638
|
+
// A PTY child may not remain the process-group leader after shell startup.
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
const owned = [...entry.ownedProcesses.entries()]
|
|
642
|
+
.filter(([pid, identity]) => {
|
|
643
|
+
const row = rows.get(pid);
|
|
644
|
+
return row?.identity === identity && !isZombie(row);
|
|
645
|
+
})
|
|
646
|
+
.sort(([leftPid], [rightPid]) => {
|
|
647
|
+
if (leftPid === entry.process.pid)
|
|
648
|
+
return 1;
|
|
649
|
+
if (rightPid === entry.process.pid)
|
|
650
|
+
return -1;
|
|
651
|
+
return rightPid - leftPid;
|
|
652
|
+
});
|
|
653
|
+
for (const [pid] of owned) {
|
|
654
|
+
try {
|
|
655
|
+
process.kill(pid, signal);
|
|
656
|
+
}
|
|
657
|
+
catch {
|
|
658
|
+
// The process exited after the bounded ownership snapshot.
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
if (!groupSignalled && owned.length === 0) {
|
|
662
|
+
try {
|
|
663
|
+
entry.process.kill(signal);
|
|
664
|
+
}
|
|
665
|
+
catch {
|
|
666
|
+
// The process is already gone.
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
function isProcessTreeAlive(entry) {
|
|
671
|
+
if (entry.process.isAlive) {
|
|
672
|
+
try {
|
|
673
|
+
return entry.process.isAlive();
|
|
674
|
+
}
|
|
675
|
+
catch {
|
|
676
|
+
return true;
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
if (process.platform === 'win32') {
|
|
680
|
+
try {
|
|
681
|
+
process.kill(entry.process.pid, 0);
|
|
682
|
+
return true;
|
|
683
|
+
}
|
|
684
|
+
catch (error) {
|
|
685
|
+
return !isMissingProcessError(error);
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
const rows = rememberProcessTree(entry);
|
|
689
|
+
for (const [pid, identity] of entry.ownedProcesses) {
|
|
690
|
+
const row = rows.get(pid);
|
|
691
|
+
if (row?.identity === identity && !isZombie(row))
|
|
692
|
+
return true;
|
|
693
|
+
}
|
|
694
|
+
return false;
|
|
695
|
+
}
|
|
696
|
+
function rememberProcessTree(entry) {
|
|
697
|
+
const rows = readProcessRows();
|
|
698
|
+
if (rows.size === 0)
|
|
699
|
+
return rows;
|
|
700
|
+
const childrenByParent = new Map();
|
|
701
|
+
for (const row of rows.values()) {
|
|
702
|
+
const children = childrenByParent.get(row.parentPid) ?? [];
|
|
703
|
+
children.push(row);
|
|
704
|
+
childrenByParent.set(row.parentPid, children);
|
|
705
|
+
}
|
|
706
|
+
const pending = [entry.process.pid, ...entry.ownedProcesses.keys()];
|
|
707
|
+
const visited = new Set();
|
|
708
|
+
while (pending.length > 0) {
|
|
709
|
+
const pid = pending.pop();
|
|
710
|
+
if (visited.has(pid))
|
|
711
|
+
continue;
|
|
712
|
+
visited.add(pid);
|
|
713
|
+
const row = rows.get(pid);
|
|
714
|
+
const existingIdentity = entry.ownedProcesses.get(pid);
|
|
715
|
+
if (existingIdentity && row?.identity !== existingIdentity)
|
|
716
|
+
continue;
|
|
717
|
+
if (row && !existingIdentity)
|
|
718
|
+
entry.ownedProcesses.set(pid, row.identity);
|
|
719
|
+
for (const child of childrenByParent.get(pid) ?? [])
|
|
720
|
+
pending.push(child.pid);
|
|
721
|
+
}
|
|
722
|
+
return rows;
|
|
723
|
+
}
|
|
724
|
+
function readProcessRows() {
|
|
725
|
+
if (process.platform === 'win32')
|
|
726
|
+
return new Map();
|
|
727
|
+
try {
|
|
728
|
+
const output = (0, child_process_1.execFileSync)('/bin/ps', ['-axo', 'pid=,ppid=,stat=,lstart='], {
|
|
729
|
+
encoding: 'utf8',
|
|
730
|
+
timeout: 1000,
|
|
731
|
+
maxBuffer: 1024 * 1024,
|
|
732
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
733
|
+
});
|
|
734
|
+
const rows = new Map();
|
|
735
|
+
for (const line of output.split('\n')) {
|
|
736
|
+
const match = line.match(/^\s*(\d+)\s+(\d+)\s+(\S+)\s+(.+?)\s*$/u);
|
|
737
|
+
if (!match)
|
|
738
|
+
continue;
|
|
739
|
+
const pid = Number(match[1]);
|
|
740
|
+
const parentPid = Number(match[2]);
|
|
741
|
+
if (!Number.isSafeInteger(pid) || !Number.isSafeInteger(parentPid))
|
|
742
|
+
continue;
|
|
743
|
+
rows.set(pid, Object.freeze({ pid, parentPid, state: match[3], identity: match[4] }));
|
|
744
|
+
}
|
|
745
|
+
return rows;
|
|
746
|
+
}
|
|
747
|
+
catch {
|
|
748
|
+
return new Map();
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
function isZombie(row) {
|
|
752
|
+
return row.state.startsWith('Z');
|
|
753
|
+
}
|
|
754
|
+
function isMissingProcessError(error) {
|
|
755
|
+
return error?.code === 'ESRCH';
|
|
756
|
+
}
|
|
757
|
+
function delay(milliseconds) {
|
|
758
|
+
return new Promise(resolvePromise => setTimeout(resolvePromise, milliseconds));
|
|
759
|
+
}
|
|
760
|
+
//# sourceMappingURL=terminal-manager.js.map
|