@solarisdk/core 0.4.4 → 0.4.5
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/dist/handle.d.ts +10 -1
- package/dist/handle.js +97 -14
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/types.d.ts +8 -0
- package/package.json +1 -1
package/dist/handle.d.ts
CHANGED
|
@@ -131,6 +131,8 @@ export declare class SessionHandle {
|
|
|
131
131
|
connect: () => Promise<void>;
|
|
132
132
|
};
|
|
133
133
|
private startCommand;
|
|
134
|
+
/** SIGTERM + close stdin now, SIGKILL after CANCEL_KILL_GRACE_MS. Best effort: a dead channel or a finished command is fine. */
|
|
135
|
+
private cancelCommand;
|
|
134
136
|
private runCommand;
|
|
135
137
|
private runCommandInner;
|
|
136
138
|
readonly pty: {
|
|
@@ -146,6 +148,7 @@ export declare class SessionHandle {
|
|
|
146
148
|
/** Create a fresh stateful kernel context (`code.context.create`). */
|
|
147
149
|
createCodeContext(language?: CodeLanguageArg): Promise<string>;
|
|
148
150
|
readonly files: {
|
|
151
|
+
/** Read a whole file. Chunked under the hood (4 MiB per frame), so a big file never travels as one frame. */
|
|
149
152
|
read: (path: string) => Promise<Uint8Array>;
|
|
150
153
|
readText: (path: string) => Promise<string>;
|
|
151
154
|
write: (path: string, data: Uint8Array | string, mode?: number) => Promise<void>;
|
|
@@ -161,7 +164,7 @@ export declare class SessionHandle {
|
|
|
161
164
|
* Returns an unwatch function.
|
|
162
165
|
*/
|
|
163
166
|
watch: (path: string, cb: (ev: FsWatchEvent) => void, recursive?: boolean) => Promise<() => Promise<void>>;
|
|
164
|
-
/** Upload local bytes to an in-guest path (
|
|
167
|
+
/** Upload local bytes to an in-guest path: sent in 4 MiB chunks (offset/truncate), so a big file never travels as one frame. */
|
|
165
168
|
upload: (path: string, data: Uint8Array | string) => Promise<void>;
|
|
166
169
|
/** Download an in-guest file's bytes. */
|
|
167
170
|
download: (path: string) => Promise<Uint8Array>;
|
|
@@ -300,4 +303,10 @@ export declare class SessionHandle {
|
|
|
300
303
|
}
|
|
301
304
|
/** Local helper type for `createCodeContext` to avoid a hard import cycle. */
|
|
302
305
|
type CodeLanguageArg = "python" | "javascript" | "typescript" | "bash" | "r";
|
|
306
|
+
/** How long a cancelled command gets between SIGTERM and SIGKILL. */
|
|
307
|
+
export declare const CANCEL_KILL_GRACE_MS = 2000;
|
|
308
|
+
/** Bytes per fs.upload / fs.download frame. */
|
|
309
|
+
export declare const FILE_CHUNK_BYTES: number;
|
|
310
|
+
/** Largest whole-file payload one single-frame call (files.write) accepts; above it use files.upload / files.download / a signed URL. */
|
|
311
|
+
export declare const MAX_SINGLE_CALL_BYTES: number;
|
|
303
312
|
export {};
|
package/dist/handle.js
CHANGED
|
@@ -127,6 +127,22 @@ export class SessionHandle {
|
|
|
127
127
|
exitResolve?.(-1);
|
|
128
128
|
}, opts.timeoutMs);
|
|
129
129
|
}
|
|
130
|
+
if (opts.signal) {
|
|
131
|
+
const signal = opts.signal;
|
|
132
|
+
const onAbort = () => {
|
|
133
|
+
clearTimeout(deadline);
|
|
134
|
+
void this.cancelCommand(cmdId);
|
|
135
|
+
this.channel.offFrame("cmd.data", cmdId);
|
|
136
|
+
this.channel.offFrame("cmd.exit", cmdId);
|
|
137
|
+
this.channel.offStreamClose(cmdId);
|
|
138
|
+
exitReject?.(abortReason(signal));
|
|
139
|
+
};
|
|
140
|
+
if (signal.aborted)
|
|
141
|
+
onAbort();
|
|
142
|
+
else
|
|
143
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
144
|
+
void exitPromise.then(() => signal.removeEventListener("abort", onAbort), () => signal.removeEventListener("abort", onAbort));
|
|
145
|
+
}
|
|
130
146
|
if (acc) {
|
|
131
147
|
dataCbs.push((c) => {
|
|
132
148
|
if (c.stream === "stdout")
|
|
@@ -191,6 +207,13 @@ export class SessionHandle {
|
|
|
191
207
|
.then(() => undefined),
|
|
192
208
|
};
|
|
193
209
|
}
|
|
210
|
+
async cancelCommand(cmdId) {
|
|
211
|
+
await this.channel.call("cmd.kill", { cmdId, signal: 15 }).catch(() => undefined);
|
|
212
|
+
await this.channel.call("cmd.stdin", { cmdId, base64: "", eof: true }).catch(() => undefined);
|
|
213
|
+
setTimeout(() => {
|
|
214
|
+
this.channel.call("cmd.kill", { cmdId, signal: 9 }).catch(() => undefined);
|
|
215
|
+
}, CANCEL_KILL_GRACE_MS).unref?.();
|
|
216
|
+
}
|
|
194
217
|
async runCommand(cmd, opts) {
|
|
195
218
|
try {
|
|
196
219
|
return await this.runCommandInner(cmd, opts);
|
|
@@ -210,6 +233,7 @@ export class SessionHandle {
|
|
|
210
233
|
!opts.onStderr &&
|
|
211
234
|
!opts.env &&
|
|
212
235
|
!opts.user &&
|
|
236
|
+
!opts.signal &&
|
|
213
237
|
timeoutMs >= MIN_REST_TIMEOUT_MS &&
|
|
214
238
|
timeoutMs <= DEFAULT_COMMAND_TIMEOUT_MS;
|
|
215
239
|
const viaRest = async () => {
|
|
@@ -320,11 +344,34 @@ export class SessionHandle {
|
|
|
320
344
|
};
|
|
321
345
|
}
|
|
322
346
|
async runCode(code, opts = {}) {
|
|
323
|
-
const
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
347
|
+
const params = { code, language: opts.language, contextId: opts.contextId };
|
|
348
|
+
const running = this.channel.call("code.run", params);
|
|
349
|
+
let r;
|
|
350
|
+
const signal = opts.signal;
|
|
351
|
+
if (signal) {
|
|
352
|
+
let onAbort;
|
|
353
|
+
const aborted = new Promise((_, reject) => {
|
|
354
|
+
onAbort = () => {
|
|
355
|
+
this.channel.call("code.interrupt", { language: opts.language, contextId: opts.contextId }).catch(() => undefined);
|
|
356
|
+
reject(abortReason(signal));
|
|
357
|
+
};
|
|
358
|
+
if (signal.aborted)
|
|
359
|
+
onAbort();
|
|
360
|
+
else
|
|
361
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
362
|
+
});
|
|
363
|
+
running.catch(() => undefined);
|
|
364
|
+
try {
|
|
365
|
+
r = await Promise.race([running, aborted]);
|
|
366
|
+
}
|
|
367
|
+
finally {
|
|
368
|
+
if (onAbort)
|
|
369
|
+
signal.removeEventListener("abort", onAbort);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
else {
|
|
373
|
+
r = await running;
|
|
374
|
+
}
|
|
328
375
|
const results = Array.isArray(r.results) ? r.results : [];
|
|
329
376
|
for (const item of results) {
|
|
330
377
|
if (item.type === "stdout" && item.text)
|
|
@@ -343,16 +390,29 @@ export class SessionHandle {
|
|
|
343
390
|
}
|
|
344
391
|
files = {
|
|
345
392
|
read: async (path) => {
|
|
346
|
-
|
|
347
|
-
|
|
393
|
+
try {
|
|
394
|
+
return await downloadAll((method, params) => this.channel.call(method, { ...params, length: FILE_CHUNK_BYTES }), path);
|
|
395
|
+
}
|
|
396
|
+
catch (err) {
|
|
397
|
+
if (!isUnknownMethod(err))
|
|
398
|
+
throw err;
|
|
399
|
+
const r = await this.channel.call("fs.read", { path });
|
|
400
|
+
return base64ToBytes(r.base64);
|
|
401
|
+
}
|
|
348
402
|
},
|
|
349
403
|
readText: async (path) => {
|
|
350
404
|
const bytes = await this.files.read(path);
|
|
351
405
|
return new TextDecoder().decode(bytes);
|
|
352
406
|
},
|
|
353
|
-
write: (path, data, mode) =>
|
|
354
|
-
|
|
355
|
-
.
|
|
407
|
+
write: (path, data, mode) => {
|
|
408
|
+
const bytes = typeof data === "string" ? new TextEncoder().encode(data) : data;
|
|
409
|
+
if (bytes.length > MAX_SINGLE_CALL_BYTES) {
|
|
410
|
+
return Promise.reject(singleCallTooLarge(bytes.length));
|
|
411
|
+
}
|
|
412
|
+
return this.channel
|
|
413
|
+
.call("fs.write", { path, base64: toB64(bytes), mode })
|
|
414
|
+
.then(() => undefined);
|
|
415
|
+
},
|
|
356
416
|
list: async (path) => {
|
|
357
417
|
const r = await this.channel.call("fs.list", {
|
|
358
418
|
path,
|
|
@@ -387,10 +447,16 @@ export class SessionHandle {
|
|
|
387
447
|
await this.channel.call("fs.unwatch", { watchId });
|
|
388
448
|
};
|
|
389
449
|
},
|
|
390
|
-
upload: (path, data) =>
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
450
|
+
upload: async (path, data) => {
|
|
451
|
+
const bytes = typeof data === "string" ? new TextEncoder().encode(data) : data;
|
|
452
|
+
let offset = 0;
|
|
453
|
+
do {
|
|
454
|
+
const slice = bytes.subarray(offset, offset + FILE_CHUNK_BYTES);
|
|
455
|
+
await this.channel.call("fs.upload", { path, base64: toB64(slice), offset, truncate: offset === 0 });
|
|
456
|
+
offset += slice.length;
|
|
457
|
+
} while (offset < bytes.length);
|
|
458
|
+
},
|
|
459
|
+
download: (path) => downloadAll((method, params) => this.channel.call(method, { ...params, length: FILE_CHUNK_BYTES }), path),
|
|
394
460
|
};
|
|
395
461
|
env(vars) {
|
|
396
462
|
return this.channel
|
|
@@ -728,6 +794,23 @@ function isRouteUnavailable(err) {
|
|
|
728
794
|
return (err instanceof GatewayError &&
|
|
729
795
|
(err.status === 404 || err.status === 405 || err.status === 501));
|
|
730
796
|
}
|
|
797
|
+
function isUnknownMethod(err) {
|
|
798
|
+
return /unknown method|method not found|not implemented/i.test(err instanceof Error ? err.message : String(err));
|
|
799
|
+
}
|
|
800
|
+
export const CANCEL_KILL_GRACE_MS = 2000;
|
|
801
|
+
function abortReason(signal) {
|
|
802
|
+
const r = signal.reason;
|
|
803
|
+
if (r instanceof Error)
|
|
804
|
+
return r;
|
|
805
|
+
const e = new Error("The command was cancelled");
|
|
806
|
+
e.name = "AbortError";
|
|
807
|
+
return e;
|
|
808
|
+
}
|
|
809
|
+
export const FILE_CHUNK_BYTES = 4 * 1024 * 1024;
|
|
810
|
+
export const MAX_SINGLE_CALL_BYTES = 32 * 1024 * 1024;
|
|
811
|
+
function singleCallTooLarge(n) {
|
|
812
|
+
return new SolariError(`file too large for a single call (${n} bytes, max ${MAX_SINGLE_CALL_BYTES}): use files.upload()/files.download() or uploadUrl()`);
|
|
813
|
+
}
|
|
731
814
|
function toB64(data) {
|
|
732
815
|
const bytes = typeof data === "string" ? new TextEncoder().encode(data) : data;
|
|
733
816
|
return bytesToBase64(bytes);
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { DEFAULT_COMMAND_TIMEOUT_MS, SessionHandle, markSessionReleased, unreleasedSessionIds, exitReminderMessages } from "./handle.js";
|
|
1
|
+
export { CANCEL_KILL_GRACE_MS, DEFAULT_COMMAND_TIMEOUT_MS, FILE_CHUNK_BYTES, MAX_SINGLE_CALL_BYTES, SessionHandle, markSessionReleased, unreleasedSessionIds, exitReminderMessages } from "./handle.js";
|
|
2
2
|
export { shortId } from "./ids.js";
|
|
3
3
|
export { formatIdlePolicy } from "./idle-policy.js";
|
|
4
4
|
export type { SessionConfig, SessionHooks } from "./handle.js";
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { DEFAULT_COMMAND_TIMEOUT_MS, SessionHandle, markSessionReleased, unreleasedSessionIds, exitReminderMessages } from "./handle.js";
|
|
1
|
+
export { CANCEL_KILL_GRACE_MS, DEFAULT_COMMAND_TIMEOUT_MS, FILE_CHUNK_BYTES, MAX_SINGLE_CALL_BYTES, SessionHandle, markSessionReleased, unreleasedSessionIds, exitReminderMessages } from "./handle.js";
|
|
2
2
|
export { shortId } from "./ids.js";
|
|
3
3
|
export { formatIdlePolicy } from "./idle-policy.js";
|
|
4
4
|
export { Desktop } from "./desktop.js";
|
package/dist/types.d.ts
CHANGED
|
@@ -767,6 +767,12 @@ export interface CommandOptions {
|
|
|
767
767
|
background?: boolean;
|
|
768
768
|
onStdout?: (data: string) => void;
|
|
769
769
|
onStderr?: (data: string) => void;
|
|
770
|
+
/**
|
|
771
|
+
* Cancel the command. On abort the process in the sandbox gets SIGTERM and its stdin is closed, then SIGKILL after
|
|
772
|
+
* 2 s if it is still running; the call rejects with the signal's reason. Giving a signal runs the command over the live
|
|
773
|
+
* connection (the one-shot REST route has nothing to kill).
|
|
774
|
+
*/
|
|
775
|
+
signal?: AbortSignal;
|
|
770
776
|
}
|
|
771
777
|
/** Terminal result of `commands.run`. */
|
|
772
778
|
export interface CommandResult {
|
|
@@ -835,6 +841,8 @@ export interface RunCodeOptions {
|
|
|
835
841
|
contextId?: string;
|
|
836
842
|
onStdout?: (data: string) => void;
|
|
837
843
|
onStderr?: (data: string) => void;
|
|
844
|
+
/** Cancel the run: the cell is interrupted in the kernel (KeyboardInterrupt) and the call rejects with the signal's reason. */
|
|
845
|
+
signal?: AbortSignal;
|
|
838
846
|
}
|
|
839
847
|
/** Kind of a structured chart extracted from a matplotlib figure. */
|
|
840
848
|
export type ChartType = "line" | "scatter" | "bar" | "pie" | "box_and_whisker" | "composite" | "unknown";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@solarisdk/core",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.5",
|
|
4
4
|
"description": "Shared runtime for the Solari SDK \u2014 transport, session handles, images, templates, and types used by @solarisdk/desktop and @solarisdk/sandbox.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|