@exagone313/dsh-podman 0.2.0-rc.4 → 0.2.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 +12 -0
- package/README.zh.md +14 -2
- package/cordis.patch.yml +58 -0
- package/dist/approval-reasons.d.ts +3 -4
- package/dist/approval-reasons.js +40 -49
- package/dist/approval-reasons.test.js +43 -15
- package/dist/approval.js +27 -10
- package/dist/approval.test.js +165 -34
- package/dist/card-route.d.ts +3 -2
- package/dist/card-route.js +103 -35
- package/dist/card-route.test.js +60 -43
- package/dist/card-test-support.js +39 -66
- package/dist/client/ContainerCard.d.ts +1 -1
- package/dist/client/ContainerCard.js +76 -70
- package/dist/client/card-protocol.d.ts +3 -3
- package/dist/client/container-card-caches.js +11 -6
- package/dist/client/container-card-controller.d.ts +13 -18
- package/dist/client/container-card-controller.js +86 -40
- package/dist/client/container-card-create-modal.d.ts +2 -2
- package/dist/client/container-card-create-modal.js +13 -5
- package/dist/client/container-card-default-env.d.ts +10 -0
- package/dist/client/container-card-default-env.js +69 -0
- package/dist/client/container-card-directory.d.ts +2 -2
- package/dist/client/container-card-directory.js +12 -4
- package/dist/client/container-card-editors.d.ts +3 -3
- package/dist/client/container-card-editors.js +104 -32
- package/dist/client/container-card-images.d.ts +4 -4
- package/dist/client/container-card-images.js +19 -6
- package/dist/client/container-card-paths.d.ts +3 -3
- package/dist/client/container-card-paths.js +18 -11
- package/dist/client/container-card-row.d.ts +10 -6
- package/dist/client/container-card-row.js +82 -13
- package/dist/client/container-card-secrets.d.ts +3 -3
- package/dist/client/container-card-secrets.js +8 -6
- package/dist/client/container-card-shared.d.ts +4 -17
- package/dist/client/container-card-shared.js +9 -25
- package/dist/client/container-card-styles.d.ts +2 -8
- package/dist/client/container-card-styles.js +14 -52
- package/dist/client/container-card-volumes.d.ts +2 -2
- package/dist/client/container-card-volumes.js +9 -5
- package/dist/client/container-card-workspace.d.ts +8 -6
- package/dist/client/container-card-workspace.js +9 -3
- package/dist/client/directory-picker.js +2 -1
- package/dist/client/index.js +13109 -2117
- package/dist/client/locales.d.ts +6 -1
- package/dist/client/locales.js +88 -38
- package/dist/client/podman-terminal-guide.d.ts +11 -0
- package/dist/client/podman-terminal-guide.js +208 -0
- package/dist/client/podman-terminal-title.d.ts +7 -0
- package/dist/client/podman-terminal-title.js +30 -0
- package/dist/client/podman-terminal.d.ts +26 -0
- package/dist/client/podman-terminal.js +412 -0
- package/dist/client/read-only-approval.d.ts +2 -2
- package/dist/client/read-only-approval.js +13 -3
- package/dist/client/slot-contract.d.ts +0 -11
- package/dist/client/terminal-preference.d.ts +26 -0
- package/dist/client/terminal-preference.js +67 -0
- package/dist/client/terminal-protocol.d.ts +85 -0
- package/dist/client/terminal-protocol.js +14 -0
- package/dist/client/terminal-tab.d.ts +60 -0
- package/dist/client/terminal-tab.js +35 -0
- package/dist/client/terminal-targets.d.ts +33 -0
- package/dist/client/terminal-targets.js +65 -0
- package/dist/client/terminal-titles.d.ts +23 -0
- package/dist/client/terminal-titles.js +52 -0
- package/dist/client/terminal-transport.d.ts +63 -0
- package/dist/client/terminal-transport.js +207 -0
- package/dist/client/tool-views.js +215 -49
- package/dist/container-env.d.ts +11 -0
- package/dist/container-env.js +96 -0
- package/dist/container-env.test.d.ts +1 -0
- package/dist/container-env.test.js +93 -0
- package/dist/containers.test.js +196 -18
- package/dist/daemons.test.js +18 -4
- package/dist/dsh-version.js +2 -1
- package/dist/env-rows.d.ts +14 -0
- package/dist/env-rows.js +52 -0
- package/dist/env-rows.test.d.ts +1 -0
- package/dist/env-rows.test.js +43 -0
- package/dist/fs-provider.d.ts +1 -0
- package/dist/fs-provider.js +26 -12
- package/dist/fs-provider.test.js +51 -10
- package/dist/generated/version.d.ts +2 -2
- package/dist/generated/version.js +2 -2
- package/dist/grpc/proto/dshctl/v1/control.proto +8 -1
- package/dist/grpc/proto/dshguest/v1/guest.proto +5 -0
- package/dist/grpc/runtime-client.d.ts +3 -1
- package/dist/grpc/runtime-client.js +90 -17
- package/dist/guest-rpc.d.ts +15 -19
- package/dist/guest-rpc.js +289 -70
- package/dist/guest-rpc.test.d.ts +1 -0
- package/dist/guest-rpc.test.js +241 -0
- package/dist/guest-terminal.d.ts +30 -0
- package/dist/guest-terminal.js +196 -0
- package/dist/images.test.js +10 -3
- package/dist/index.d.ts +11 -16
- package/dist/index.js +42 -36
- package/dist/locales.test.d.ts +1 -0
- package/dist/locales.test.js +34 -0
- package/dist/misc.test.js +38 -10
- package/dist/mount-enums.js +2 -1
- package/dist/mount-input.d.ts +2 -0
- package/dist/mount-input.js +38 -22
- package/dist/mounts.test.js +81 -11
- package/dist/naming.test.d.ts +1 -0
- package/dist/naming.test.js +28 -0
- package/dist/output-reader.js +36 -4
- package/dist/package-deps.test.d.ts +1 -0
- package/dist/package-deps.test.js +49 -0
- package/dist/paths.test.js +4 -2
- package/dist/plugin-meta.test.d.ts +1 -0
- package/dist/plugin-meta.test.js +29 -0
- package/dist/project-path.js +33 -7
- package/dist/project-path.test.js +14 -0
- package/dist/prompts.d.ts +0 -4
- package/dist/prompts.js +15 -84
- package/dist/read-only-shell.js +3 -1
- package/dist/read-only-shell.test.js +80 -25
- package/dist/runtime-client.test.d.ts +1 -0
- package/dist/runtime-client.test.js +53 -0
- package/dist/secrets.test.js +21 -6
- package/dist/settings-commands.test.js +61 -14
- package/dist/settings-create.test.js +32 -10
- package/dist/settings-default-env.test.d.ts +1 -0
- package/dist/settings-default-env.test.js +175 -0
- package/dist/settings-mounts.test.js +46 -7
- package/dist/settings-schema.d.ts +6 -11
- package/dist/settings-schema.js +4 -8
- package/dist/settings-schema.test.d.ts +1 -0
- package/dist/settings-schema.test.js +35 -0
- package/dist/settings-workspaces.test.js +36 -63
- package/dist/spill-store.d.ts +1 -0
- package/dist/spill-store.js +17 -4
- package/dist/spill-store.test.js +31 -8
- package/dist/subprocess.d.ts +4 -0
- package/dist/subprocess.js +148 -162
- package/dist/subprocess.test.js +118 -8
- package/dist/terminal-preference.test.d.ts +1 -0
- package/dist/terminal-preference.test.js +62 -0
- package/dist/terminal-route.d.ts +10 -0
- package/dist/terminal-route.js +270 -0
- package/dist/terminal-route.test.d.ts +1 -0
- package/dist/terminal-route.test.js +265 -0
- package/dist/terminal-sessions.d.ts +69 -0
- package/dist/terminal-sessions.js +252 -0
- package/dist/terminal-shells.d.ts +22 -0
- package/dist/terminal-shells.js +99 -0
- package/dist/terminal-tab.test.d.ts +1 -0
- package/dist/terminal-tab.test.js +58 -0
- package/dist/terminal-targets.test.d.ts +1 -0
- package/dist/terminal-targets.test.js +43 -0
- package/dist/terminal-titles.test.d.ts +1 -0
- package/dist/terminal-titles.test.js +36 -0
- package/dist/test-support.d.ts +35 -5
- package/dist/test-support.js +91 -29
- package/dist/tool-defs.js +19 -4
- package/dist/tool-handlers.js +80 -28
- package/dist/tool-params.js +19 -5
- package/dist/volumes.test.js +1 -1
- package/dist/workspace-binding.d.ts +18 -9
- package/dist/workspace-binding.js +244 -77
- package/dist/workspace-binding.test.js +189 -25
- package/docs/architecture.md +62 -21
- package/docs/architecture.zh.md +57 -13
- package/docs/configuration.md +61 -20
- package/docs/configuration.zh.md +53 -19
- package/docs/development-prompt.md +72 -0
- package/docs/development-prompt.zh.md +69 -0
- package/docs/development.md +141 -21
- package/docs/development.zh.md +125 -17
- package/docs/install-dsh-and-dsh-podman.md +21 -8
- package/docs/install-dsh-and-dsh-podman.zh.md +18 -8
- package/docs/uninstall.md +83 -0
- package/docs/uninstall.zh.md +79 -0
- package/docs/update.md +120 -0
- package/docs/update.zh.md +114 -0
- package/docs/usage.md +108 -61
- package/docs/usage.zh.md +76 -38
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +58 -25
- package/quadlet/dsh-podman-orchestrator.container +46 -0
- package/quadlet/dsh.container +35 -0
- package/LICENSE.pkg +0 -17103
- package/dist/preferences.d.ts +0 -6
- package/dist/preferences.js +0 -27
package/dist/guest-rpc.js
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
// SPDX-FileCopyrightText: 2026 Elouan Martinet <exa@elou.world>
|
|
2
2
|
//
|
|
3
3
|
// SPDX-License-Identifier: MIT
|
|
4
|
-
import { metadata, workspaceSlug } from "./workspace-binding.js";
|
|
4
|
+
import { metadata, workspaceSlug, } from "./workspace-binding.js";
|
|
5
|
+
import { grpc } from "./grpc/runtime-client.js";
|
|
6
|
+
import { discardUnneededSpill, outputReader, SPILL_ROOT, } from "./output-reader.js";
|
|
7
|
+
import { randomUUID } from "node:crypto";
|
|
5
8
|
import { isAbsolute, resolve as resolvePath } from "node:path";
|
|
6
9
|
// Resolve a container-side path against the session working directory.
|
|
7
10
|
// Absolute paths pass through unchanged. The session cwd and the container's
|
|
@@ -10,7 +13,9 @@ import { isAbsolute, resolve as resolvePath } from "node:path";
|
|
|
10
13
|
function resolveAgainstSession(path, sessionCwd) {
|
|
11
14
|
if (isAbsolute(path))
|
|
12
15
|
return path;
|
|
13
|
-
const base = typeof sessionCwd === "string" && sessionCwd !== ""
|
|
16
|
+
const base = typeof sessionCwd === "string" && sessionCwd !== ""
|
|
17
|
+
? sessionCwd
|
|
18
|
+
: undefined;
|
|
14
19
|
if (base === undefined) {
|
|
15
20
|
throw new Error("relative paths need a session working directory; pass an absolute path");
|
|
16
21
|
}
|
|
@@ -110,6 +115,26 @@ function onAbortCancel(call, signal) {
|
|
|
110
115
|
signal.addEventListener("abort", cancel, { once: true });
|
|
111
116
|
return () => signal.removeEventListener("abort", cancel);
|
|
112
117
|
}
|
|
118
|
+
// isUnauthenticated reports whether a guest call failed because the agent
|
|
119
|
+
// rejected the caller's credential.
|
|
120
|
+
function isUnauthenticated(error) {
|
|
121
|
+
return error?.code === grpc.status.UNAUTHENTICATED;
|
|
122
|
+
}
|
|
123
|
+
// withGuestAuth runs a guest call and, when the agent rejects the credential,
|
|
124
|
+
// refreshes the binding once and retries. A rejection happens in the agent's
|
|
125
|
+
// auth interceptor, before the handler runs, so the retry cannot duplicate a
|
|
126
|
+
// side effect; a second rejection propagates.
|
|
127
|
+
export async function withGuestAuth(binding, run) {
|
|
128
|
+
try {
|
|
129
|
+
return await run(binding.guest, binding.token);
|
|
130
|
+
}
|
|
131
|
+
catch (error) {
|
|
132
|
+
if (!isUnauthenticated(error) || binding.refresh === undefined)
|
|
133
|
+
throw error;
|
|
134
|
+
const refreshed = await binding.refresh();
|
|
135
|
+
return await run(refreshed.guest, refreshed.token);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
113
138
|
// Stream raw bytes from the guest's ReadFile. A zero/absent length reads to
|
|
114
139
|
// EOF. Cancelling the signal cancels the call and reports FS_ABORTED.
|
|
115
140
|
export async function* guestChunks(target, range, signal) {
|
|
@@ -121,20 +146,48 @@ export async function* guestChunks(target, range, signal) {
|
|
|
121
146
|
if (range?.length !== undefined && range.length > 0) {
|
|
122
147
|
request.length = range.length;
|
|
123
148
|
}
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
149
|
+
// The read is retried once with a refreshed binding when the agent rejects
|
|
150
|
+
// the credential, but only before any byte was emitted, so a retry can never
|
|
151
|
+
// duplicate output.
|
|
152
|
+
let binding = target.binding;
|
|
153
|
+
for (let attempt = 0;; attempt++) {
|
|
154
|
+
const call = binding.guest.readFile(request, metadata(binding.token));
|
|
155
|
+
const detach = onAbortCancel(call, signal);
|
|
156
|
+
let emitted = false;
|
|
157
|
+
let completed = false;
|
|
158
|
+
try {
|
|
159
|
+
for await (const chunk of call) {
|
|
160
|
+
emitted = true;
|
|
161
|
+
yield Buffer.from(chunk.data);
|
|
162
|
+
}
|
|
163
|
+
completed = true;
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
catch (error) {
|
|
167
|
+
if (signal?.aborted)
|
|
168
|
+
throw fsError("FS_ABORTED", "read aborted");
|
|
169
|
+
if (attempt === 0 &&
|
|
170
|
+
!emitted &&
|
|
171
|
+
isUnauthenticated(error) &&
|
|
172
|
+
binding.refresh !== undefined) {
|
|
173
|
+
binding = await binding.refresh();
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
throw error;
|
|
177
|
+
}
|
|
178
|
+
finally {
|
|
179
|
+
detach();
|
|
180
|
+
if (!completed) {
|
|
181
|
+
// A consumer that stopped early (a bounded line read) or an error path
|
|
182
|
+
// must not leave the guest reading a file nobody consumes.
|
|
183
|
+
try {
|
|
184
|
+
call.cancel?.();
|
|
185
|
+
}
|
|
186
|
+
catch {
|
|
187
|
+
// The call already finished.
|
|
188
|
+
}
|
|
189
|
+
}
|
|
129
190
|
}
|
|
130
|
-
}
|
|
131
|
-
catch (error) {
|
|
132
|
-
if (signal?.aborted)
|
|
133
|
-
throw fsError("FS_ABORTED", "read aborted");
|
|
134
|
-
throw error;
|
|
135
|
-
}
|
|
136
|
-
finally {
|
|
137
|
-
detach();
|
|
138
191
|
}
|
|
139
192
|
}
|
|
140
193
|
// Stream the guest file as decoded UTF-8 text, rejecting binary content (a NUL
|
|
@@ -191,12 +244,14 @@ export function guestVersion(result) {
|
|
|
191
244
|
}
|
|
192
245
|
export async function writeGuestFile(binding, path, content, opts, signal) {
|
|
193
246
|
throwIfAborted(signal, "write");
|
|
194
|
-
return new Promise((resolveDone, reject) => {
|
|
247
|
+
return withGuestAuth(binding, (guest, token) => new Promise((resolveDone, reject) => {
|
|
195
248
|
let detach = () => { };
|
|
196
|
-
const call =
|
|
249
|
+
const call = guest.writeFile(metadata(token), {}, (error, result) => {
|
|
197
250
|
detach();
|
|
198
251
|
error
|
|
199
|
-
? reject(signal?.aborted
|
|
252
|
+
? reject(signal?.aborted
|
|
253
|
+
? fsError("FS_ABORTED", "write aborted")
|
|
254
|
+
: error)
|
|
200
255
|
: resolveDone(Number(result?.bytesWritten ?? 0));
|
|
201
256
|
});
|
|
202
257
|
detach = onAbortCancel(call, signal);
|
|
@@ -205,78 +260,195 @@ export async function writeGuestFile(binding, path, content, opts, signal) {
|
|
|
205
260
|
});
|
|
206
261
|
call.write({ dataChunk: Buffer.from(content) });
|
|
207
262
|
call.end();
|
|
208
|
-
});
|
|
263
|
+
}));
|
|
209
264
|
}
|
|
210
265
|
export async function readGuestFile(binding, path) {
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
const call =
|
|
266
|
+
return withGuestAuth(binding, (guest, token) => new Promise((resolveDone, reject) => {
|
|
267
|
+
const chunks = [];
|
|
268
|
+
const call = guest.readFile({ path }, metadata(token));
|
|
214
269
|
call.on("data", (chunk) => chunks.push(Buffer.from(chunk.data)));
|
|
215
270
|
call.on("error", reject);
|
|
216
|
-
call.on("end", resolveDone);
|
|
217
|
-
});
|
|
218
|
-
return Buffer.concat(chunks).toString("utf8");
|
|
271
|
+
call.on("end", () => resolveDone(Buffer.concat(chunks).toString("utf8")));
|
|
272
|
+
}));
|
|
219
273
|
}
|
|
220
274
|
const READ_LIMIT = 2000;
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
275
|
+
// In-memory retention per stream for one command. The full stream is also
|
|
276
|
+
// written to a bounded guest spill file, so the plugin never buffers an
|
|
277
|
+
// unbounded command output and the caller can read the rest back with
|
|
278
|
+
// container_read.
|
|
279
|
+
const EXEC_RETAIN_BYTES = 1 << 20;
|
|
280
|
+
const EXEC_SPILL_BYTES = 8 << 20;
|
|
281
|
+
// The grace a cancelled command is given between SIGTERM and SIGKILL, matching
|
|
282
|
+
// the harness's own termination grace.
|
|
283
|
+
const EXEC_KILL_GRACE_MS = 5000;
|
|
284
|
+
export async function runExec(binding, argv, cwd, env, timeoutMs, identity, options = {}) {
|
|
285
|
+
const signal = options.signal;
|
|
286
|
+
throwIfAborted(signal, "exec");
|
|
287
|
+
const maxBytes = options.maxBytes ?? EXEC_RETAIN_BYTES;
|
|
288
|
+
const spillBytes = options.spillBytes ?? EXEC_SPILL_BYTES;
|
|
289
|
+
const configuredGrace = Number(options.killGraceMs);
|
|
290
|
+
const killGraceMs = Number.isFinite(configuredGrace) && configuredGrace > 0
|
|
291
|
+
? configuredGrace
|
|
292
|
+
: EXEC_KILL_GRACE_MS;
|
|
293
|
+
const stdoutSpec = {
|
|
294
|
+
path: `${SPILL_ROOT}/${randomUUID()}.stdout`,
|
|
295
|
+
maxBytes: spillBytes,
|
|
296
|
+
};
|
|
297
|
+
const stderrSpec = {
|
|
298
|
+
path: `${SPILL_ROOT}/${randomUUID()}.stderr`,
|
|
299
|
+
maxBytes: spillBytes,
|
|
300
|
+
};
|
|
301
|
+
return withGuestAuth(binding, (guest, token) => new Promise((resolveDone, reject) => {
|
|
302
|
+
const stdoutReader = outputReader({ maxBytes }, stdoutSpec);
|
|
303
|
+
const stderrReader = outputReader({ maxBytes }, stderrSpec);
|
|
304
|
+
if (stdoutReader === undefined || stderrReader === undefined) {
|
|
305
|
+
reject(new Error("exec output reader is unavailable"));
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
const stream = guest.exec(metadata(token));
|
|
226
309
|
let processId;
|
|
227
310
|
let timer;
|
|
228
|
-
|
|
311
|
+
let killTimer;
|
|
312
|
+
let settled = false;
|
|
313
|
+
let detach = () => { };
|
|
314
|
+
// settle runs one final action exactly once and tears down the timeout
|
|
315
|
+
// and the abort listener, so a timeout, an abort, and an exit cannot
|
|
316
|
+
// race. The kill timer deliberately outlives settle: it is the
|
|
317
|
+
// escalation for a command that ignored the cancellation's SIGTERM.
|
|
318
|
+
const settle = (finish) => {
|
|
319
|
+
if (settled)
|
|
320
|
+
return;
|
|
321
|
+
settled = true;
|
|
229
322
|
if (timer !== undefined) {
|
|
230
323
|
clearTimeout(timer);
|
|
231
324
|
timer = undefined;
|
|
232
325
|
}
|
|
326
|
+
detach();
|
|
327
|
+
finish();
|
|
328
|
+
};
|
|
329
|
+
const clearKillTimer = () => {
|
|
330
|
+
if (killTimer !== undefined) {
|
|
331
|
+
clearTimeout(killTimer);
|
|
332
|
+
killTimer = undefined;
|
|
333
|
+
}
|
|
334
|
+
};
|
|
335
|
+
const stopProcess = (sig) => {
|
|
336
|
+
if (processId === undefined)
|
|
337
|
+
return;
|
|
338
|
+
// Best effort: ask the guest to terminate the running process tree.
|
|
339
|
+
unaryGuest({ binding: { guest, token } }, "signal", {
|
|
340
|
+
processId,
|
|
341
|
+
signal: sig,
|
|
342
|
+
}).catch(() => { });
|
|
343
|
+
};
|
|
344
|
+
const cancelStream = () => {
|
|
345
|
+
try {
|
|
346
|
+
stream.cancel?.();
|
|
347
|
+
}
|
|
348
|
+
catch {
|
|
349
|
+
// The stream already ended; nothing to cancel.
|
|
350
|
+
}
|
|
351
|
+
};
|
|
352
|
+
// A cancellation asks the command to stop with SIGTERM, then kills its
|
|
353
|
+
// process group once the grace period elapses without an exit. The
|
|
354
|
+
// stream stays open for that grace so the guest still knows the process;
|
|
355
|
+
// the guest also escalates a cancelled stream to a group SIGKILL, so a
|
|
356
|
+
// lost connection stops the tree too.
|
|
357
|
+
const escalateStop = () => {
|
|
358
|
+
if (processId === undefined) {
|
|
359
|
+
// Nothing to signal yet: cancelling the stream is all that is left.
|
|
360
|
+
cancelStream();
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
stopProcess("SIGTERM");
|
|
364
|
+
if (killTimer !== undefined)
|
|
365
|
+
return;
|
|
366
|
+
killTimer = setTimeout(() => {
|
|
367
|
+
killTimer = undefined;
|
|
368
|
+
stopProcess("SIGKILL");
|
|
369
|
+
cancelStream();
|
|
370
|
+
}, killGraceMs);
|
|
371
|
+
killTimer.unref?.();
|
|
372
|
+
};
|
|
373
|
+
const abortWith = (error) => {
|
|
374
|
+
escalateStop();
|
|
375
|
+
settle(() => reject(error));
|
|
233
376
|
};
|
|
234
377
|
if (typeof timeoutMs === "number" && timeoutMs > 0) {
|
|
235
378
|
timer = setTimeout(() => {
|
|
236
379
|
timer = undefined;
|
|
237
|
-
|
|
238
|
-
// Best effort: ask the guest to terminate the running process.
|
|
239
|
-
unaryGuest({ binding }, "signal", {
|
|
240
|
-
processId,
|
|
241
|
-
signal: "SIGTERM",
|
|
242
|
-
}).catch(() => { });
|
|
243
|
-
}
|
|
244
|
-
reject(new Error(`command timed out after ${timeoutMs} ms`));
|
|
380
|
+
abortWith(new Error(`command timed out after ${timeoutMs} ms`));
|
|
245
381
|
}, timeoutMs);
|
|
246
382
|
}
|
|
383
|
+
if (signal !== undefined) {
|
|
384
|
+
if (signal.aborted) {
|
|
385
|
+
abortWith(fsError("FS_ABORTED", "exec aborted"));
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
const onAbort = () => abortWith(fsError("FS_ABORTED", "exec aborted"));
|
|
389
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
390
|
+
detach = () => signal.removeEventListener("abort", onAbort);
|
|
391
|
+
}
|
|
247
392
|
stream.on("data", (output) => {
|
|
248
393
|
if (output.processId)
|
|
249
394
|
processId = String(output.processId);
|
|
250
|
-
if (output.stdoutChunk)
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
395
|
+
if (output.stdoutChunk) {
|
|
396
|
+
stdoutReader.append(Buffer.from(output.stdoutChunk));
|
|
397
|
+
}
|
|
398
|
+
if (output.stderrChunk) {
|
|
399
|
+
stderrReader.append(Buffer.from(output.stderrChunk));
|
|
400
|
+
}
|
|
254
401
|
if (output.exit) {
|
|
255
|
-
|
|
256
|
-
|
|
402
|
+
// The command stopped on its own within the grace period: nothing is
|
|
403
|
+
// left to escalate.
|
|
404
|
+
clearKillTimer();
|
|
405
|
+
stdoutReader.setSpillValid(Boolean(output.exit.stdoutSpillValid));
|
|
406
|
+
stderrReader.setSpillValid(Boolean(output.exit.stderrSpillValid));
|
|
407
|
+
const out = stdoutReader.readFrom(0);
|
|
408
|
+
const err = stderrReader.readFrom(0);
|
|
409
|
+
// A spill the in-memory tail already covered is deleted; a valid one
|
|
410
|
+
// holding dropped output stays for the caller to read.
|
|
411
|
+
discardUnneededSpill({ guest, token }, stdoutReader);
|
|
412
|
+
discardUnneededSpill({ guest, token }, stderrReader);
|
|
413
|
+
settle(() => resolveDone({
|
|
257
414
|
exitCode: output.exit.exitCode,
|
|
258
415
|
signal: output.exit.signaled ? output.exit.signal : null,
|
|
259
|
-
stdout:
|
|
260
|
-
stderr:
|
|
261
|
-
|
|
416
|
+
stdout: out.text,
|
|
417
|
+
stderr: err.text,
|
|
418
|
+
...(out.spillPath === undefined
|
|
419
|
+
? {}
|
|
420
|
+
: { stdoutSpillPath: out.spillPath }),
|
|
421
|
+
...(err.spillPath === undefined
|
|
422
|
+
? {}
|
|
423
|
+
: { stderrSpillPath: err.spillPath }),
|
|
424
|
+
}));
|
|
262
425
|
}
|
|
263
426
|
});
|
|
264
|
-
stream.on("error", (error) =>
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
427
|
+
stream.on("error", (error) => settle(() => reject(error)));
|
|
428
|
+
stream.on("end", () =>
|
|
429
|
+
// A stream that ends without an exit message (guest restart, dropped
|
|
430
|
+
// socket) must not leave the caller pending forever.
|
|
431
|
+
settle(() => reject(new Error("exec stream ended before the process exited"))));
|
|
268
432
|
stream.write({
|
|
269
433
|
start: {
|
|
270
434
|
argv: remoteArgv(argv),
|
|
271
435
|
cwd,
|
|
272
436
|
env: env ?? {},
|
|
273
|
-
...(identity?.uid !== undefined
|
|
274
|
-
|
|
275
|
-
|
|
437
|
+
...(identity?.uid !== undefined
|
|
438
|
+
? { uid: { value: identity.uid } }
|
|
439
|
+
: {}),
|
|
440
|
+
...(identity?.gid !== undefined
|
|
441
|
+
? { gid: { value: identity.gid } }
|
|
442
|
+
: {}),
|
|
443
|
+
...(identity?.groups !== undefined
|
|
444
|
+
? { groups: identity.groups }
|
|
445
|
+
: {}),
|
|
446
|
+
spillStdout: stdoutSpec,
|
|
447
|
+
spillStderr: stderrSpec,
|
|
276
448
|
},
|
|
277
449
|
});
|
|
278
450
|
stream.end();
|
|
279
|
-
});
|
|
451
|
+
}));
|
|
280
452
|
}
|
|
281
453
|
// sliceLines returns the requested 1-based line range of a file's content,
|
|
282
454
|
// defaulting to the first READ_LIMIT lines like the harness's read tool.
|
|
@@ -289,6 +461,45 @@ export function sliceLines(content, offset, limit) {
|
|
|
289
461
|
: READ_LIMIT;
|
|
290
462
|
return content.split("\n").slice(start - 1, start - 1 + max).join("\n");
|
|
291
463
|
}
|
|
464
|
+
// streamLines returns the requested 1-based line window of a text stream
|
|
465
|
+
// without buffering the whole source. It matches sliceLines exactly — the
|
|
466
|
+
// source is split on "\n", so a trailing newline keeps a final empty line —
|
|
467
|
+
// while consuming only as much of the stream as the window needs.
|
|
468
|
+
export async function streamLines(stream, offset, limit) {
|
|
469
|
+
const start = typeof offset === "number" && Number.isInteger(offset) && offset > 0
|
|
470
|
+
? offset
|
|
471
|
+
: 1;
|
|
472
|
+
const max = typeof limit === "number" && Number.isInteger(limit) && limit > 0
|
|
473
|
+
? limit
|
|
474
|
+
: READ_LIMIT;
|
|
475
|
+
const end = start - 1 + max;
|
|
476
|
+
const out = [];
|
|
477
|
+
let index = 0;
|
|
478
|
+
let carry = "";
|
|
479
|
+
let filled = false;
|
|
480
|
+
for await (const chunk of stream) {
|
|
481
|
+
carry += chunk;
|
|
482
|
+
let newline = carry.indexOf("\n");
|
|
483
|
+
while (newline >= 0) {
|
|
484
|
+
if (index >= start - 1 && index < end)
|
|
485
|
+
out.push(carry.slice(0, newline));
|
|
486
|
+
index++;
|
|
487
|
+
carry = carry.slice(newline + 1);
|
|
488
|
+
newline = carry.indexOf("\n");
|
|
489
|
+
if (index >= end) {
|
|
490
|
+
filled = true;
|
|
491
|
+
break;
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
if (filled)
|
|
495
|
+
break;
|
|
496
|
+
}
|
|
497
|
+
// The segment after the last newline, which split always keeps (possibly
|
|
498
|
+
// empty) when the stream ended before the window was filled.
|
|
499
|
+
if (!filled && index >= start - 1 && index < end)
|
|
500
|
+
out.push(carry);
|
|
501
|
+
return out.join("\n");
|
|
502
|
+
}
|
|
292
503
|
export function outputLines(text) {
|
|
293
504
|
return text
|
|
294
505
|
.split("\n")
|
|
@@ -305,14 +516,18 @@ export function currentCwd(exec) {
|
|
|
305
516
|
}
|
|
306
517
|
export async function sessionWorkspaceSlug(resolver, cwd) {
|
|
307
518
|
const workspace = await resolver.registry?.resolveByPath?.(String(cwd));
|
|
308
|
-
if (workspace === undefined)
|
|
309
|
-
|
|
519
|
+
if (workspace === undefined) {
|
|
520
|
+
// "default" is not a workspace slug, so returning it only produced an
|
|
521
|
+
// opaque orchestrator error downstream and a fabricated container_list
|
|
522
|
+
// row; fail with the same shape resolve() uses instead.
|
|
523
|
+
throw new Error(`cannot resolve a DH workspace without a session working directory (got ${JSON.stringify(cwd)})`);
|
|
524
|
+
}
|
|
310
525
|
return workspaceSlug(workspace.id);
|
|
311
526
|
}
|
|
312
|
-
export async function resolveToolBinding(resolver, cwd, container) {
|
|
527
|
+
export async function resolveToolBinding(resolver, cwd, container, signal) {
|
|
313
528
|
if (container === "default")
|
|
314
|
-
return resolver.resolve(cwd);
|
|
315
|
-
return resolver.containerBinding(cwd, container);
|
|
529
|
+
return resolver.resolve(cwd, signal);
|
|
530
|
+
return resolver.containerBinding(cwd, container, signal);
|
|
316
531
|
}
|
|
317
532
|
export async function guestStat(target, signal) {
|
|
318
533
|
const result = await guestStatResponse(target, signal);
|
|
@@ -326,29 +541,31 @@ export async function guestStat(target, signal) {
|
|
|
326
541
|
}
|
|
327
542
|
export function guestStatResponse(target, signal) {
|
|
328
543
|
throwIfAborted(signal, "stat");
|
|
329
|
-
return new Promise((resolveDone, reject) => {
|
|
544
|
+
return withGuestAuth(target.binding, (guest, token) => new Promise((resolveDone, reject) => {
|
|
330
545
|
let detach = () => { };
|
|
331
|
-
const call =
|
|
546
|
+
const call = guest.stat({ path: target.targetKey }, metadata(token), (error, value) => {
|
|
332
547
|
detach();
|
|
333
548
|
error
|
|
334
549
|
? reject(signal?.aborted ? fsError("FS_ABORTED", "stat aborted") : error)
|
|
335
550
|
: resolveDone(value);
|
|
336
551
|
});
|
|
337
552
|
detach = onAbortCancel(call, signal);
|
|
338
|
-
});
|
|
553
|
+
}));
|
|
339
554
|
}
|
|
340
555
|
export async function unaryGuest(target, method, request, signal) {
|
|
341
556
|
throwIfAborted(signal, method);
|
|
342
|
-
return new Promise((resolveDone, reject) => {
|
|
557
|
+
return withGuestAuth(target.binding, (guest, token) => new Promise((resolveDone, reject) => {
|
|
343
558
|
let detach = () => { };
|
|
344
|
-
const call =
|
|
559
|
+
const call = guest[method](request, metadata(token), (error, result) => {
|
|
345
560
|
detach();
|
|
346
561
|
error
|
|
347
|
-
? reject(signal?.aborted
|
|
562
|
+
? reject(signal?.aborted
|
|
563
|
+
? fsError("FS_ABORTED", `${method} aborted`)
|
|
564
|
+
: error)
|
|
348
565
|
: resolveDone(result);
|
|
349
566
|
});
|
|
350
567
|
detach = onAbortCancel(call, signal);
|
|
351
|
-
});
|
|
568
|
+
}));
|
|
352
569
|
}
|
|
353
570
|
// globCwd returns the working directory a ripgrep discovery listing must run
|
|
354
571
|
// in, or undefined to keep the caller's. The harness's glob tool passes its
|
|
@@ -359,8 +576,9 @@ export async function unaryGuest(target, method, request, signal) {
|
|
|
359
576
|
// the harness renders against the session working directory.
|
|
360
577
|
export function globCwd(argv, cwd) {
|
|
361
578
|
const runner = argv[0];
|
|
362
|
-
if (runner === undefined || !/(?:^|\/)rg(?:\.exe)?$/.test(runner))
|
|
579
|
+
if (runner === undefined || !/(?:^|\/)rg(?:\.exe)?$/.test(runner)) {
|
|
363
580
|
return undefined;
|
|
581
|
+
}
|
|
364
582
|
if (!argv.includes("--files"))
|
|
365
583
|
return undefined;
|
|
366
584
|
const separator = argv.indexOf("--");
|
|
@@ -370,8 +588,9 @@ export function globCwd(argv, cwd) {
|
|
|
370
588
|
if (roots.length !== 1)
|
|
371
589
|
return undefined;
|
|
372
590
|
const root = roots[0];
|
|
373
|
-
if (typeof cwd !== "string" || cwd === "" || !isAbsolute(root))
|
|
591
|
+
if (typeof cwd !== "string" || cwd === "" || !isAbsolute(root)) {
|
|
374
592
|
return undefined;
|
|
593
|
+
}
|
|
375
594
|
return root;
|
|
376
595
|
}
|
|
377
596
|
// remoteArgv rewrites an argv for the guest's execution world: the harness's
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|