@promigence/sdk 0.1.0-beta.15 → 0.1.0-beta.17
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/caller.js +1 -1
- package/dist/cjs/caller.js +1 -1
- package/dist/cjs/client.d.ts +173 -19
- package/dist/cjs/client.js +451 -129
- package/dist/cjs/command-errors.d.ts +1 -0
- package/dist/cjs/command-errors.js +2 -0
- package/dist/cjs/context.d.ts +20 -4
- package/dist/cjs/context.js +82 -5
- package/dist/cjs/core.d.ts +56 -5
- package/dist/cjs/core.js +49 -5
- package/dist/cjs/handles.js +2 -1
- package/dist/cjs/http2.d.ts +8 -0
- package/dist/cjs/http2.js +27 -1
- package/dist/cjs/models.gen.d.ts +269 -30
- package/dist/cjs/proxy.js +32 -3
- package/dist/cjs/sandbox.d.ts +38 -13
- package/dist/cjs/sandbox.js +29 -5
- package/dist/cjs/transport-worker.js +10 -1
- package/dist/client.d.ts +173 -19
- package/dist/client.js +450 -126
- package/dist/command-errors.d.ts +1 -0
- package/dist/command-errors.js +3 -1
- package/dist/context.d.ts +20 -4
- package/dist/context.js +79 -4
- package/dist/core.d.ts +56 -5
- package/dist/core.js +44 -4
- package/dist/handles.js +2 -1
- package/dist/http2.d.ts +8 -0
- package/dist/http2.js +27 -1
- package/dist/models.gen.d.ts +269 -30
- package/dist/proxy.js +33 -4
- package/dist/sandbox.d.ts +38 -13
- package/dist/sandbox.js +29 -5
- package/dist/transport-worker.js +10 -1
- package/package.json +1 -1
package/dist/client.js
CHANGED
|
@@ -2,7 +2,7 @@ import { readFileSync, statSync } from "node:fs";
|
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import { inspect } from "node:util";
|
|
4
4
|
import { dirname, join } from "node:path";
|
|
5
|
-
import { ApiError, AuthError, EnvFailedError, PromigenceError, InvalidArgumentError, NotFoundError, NotReleasedError, NotVerifiedError, RateLimitError, ServiceRestartError, SpendCapError, TimeoutError, UnreachableError, DEFAULT_API_URL,
|
|
5
|
+
import { ApiError, AuthError, EnvFailedError, ExecLostError, PromigenceError, InvalidArgumentError, NotFoundError, NotReleasedError, NotVerifiedError, RateLimitError, ServiceRestartError, SpendCapError, TimeoutError, UnreachableError, DEFAULT_API_URL, FILE_PUT_CHUNK_BYTES, MAX_OUTPUT_BYTES_DEFAULT, VERSION, WAIT_TIMEOUT_MS, PreviewsUnavailableError, SANDBOX_ENDED_FIX, SandboxEndedError, errorsDocsFor, exitForCode, previewsOff, sandboxEndedIn, withPreviewsFix, } from "./core.js";
|
|
6
6
|
import { buildBundle } from "./bundle.js";
|
|
7
7
|
import { keychainReadCached, keychainStore, storeLabel } from "./credstore.js";
|
|
8
8
|
import { CALLER_HEADER, resolveCaller, userAgentWithCaller } from "./caller.js";
|
|
@@ -16,6 +16,7 @@ import { isKeyedCreate } from "./keyed.js";
|
|
|
16
16
|
import { customDispatcher, longRequestDispatcher } from "./dispatcher.js";
|
|
17
17
|
import { compromisedKeyCause, guardedFetchDispatcher } from "./ca-guard.js";
|
|
18
18
|
import { hintArg } from "./hint.js";
|
|
19
|
+
import { answerTimedOut } from "./command-errors.js";
|
|
19
20
|
import { GITHUB_REPO_PREFIX, githubCreateBody, } from "./github.js";
|
|
20
21
|
import { GITLAB_REPO_PREFIX, } from "./gitlab.js";
|
|
21
22
|
export const FILE_TIMEOUT_MS = 15 * 60_000;
|
|
@@ -93,27 +94,46 @@ function pageQuery(limit, max, cursor) {
|
|
|
93
94
|
}
|
|
94
95
|
export const OUTPUT_RETENTION_MAX_DAYS = 30;
|
|
95
96
|
function projectUpdateBody(u) {
|
|
97
|
+
return settingsBody(u, "projects.update", "the organization's");
|
|
98
|
+
}
|
|
99
|
+
function orgUpdateBody(u) {
|
|
100
|
+
return settingsBody(u, "orgs.update", "the default");
|
|
101
|
+
}
|
|
102
|
+
function settingsBody(u, what, fallback) {
|
|
96
103
|
const body = {};
|
|
97
104
|
if (u.name !== undefined)
|
|
98
105
|
body["name"] = u.name;
|
|
99
106
|
if (u.outputRetentionDays !== undefined) {
|
|
100
107
|
const d = u.outputRetentionDays;
|
|
101
108
|
if (d !== null && (!Number.isInteger(d) || d < 1 || d > OUTPUT_RETENTION_MAX_DAYS)) {
|
|
102
|
-
throw new InvalidArgumentError(`outputRetentionDays must be a whole number from 1 to ${OUTPUT_RETENTION_MAX_DAYS}, or null, not ${String(d)}`,
|
|
109
|
+
throw new InvalidArgumentError(`outputRetentionDays must be a whole number from 1 to ${OUTPUT_RETENTION_MAX_DAYS}, or null, not ${String(d)}`, `Pass null for ${fallback} setting.`);
|
|
103
110
|
}
|
|
104
111
|
body["output_retention_days"] = d;
|
|
105
112
|
}
|
|
106
113
|
if (u.failureCaptures !== undefined) {
|
|
107
114
|
if (u.failureCaptures !== null && typeof u.failureCaptures !== "boolean") {
|
|
108
|
-
throw new InvalidArgumentError("failureCaptures must be true, false or null",
|
|
115
|
+
throw new InvalidArgumentError("failureCaptures must be true, false or null", `Pass null for ${fallback} setting.`);
|
|
109
116
|
}
|
|
110
117
|
if (Object.keys(body).length) {
|
|
111
118
|
throw new InvalidArgumentError("failureCaptures is set on its own", "Change it in a call of its own, without name or outputRetentionDays.");
|
|
112
119
|
}
|
|
113
120
|
body["failure_captures"] = u.failureCaptures;
|
|
114
121
|
}
|
|
122
|
+
if (u.previewLinkLength !== undefined) {
|
|
123
|
+
const l = u.previewLinkLength;
|
|
124
|
+
if (l === "custom") {
|
|
125
|
+
throw new InvalidArgumentError("custom domains for preview links are coming soon", `Pass "short", "long" or null (${fallback}) for now.`);
|
|
126
|
+
}
|
|
127
|
+
if (l !== null && l !== "short" && l !== "long") {
|
|
128
|
+
throw new InvalidArgumentError(`previewLinkLength must be "short", "long" or null, not ${JSON.stringify(l)}`, `Pass null for ${fallback} setting.`);
|
|
129
|
+
}
|
|
130
|
+
if (Object.keys(body).length) {
|
|
131
|
+
throw new InvalidArgumentError("previewLinkLength is set on its own", "Change it in a call of its own, without the other settings.");
|
|
132
|
+
}
|
|
133
|
+
body["preview_link_length"] = l;
|
|
134
|
+
}
|
|
115
135
|
if (!Object.keys(body).length) {
|
|
116
|
-
throw new InvalidArgumentError(
|
|
136
|
+
throw new InvalidArgumentError(`${what} needs something to change`, "Pass name, outputRetentionDays, failureCaptures or previewLinkLength.");
|
|
117
137
|
}
|
|
118
138
|
return body;
|
|
119
139
|
}
|
|
@@ -125,6 +145,14 @@ function expiryFields(o) {
|
|
|
125
145
|
body["expires_at"] = o.expiresAt;
|
|
126
146
|
return body;
|
|
127
147
|
}
|
|
148
|
+
function checkPreviewLength(length) {
|
|
149
|
+
if (length === undefined || length === "short" || length === "long")
|
|
150
|
+
return;
|
|
151
|
+
if (length === "custom") {
|
|
152
|
+
throw new InvalidArgumentError("custom domains for preview links are coming soon", "Pass length: \"short\" (the default) or \"long\" for now.");
|
|
153
|
+
}
|
|
154
|
+
throw new InvalidArgumentError(`length must be "short" or "long", got ${JSON.stringify(length)}`, "Omit it for the length your project or organization chose.");
|
|
155
|
+
}
|
|
128
156
|
export const NOTICE_HEADER = "promigence-notice";
|
|
129
157
|
export const MAX_SOURCES = 8;
|
|
130
158
|
export function configDir(env = process.env) {
|
|
@@ -276,30 +304,6 @@ export function readCredentials(env = process.env) {
|
|
|
276
304
|
function storedName(store, env) {
|
|
277
305
|
return store === undefined || store === "file" ? `the credentials file (${credentialsPath(env)})` : storeLabel(store);
|
|
278
306
|
}
|
|
279
|
-
const COMPAT_REFUSED_HOSTS = /(^|\.)e2b(-staging)?\.(app|dev|pro)$/i;
|
|
280
|
-
export function apiUrlFromCompatUrl(url) {
|
|
281
|
-
const u = (url ?? "").trim();
|
|
282
|
-
if (!u)
|
|
283
|
-
return undefined;
|
|
284
|
-
let host = "";
|
|
285
|
-
try {
|
|
286
|
-
host = new URL(u.includes("://") ? u : `https://${u}`).hostname;
|
|
287
|
-
}
|
|
288
|
-
catch {
|
|
289
|
-
return undefined;
|
|
290
|
-
}
|
|
291
|
-
return COMPAT_REFUSED_HOSTS.test(host) ? undefined : u;
|
|
292
|
-
}
|
|
293
|
-
export function apiUrlFromCompatDomain(domain) {
|
|
294
|
-
const d = (domain ?? "").trim().replace(/^https?:\/\//, "").replace(/\/+$/, "");
|
|
295
|
-
if (!d || COMPAT_REFUSED_HOSTS.test(d))
|
|
296
|
-
return undefined;
|
|
297
|
-
return `https://api.${d}/v1`;
|
|
298
|
-
}
|
|
299
|
-
export function compatApiKey(key) {
|
|
300
|
-
const k = key?.trim();
|
|
301
|
-
return k && !k.startsWith("e2b_") ? k : undefined;
|
|
302
|
-
}
|
|
303
307
|
export function keyOrigin(env = process.env) {
|
|
304
308
|
if (env["PROMIGENCE_API_KEY"]?.trim())
|
|
305
309
|
return "PROMIGENCE_API_KEY";
|
|
@@ -326,12 +330,13 @@ function resolveCredentials(opts, env = process.env) {
|
|
|
326
330
|
const inputs = optKey === undefined && envKey === undefined
|
|
327
331
|
? undefined
|
|
328
332
|
: `${optKey ?? "\0"}\n${envKey ?? "\0"}\n${envUrl ?? "\0"}\n${opts.apiUrl ?? "\0"}`;
|
|
329
|
-
if (inputs !== undefined && inputs === credsLastInputs && credsLast !== undefined)
|
|
330
|
-
return { apiKey: credsLast.apiKey, apiUrl: credsLast.apiUrl };
|
|
333
|
+
if (inputs !== undefined && inputs === credsLastInputs && credsLast !== undefined) {
|
|
334
|
+
return { apiKey: credsLast.apiKey, apiUrl: credsLast.apiUrl, ...(credsLast.origin ? { origin: credsLast.origin } : {}) };
|
|
335
|
+
}
|
|
331
336
|
const worked = resolveCredentialsFrom(opts, env, set, trim, optKey, envKey, envUrl);
|
|
332
337
|
if (inputs !== undefined && worked.refusal === undefined) {
|
|
333
338
|
credsLastInputs = inputs;
|
|
334
|
-
credsLast = { apiKey: worked.apiKey, apiUrl: worked.apiUrl };
|
|
339
|
+
credsLast = { apiKey: worked.apiKey, apiUrl: worked.apiUrl, ...(worked.origin ? { origin: worked.origin } : {}) };
|
|
335
340
|
}
|
|
336
341
|
return worked;
|
|
337
342
|
}
|
|
@@ -365,8 +370,15 @@ function resolveCredentialsFrom(opts, env, set, trim, optKey, envKey, envUrl) {
|
|
|
365
370
|
if (refusal)
|
|
366
371
|
return { apiKey: undefined, apiUrl, refusal };
|
|
367
372
|
}
|
|
368
|
-
|
|
373
|
+
const origin = !key ? undefined
|
|
374
|
+
: source === "opts" ? { source: "option" }
|
|
375
|
+
: source === "env" ? { source: "env" }
|
|
376
|
+
: source === "file" ? { source: "stored", ...(store === undefined ? {} : { store }) }
|
|
377
|
+
: { source: "fallback" };
|
|
378
|
+
return { apiKey: key, apiUrl, ...(origin ? { origin } : {}) };
|
|
369
379
|
}
|
|
380
|
+
export const NO_KEY_MESSAGE = "no API key: none was given, PROMIGENCE_API_KEY is not set, and none is stored";
|
|
381
|
+
export const NO_KEY_FIX = "Pass { apiKey } or set PROMIGENCE_API_KEY. No key yet: `promigence login` signs you up or in and stores one.";
|
|
370
382
|
function keyRefusal(key, from) {
|
|
371
383
|
if (/^[\x21-\x7E]+$/.test(key))
|
|
372
384
|
return undefined;
|
|
@@ -646,6 +658,8 @@ export function snapshotCreateBody(opts) {
|
|
|
646
658
|
}
|
|
647
659
|
if (o.alias)
|
|
648
660
|
body["alias"] = o.alias;
|
|
661
|
+
if (o.alias && o.moveAlias)
|
|
662
|
+
body["move_alias"] = true;
|
|
649
663
|
if (o.grade) {
|
|
650
664
|
if (!o.grade.cmd)
|
|
651
665
|
throw new InvalidArgumentError("snapshots.create grade needs a cmd", "Pass { grade: { cmd: 'pytest -q tests/' } }.");
|
|
@@ -680,6 +694,9 @@ const REFUSED_503_CODES = new Set([
|
|
|
680
694
|
"builder_unavailable", "builder_busy", "overloaded", "tier_unavailable", "registry_unavailable",
|
|
681
695
|
"secrets_unavailable",
|
|
682
696
|
]);
|
|
697
|
+
const SDK_FIXES = {
|
|
698
|
+
cap_exceeded: "Raise `spendCap`, lower `timeoutMs` or `count`, or pass `allowPartial` to launch what fits.",
|
|
699
|
+
};
|
|
683
700
|
function withoutCliHelp(fix) {
|
|
684
701
|
const out = fix.replace(/\s*See `promigence [^`]*--help`\.?\s*$/, "").trim();
|
|
685
702
|
if (out === fix.trim())
|
|
@@ -689,7 +706,7 @@ function withoutCliHelp(fix) {
|
|
|
689
706
|
export function errorFor(code, message, fix, extra = {}) {
|
|
690
707
|
const attach = (e) => {
|
|
691
708
|
const w = e;
|
|
692
|
-
w.docs = extra.docs
|
|
709
|
+
w.docs = extra.docs !== undefined && e.code === code ? extra.docs : errorsDocsFor(e.code);
|
|
693
710
|
if (extra.status !== undefined)
|
|
694
711
|
w.status = extra.status;
|
|
695
712
|
if (extra.requestId !== undefined)
|
|
@@ -705,6 +722,8 @@ export function errorFor(code, message, fix, extra = {}) {
|
|
|
705
722
|
if ((code === "usage" || code === "invalid_argument" || code === "unsupported") && previewsOff(message)) {
|
|
706
723
|
return attach(new PreviewsUnavailableError());
|
|
707
724
|
}
|
|
725
|
+
if (!extra.cli && Object.prototype.hasOwnProperty.call(SDK_FIXES, code))
|
|
726
|
+
fix = SDK_FIXES[code];
|
|
708
727
|
switch (code) {
|
|
709
728
|
case "auth": return attach(new AuthError(message, fix));
|
|
710
729
|
case "not_found": return attach(new NotFoundError(message, fix));
|
|
@@ -718,8 +737,13 @@ export function errorFor(code, message, fix, extra = {}) {
|
|
|
718
737
|
case "not_verified": return attach(new NotVerifiedError(message, fix));
|
|
719
738
|
case "timeout": return attach(new TimeoutError(message, fix));
|
|
720
739
|
case "usage":
|
|
721
|
-
case "invalid_argument":
|
|
740
|
+
case "invalid_argument": {
|
|
741
|
+
const ended = code === "usage" ? sandboxEndedIn(message) : undefined;
|
|
742
|
+
if (ended !== undefined) {
|
|
743
|
+
return attach(new SandboxEndedError(message, ended.sandboxId, ended.state, extra.cli ? fix : SANDBOX_ENDED_FIX));
|
|
744
|
+
}
|
|
722
745
|
return attach(new InvalidArgumentError(message, extra.cli ? fix : withoutCliHelp(fix), code));
|
|
746
|
+
}
|
|
723
747
|
case "not_released": return attach(new NotReleasedError(message));
|
|
724
748
|
case "service_restart": return attach(new ServiceRestartError(message, fix));
|
|
725
749
|
default:
|
|
@@ -772,6 +796,76 @@ function parseRetryAfter(header) {
|
|
|
772
796
|
const MAX_RETRY_SLEEP_MS = 10_000;
|
|
773
797
|
const STREAM_IDLE_MS = 90_000;
|
|
774
798
|
const sleep = (ms) => new Promise((r) => { setTimeout(r, ms); });
|
|
799
|
+
const EXEC_KEY_HEADER = "promigence-exec-key";
|
|
800
|
+
const EXEC_HOLD_DEFAULT_S = 120;
|
|
801
|
+
const REATTACH_SLACK_MS = 30_000;
|
|
802
|
+
const REATTACH_BACKOFF_MS = 250;
|
|
803
|
+
const REATTACH_BACKOFF_MAX_MS = 2_000;
|
|
804
|
+
const REATTACH_RETRY_AFTER_MAX_MS = 5_000;
|
|
805
|
+
const REATTACH_TAKEN_MS = 10_000;
|
|
806
|
+
const EXEC_CANCEL_TIMEOUT_MS = 3_000;
|
|
807
|
+
const REATTACH_AGAIN = new Set([429, 502, 503, 504]);
|
|
808
|
+
function newExecKey() {
|
|
809
|
+
const bytes = globalThis.crypto.getRandomValues(new Uint8Array(16));
|
|
810
|
+
let s = "";
|
|
811
|
+
for (const b of bytes)
|
|
812
|
+
s += b.toString(16).padStart(2, "0");
|
|
813
|
+
return s;
|
|
814
|
+
}
|
|
815
|
+
const UNSENT_CODES = /^(?:ECONNREFUSED|ENOTFOUND|EAI_[A-Z]+|ENETUNREACH|EHOSTUNREACH|UND_ERR_CONNECT_TIMEOUT|ERR_TLS_[A-Z_]+|ERR_SSL_[A-Z_]+|CERT_[A-Z_]+|UNABLE_TO_[A-Z_]+|DEPTH_ZERO_SELF_SIGNED_CERT|SELF_SIGNED_CERT_IN_CHAIN|HOSTNAME_MISMATCH|EPROTO)$/;
|
|
816
|
+
function provedUnsent(err) {
|
|
817
|
+
if (compromisedKeyCause(err) !== undefined)
|
|
818
|
+
return true;
|
|
819
|
+
const top = err;
|
|
820
|
+
if (top?.name === "TypeError" && /header/i.test(top.message ?? ""))
|
|
821
|
+
return true;
|
|
822
|
+
const unsent = (x) => {
|
|
823
|
+
const code = x?.code;
|
|
824
|
+
return typeof code === "string" && UNSENT_CODES.test(code);
|
|
825
|
+
};
|
|
826
|
+
let c = err;
|
|
827
|
+
for (let depth = 0; c !== null && typeof c === "object" && depth < 6; depth++) {
|
|
828
|
+
if (unsent(c))
|
|
829
|
+
return true;
|
|
830
|
+
const each = c.errors;
|
|
831
|
+
if (Array.isArray(each) && each.length > 0 && each.every(unsent))
|
|
832
|
+
return true;
|
|
833
|
+
c = c.cause;
|
|
834
|
+
}
|
|
835
|
+
return false;
|
|
836
|
+
}
|
|
837
|
+
class HeldDrop extends Error {
|
|
838
|
+
key;
|
|
839
|
+
constructor(key, why) {
|
|
840
|
+
super(why);
|
|
841
|
+
this.key = key;
|
|
842
|
+
this.name = "HeldDrop";
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
function sleepUnless(ms, signal) {
|
|
846
|
+
if (signal === undefined)
|
|
847
|
+
return sleep(ms);
|
|
848
|
+
return new Promise((resolve) => {
|
|
849
|
+
if (signal.aborted) {
|
|
850
|
+
resolve();
|
|
851
|
+
return;
|
|
852
|
+
}
|
|
853
|
+
const done = () => { clearTimeout(t); signal.removeEventListener("abort", done); resolve(); };
|
|
854
|
+
const t = setTimeout(done, ms);
|
|
855
|
+
signal.addEventListener("abort", done, { once: true });
|
|
856
|
+
});
|
|
857
|
+
}
|
|
858
|
+
function reattachWait(attempt, retryAfterMs) {
|
|
859
|
+
if (retryAfterMs !== undefined)
|
|
860
|
+
return Math.min(retryAfterMs, REATTACH_RETRY_AFTER_MAX_MS);
|
|
861
|
+
return Math.min(REATTACH_BACKOFF_MAX_MS, REATTACH_BACKOFF_MS * 2 ** Math.min(attempt, 8)) * (0.5 + Math.random() * 0.5);
|
|
862
|
+
}
|
|
863
|
+
function stoppedUnattended(answer) {
|
|
864
|
+
return answer?.reason === "not_reattached";
|
|
865
|
+
}
|
|
866
|
+
function abortedByCaller(method, path) {
|
|
867
|
+
return new TimeoutError(`request ${method} ${path} was aborted by the caller's signal`, "Nothing to fix: the caller aborted it.");
|
|
868
|
+
}
|
|
775
869
|
const POLLING_TICK_MS = 5;
|
|
776
870
|
const PARKED_TICK_MS = 50;
|
|
777
871
|
async function keepingLoopAwake(everyMs, fn) {
|
|
@@ -790,6 +884,21 @@ const WAIT_MS_CAP = 30_000;
|
|
|
790
884
|
export const LONG_POLL_MS = 5_000;
|
|
791
885
|
const BUILD_POLL_MS = 10_000;
|
|
792
886
|
const PROGRESS_POLL_MS = 200;
|
|
887
|
+
export const RUN_SETTLE_MS = 5_000;
|
|
888
|
+
export function runWorkDone(run, endedSince, now = Date.now()) {
|
|
889
|
+
if (run.state === "completed" || run.state === "cap_killed" || run.state === "aborted")
|
|
890
|
+
return true;
|
|
891
|
+
const sbs = Array.isArray(run.sandboxes) ? run.sandboxes : [];
|
|
892
|
+
const allDone = sbs.length > 0 && sbs.every((s) => s.state === "ended" || s.command_outcome != null);
|
|
893
|
+
if (!allDone) {
|
|
894
|
+
delete endedSince.at;
|
|
895
|
+
return false;
|
|
896
|
+
}
|
|
897
|
+
if (sbs.some((s) => s.state !== "ended"))
|
|
898
|
+
return true;
|
|
899
|
+
endedSince.at ??= now;
|
|
900
|
+
return now - endedSince.at >= RUN_SETTLE_MS;
|
|
901
|
+
}
|
|
793
902
|
const WAITED_HEADER = "promigence-waited-ms";
|
|
794
903
|
const seg = (s) => encodeURIComponent(s).replace(/%40/g, "@");
|
|
795
904
|
function waitQuery(waitMs) {
|
|
@@ -827,6 +936,9 @@ export class PromigenceClient {
|
|
|
827
936
|
apiUrl;
|
|
828
937
|
#apiKey;
|
|
829
938
|
#keyRefusal;
|
|
939
|
+
#keyOrigin;
|
|
940
|
+
#onKeyUse;
|
|
941
|
+
#keyUseSaid = false;
|
|
830
942
|
#bearer;
|
|
831
943
|
fetchImpl;
|
|
832
944
|
ownFetch;
|
|
@@ -848,6 +960,8 @@ export class PromigenceClient {
|
|
|
848
960
|
const resolved = resolveCredentials(opts);
|
|
849
961
|
this.#apiKey = resolved.apiKey;
|
|
850
962
|
this.#keyRefusal = resolved.refusal;
|
|
963
|
+
this.#keyOrigin = resolved.origin;
|
|
964
|
+
this.#onKeyUse = opts.onKeyUse;
|
|
851
965
|
this.apiUrl = resolved.apiUrl;
|
|
852
966
|
const env = opts.fetch === undefined ? transportEnv() : undefined;
|
|
853
967
|
const proxy = env !== undefined ? proxyFor(this.apiUrl, env) : undefined;
|
|
@@ -913,11 +1027,18 @@ export class PromigenceClient {
|
|
|
913
1027
|
catch {
|
|
914
1028
|
}
|
|
915
1029
|
}
|
|
1030
|
+
get keyOrigin() {
|
|
1031
|
+
return this.#keyOrigin;
|
|
1032
|
+
}
|
|
916
1033
|
requireKey() {
|
|
917
1034
|
if (this.#keyRefusal)
|
|
918
1035
|
throw this.#keyRefusal;
|
|
919
1036
|
if (!this.#apiKey)
|
|
920
|
-
throw new AuthError();
|
|
1037
|
+
throw this.cliErrors ? new AuthError(NO_KEY_MESSAGE) : new AuthError(NO_KEY_MESSAGE, NO_KEY_FIX);
|
|
1038
|
+
if (!this.#keyUseSaid && this.#onKeyUse !== undefined && this.#keyOrigin !== undefined) {
|
|
1039
|
+
this.#keyUseSaid = true;
|
|
1040
|
+
this.#onKeyUse(this.#keyOrigin);
|
|
1041
|
+
}
|
|
921
1042
|
return this.#apiKey;
|
|
922
1043
|
}
|
|
923
1044
|
bearer() {
|
|
@@ -965,6 +1086,82 @@ export class PromigenceClient {
|
|
|
965
1086
|
await sleep(backoff * (0.5 + Math.random() * 0.5));
|
|
966
1087
|
return true;
|
|
967
1088
|
}
|
|
1089
|
+
async heldExec(sandboxId, req) {
|
|
1090
|
+
const signal = req.signal;
|
|
1091
|
+
const abortedBefore = signal?.aborted === true;
|
|
1092
|
+
let key;
|
|
1093
|
+
try {
|
|
1094
|
+
let res;
|
|
1095
|
+
try {
|
|
1096
|
+
res = await this.request({ ...req, held: { onKey: (k) => { key = k; } } });
|
|
1097
|
+
}
|
|
1098
|
+
catch (err) {
|
|
1099
|
+
if (!(err instanceof HeldDrop))
|
|
1100
|
+
throw err;
|
|
1101
|
+
res = await this.reattachBuffered(sandboxId, err.key, req);
|
|
1102
|
+
}
|
|
1103
|
+
if (stoppedUnattended(res)) {
|
|
1104
|
+
throw new ExecLostError(`the command on sandbox ${sandboxId} was stopped: its connection to the service dropped and no client came back for it in time`, { sandboxId }, "not_reattached");
|
|
1105
|
+
}
|
|
1106
|
+
return res;
|
|
1107
|
+
}
|
|
1108
|
+
catch (err) {
|
|
1109
|
+
if (key !== undefined && signal?.aborted && !abortedBefore)
|
|
1110
|
+
await this.cancelHeldExec(sandboxId, key);
|
|
1111
|
+
throw err;
|
|
1112
|
+
}
|
|
1113
|
+
}
|
|
1114
|
+
async reattachBuffered(sandboxId, key, req) {
|
|
1115
|
+
const budgetMs = EXEC_HOLD_DEFAULT_S * 1000 + REATTACH_SLACK_MS;
|
|
1116
|
+
let droppedAt = Date.now();
|
|
1117
|
+
for (let attempt = 0;; attempt++) {
|
|
1118
|
+
if (req.signal?.aborted)
|
|
1119
|
+
throw abortedByCaller(req.method, req.path);
|
|
1120
|
+
this.stats.retries++;
|
|
1121
|
+
const sentAt = Date.now();
|
|
1122
|
+
try {
|
|
1123
|
+
return await this.request({
|
|
1124
|
+
method: "POST",
|
|
1125
|
+
path: `/sandboxes/${encodeURIComponent(sandboxId)}/execs/attach`,
|
|
1126
|
+
body: { key },
|
|
1127
|
+
once: true,
|
|
1128
|
+
...(req.timeoutMs === undefined ? {} : { timeoutMs: req.timeoutMs }),
|
|
1129
|
+
...(req.signal === undefined ? {} : { signal: req.signal }),
|
|
1130
|
+
});
|
|
1131
|
+
}
|
|
1132
|
+
catch (err) {
|
|
1133
|
+
if (req.signal?.aborted)
|
|
1134
|
+
throw abortedByCaller(req.method, req.path);
|
|
1135
|
+
const status = err instanceof PromigenceError ? err.status : undefined;
|
|
1136
|
+
if (status === 404) {
|
|
1137
|
+
throw new ExecLostError(`the connection to the service dropped before the command on sandbox ${sandboxId} answered, and it could not be picked up again: nothing is kept for it (it ended, it never started, or this service cannot keep it)`, { sandboxId }, "gone");
|
|
1138
|
+
}
|
|
1139
|
+
const lostConnection = status === undefined && (err instanceof UnreachableError || err instanceof TimeoutError);
|
|
1140
|
+
if (!lostConnection && !(status !== undefined && REATTACH_AGAIN.has(status)))
|
|
1141
|
+
throw err;
|
|
1142
|
+
if (Date.now() - sentAt >= REATTACH_TAKEN_MS)
|
|
1143
|
+
droppedAt = Date.now();
|
|
1144
|
+
const left = budgetMs - (Date.now() - droppedAt);
|
|
1145
|
+
if (left <= 0) {
|
|
1146
|
+
throw new ExecLostError(`the connection to the service dropped while the command on sandbox ${sandboxId} was running, and the service could not be reached again in time: the command is stopped ${EXEC_HOLD_DEFAULT_S} s after the drop`, { sandboxId }, "unreachable");
|
|
1147
|
+
}
|
|
1148
|
+
await sleepUnless(Math.min(left, reattachWait(attempt, err instanceof PromigenceError ? err.retryAfterMs : undefined)), req.signal);
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1152
|
+
async cancelHeldExec(sandboxId, key) {
|
|
1153
|
+
try {
|
|
1154
|
+
await this.request({
|
|
1155
|
+
method: "POST",
|
|
1156
|
+
path: `/sandboxes/${encodeURIComponent(sandboxId)}/execs/cancel`,
|
|
1157
|
+
body: { key },
|
|
1158
|
+
timeoutMs: EXEC_CANCEL_TIMEOUT_MS,
|
|
1159
|
+
once: true,
|
|
1160
|
+
});
|
|
1161
|
+
}
|
|
1162
|
+
catch {
|
|
1163
|
+
}
|
|
1164
|
+
}
|
|
968
1165
|
async request(opts) {
|
|
969
1166
|
const tr = traceBegin(opts.method, opts.path);
|
|
970
1167
|
const url = `${this.apiUrl}${opts.path}`;
|
|
@@ -995,9 +1192,16 @@ export class PromigenceClient {
|
|
|
995
1192
|
const timeoutMs = opts.timeoutMs ?? this.requestTimeoutMs;
|
|
996
1193
|
let dispatcher;
|
|
997
1194
|
let resentDelete = false;
|
|
1195
|
+
const maxRetries = opts.once === true ? 0 : this.maxRetries;
|
|
1196
|
+
let execKey;
|
|
998
1197
|
for (let attempt = 0;; attempt++) {
|
|
999
1198
|
let res;
|
|
1000
1199
|
this.stats.requests++;
|
|
1200
|
+
if (opts.held !== undefined) {
|
|
1201
|
+
execKey = newExecKey();
|
|
1202
|
+
headers[EXEC_KEY_HEADER] = execKey;
|
|
1203
|
+
opts.held.onKey(execKey);
|
|
1204
|
+
}
|
|
1001
1205
|
const sentAt = performance.now();
|
|
1002
1206
|
try {
|
|
1003
1207
|
if (this.h2) {
|
|
@@ -1040,11 +1244,14 @@ export class PromigenceClient {
|
|
|
1040
1244
|
}
|
|
1041
1245
|
catch (err) {
|
|
1042
1246
|
const e = err;
|
|
1247
|
+
if (execKey !== undefined && !opts.signal?.aborted && !provedUnsent(err)) {
|
|
1248
|
+
throw new HeldDrop(execKey, transportDetail(e));
|
|
1249
|
+
}
|
|
1043
1250
|
if (keyedCreate && idempotencyKey && await this.againKeyed(attempt, opts.signal))
|
|
1044
1251
|
continue;
|
|
1045
1252
|
const cutOff = e?.cause?.code === "UND_ERR_HEADERS_TIMEOUT" || e?.cause?.code === "UND_ERR_BODY_TIMEOUT";
|
|
1046
1253
|
const timedOut = e?.name === "TimeoutError" || e?.name === "AbortError" || cutOff;
|
|
1047
|
-
if (!timedOut && replayable && attempt <
|
|
1254
|
+
if (!timedOut && replayable && attempt < maxRetries && !opts.signal?.aborted) {
|
|
1048
1255
|
this.stats.retries++;
|
|
1049
1256
|
if (opts.method === "DELETE")
|
|
1050
1257
|
resentDelete = true;
|
|
@@ -1052,9 +1259,8 @@ export class PromigenceClient {
|
|
|
1052
1259
|
await sleep(backoff * (0.5 + Math.random() * 0.5));
|
|
1053
1260
|
continue;
|
|
1054
1261
|
}
|
|
1055
|
-
if (opts.signal?.aborted)
|
|
1056
|
-
throw
|
|
1057
|
-
}
|
|
1262
|
+
if (opts.signal?.aborted)
|
|
1263
|
+
throw abortedByCaller(opts.method, opts.path);
|
|
1058
1264
|
if (e?.name === "TimeoutError" || e?.name === "AbortError") {
|
|
1059
1265
|
throw new TimeoutError(`request ${opts.method} ${opts.path} timed out after ${timeoutMs} ms`, "Retry. To allow longer: promigence --request-timeout 120s <command>, " +
|
|
1060
1266
|
"PROMIGENCE_REQUEST_TIMEOUT=120s, or requestTimeoutMs in the SDK. A --wait is bounded " +
|
|
@@ -1071,6 +1277,11 @@ export class PromigenceClient {
|
|
|
1071
1277
|
const gateway = (res.status === 502 || res.status === 504) && replayable;
|
|
1072
1278
|
let retryable = (res.status === 429 || res.status === 503 || gateway) && (retryAfterMs ?? 0) <= MAX_RETRY_SLEEP_MS;
|
|
1073
1279
|
let body;
|
|
1280
|
+
if (execKey !== undefined && (res.status === 502 || res.status === 504)) {
|
|
1281
|
+
body = await res.text().catch(() => "");
|
|
1282
|
+
if (errorCodeOf(body) === undefined)
|
|
1283
|
+
throw new HeldDrop(execKey, `HTTP ${res.status}`);
|
|
1284
|
+
}
|
|
1074
1285
|
if (retryable && res.status === 503 && !replayable && retryAfterMs === undefined) {
|
|
1075
1286
|
body = await res.text().catch(() => "");
|
|
1076
1287
|
retryable = REFUSED_503_CODES.has(errorCodeOf(body) ?? "");
|
|
@@ -1079,7 +1290,7 @@ export class PromigenceClient {
|
|
|
1079
1290
|
body = await res.text().catch(() => "");
|
|
1080
1291
|
retryable = errorCodeOf(body) === undefined;
|
|
1081
1292
|
}
|
|
1082
|
-
if (retryable && attempt <
|
|
1293
|
+
if (retryable && attempt < maxRetries) {
|
|
1083
1294
|
const backoff = Math.min(MAX_RETRY_SLEEP_MS, this.retryBaseMs * 2 ** attempt);
|
|
1084
1295
|
const jitter = Math.random();
|
|
1085
1296
|
const waitMs = retryAfterMs !== undefined
|
|
@@ -1112,8 +1323,10 @@ export class PromigenceClient {
|
|
|
1112
1323
|
text = "";
|
|
1113
1324
|
else {
|
|
1114
1325
|
const e = err;
|
|
1326
|
+
if (execKey !== undefined && !opts.signal?.aborted)
|
|
1327
|
+
throw new HeldDrop(execKey, transportDetail(e));
|
|
1115
1328
|
const timedOut = e?.name === "TimeoutError" || e?.name === "AbortError";
|
|
1116
|
-
if (!timedOut && replayable && attempt <
|
|
1329
|
+
if (!timedOut && replayable && attempt < maxRetries && !opts.signal?.aborted) {
|
|
1117
1330
|
this.stats.retries++;
|
|
1118
1331
|
if (opts.method === "DELETE")
|
|
1119
1332
|
resentDelete = true;
|
|
@@ -1239,10 +1452,12 @@ export class PromigenceClient {
|
|
|
1239
1452
|
continue;
|
|
1240
1453
|
}
|
|
1241
1454
|
const code = payload.error?.code ?? codeForStatus(res.status);
|
|
1455
|
+
const rid = headerOf(res, "request-id") ?? headerOf(res, "x-request-id");
|
|
1242
1456
|
throw errorFor(code, payload.error?.message ?? `HTTP ${res.status} from ${method} ${path}`, payload.error?.fix ?? "See the API response for details.", {
|
|
1243
1457
|
status: res.status,
|
|
1244
1458
|
...bodyExtra(payload),
|
|
1245
1459
|
...(retryAfterMs === undefined ? {} : { retryAfterMs }),
|
|
1460
|
+
...(rid ? { requestId: rid } : {}),
|
|
1246
1461
|
});
|
|
1247
1462
|
}
|
|
1248
1463
|
}
|
|
@@ -1322,10 +1537,12 @@ export class PromigenceClient {
|
|
|
1322
1537
|
continue;
|
|
1323
1538
|
}
|
|
1324
1539
|
const code = payload.error?.code ?? codeForStatus(res.status);
|
|
1540
|
+
const rid = headerOf(res, "request-id") ?? headerOf(res, "x-request-id");
|
|
1325
1541
|
throw errorFor(code, payload.error?.message ?? `HTTP ${res.status} from ${method} ${path}`, payload.error?.fix ?? "See the API response for details.", {
|
|
1326
1542
|
status: res.status,
|
|
1327
1543
|
...bodyExtra(payload),
|
|
1328
1544
|
...(retryAfterMs === undefined ? {} : { retryAfterMs }),
|
|
1545
|
+
...(rid ? { requestId: rid } : {}),
|
|
1329
1546
|
});
|
|
1330
1547
|
}
|
|
1331
1548
|
}
|
|
@@ -1486,6 +1703,7 @@ export class PromigenceClient {
|
|
|
1486
1703
|
method: "PATCH", path: `/orgs/${orgSeg(org)}/projects/${encodeURIComponent(project)}`, body: projectUpdateBody(update),
|
|
1487
1704
|
}),
|
|
1488
1705
|
},
|
|
1706
|
+
update: async (org, update) => this.request({ method: "PATCH", path: `/orgs/${orgSeg(org)}`, body: orgUpdateBody(update) }),
|
|
1489
1707
|
};
|
|
1490
1708
|
}
|
|
1491
1709
|
group_credits() {
|
|
@@ -1565,6 +1783,8 @@ export class PromigenceClient {
|
|
|
1565
1783
|
body["network"] = opts.network;
|
|
1566
1784
|
if (opts.optimizeFor !== undefined)
|
|
1567
1785
|
body["optimize_for"] = opts.optimizeFor;
|
|
1786
|
+
if (opts.capture !== undefined)
|
|
1787
|
+
body["capture"] = opts.capture;
|
|
1568
1788
|
if (opts.secrets?.length)
|
|
1569
1789
|
body["secrets"] = opts.secrets;
|
|
1570
1790
|
if (opts.labels && Object.keys(opts.labels).length)
|
|
@@ -1701,12 +1921,8 @@ export class PromigenceClient {
|
|
|
1701
1921
|
return { sandboxes: r.value, timedOut: r.timedOut, waitedMs: r.waitedMs };
|
|
1702
1922
|
},
|
|
1703
1923
|
waitDone: async (runId, opts = {}) => {
|
|
1704
|
-
const
|
|
1705
|
-
|
|
1706
|
-
const allDone = Array.isArray(run.sandboxes) && run.sandboxes.length > 0 &&
|
|
1707
|
-
run.sandboxes.every((s) => s.state === "ended" || s.command_outcome != null);
|
|
1708
|
-
return terminal || allDone;
|
|
1709
|
-
});
|
|
1924
|
+
const endedSince = {};
|
|
1925
|
+
const r = await this.waitLoop(opts, LONG_POLL_MS, { run_id: runId, state: "running" }, (read) => this.runs.get(runId, read), (run) => runWorkDone(run, endedSince));
|
|
1710
1926
|
return { run: r.value, timedOut: r.timedOut, waitedMs: r.waitedMs };
|
|
1711
1927
|
},
|
|
1712
1928
|
};
|
|
@@ -1802,7 +2018,9 @@ export class PromigenceClient {
|
|
|
1802
2018
|
req.idempotencyKey = opts.idempotencyKey;
|
|
1803
2019
|
if (opts.signal)
|
|
1804
2020
|
req.signal = opts.signal;
|
|
1805
|
-
|
|
2021
|
+
if (opts.terminal)
|
|
2022
|
+
return this.request(req);
|
|
2023
|
+
return this.heldExec(sandboxId, req);
|
|
1806
2024
|
},
|
|
1807
2025
|
execStream: async (sandboxId, opts, on = {}) => {
|
|
1808
2026
|
if (!opts.argv?.length && !opts.sh) {
|
|
@@ -1831,7 +2049,9 @@ export class PromigenceClient {
|
|
|
1831
2049
|
body["timeout_ms"] = commandTimeoutMs(opts.timeoutMs) ?? 15 * 60_000;
|
|
1832
2050
|
body["max_output_bytes"] = opts.maxOutputBytes ?? MAX_OUTPUT_BYTES_DEFAULT;
|
|
1833
2051
|
const url = `${this.apiUrl}/sandboxes/${encodeURIComponent(sandboxId)}/execs`;
|
|
1834
|
-
const
|
|
2052
|
+
const keyed = opts.terminal !== true;
|
|
2053
|
+
let key;
|
|
2054
|
+
let idle = new AbortController();
|
|
1835
2055
|
let silent = false;
|
|
1836
2056
|
let reader;
|
|
1837
2057
|
let timer;
|
|
@@ -1843,23 +2063,33 @@ export class PromigenceClient {
|
|
|
1843
2063
|
reader?.cancel().catch(() => { });
|
|
1844
2064
|
}, STREAM_IDLE_MS);
|
|
1845
2065
|
};
|
|
2066
|
+
const fresh = () => { idle = new AbortController(); silent = false; reader = undefined; };
|
|
2067
|
+
const abortedBefore = opts.signal?.aborted === true;
|
|
2068
|
+
const abandon = async (err) => {
|
|
2069
|
+
if (key !== undefined && !abortedBefore)
|
|
2070
|
+
await this.cancelHeldExec(sandboxId, key);
|
|
2071
|
+
throw err;
|
|
2072
|
+
};
|
|
1846
2073
|
let stream;
|
|
1847
2074
|
const bearer = `Bearer ${this.requireKey()}`;
|
|
1848
2075
|
const dispatcher = this.guardDispatcher();
|
|
2076
|
+
const headers = () => ({
|
|
2077
|
+
accept: "text/event-stream",
|
|
2078
|
+
"content-type": "application/json",
|
|
2079
|
+
"user-agent": this.userAgent,
|
|
2080
|
+
authorization: bearer,
|
|
2081
|
+
...(this.caller ? { [CALLER_HEADER]: this.caller } : {}),
|
|
2082
|
+
});
|
|
1849
2083
|
for (let attempt = 0; stream === undefined; attempt++) {
|
|
1850
2084
|
let r;
|
|
1851
2085
|
this.stats.requests++;
|
|
2086
|
+
if (keyed)
|
|
2087
|
+
key = newExecKey();
|
|
1852
2088
|
arm();
|
|
1853
2089
|
try {
|
|
1854
2090
|
r = await this.fetchImpl(url, {
|
|
1855
2091
|
method: "POST",
|
|
1856
|
-
headers: {
|
|
1857
|
-
accept: "text/event-stream",
|
|
1858
|
-
"content-type": "application/json",
|
|
1859
|
-
"user-agent": this.userAgent,
|
|
1860
|
-
authorization: bearer,
|
|
1861
|
-
...(this.caller ? { [CALLER_HEADER]: this.caller } : {}),
|
|
1862
|
-
},
|
|
2092
|
+
headers: { ...headers(), ...(key === undefined ? {} : { [EXEC_KEY_HEADER]: key }) },
|
|
1863
2093
|
body: JSON.stringify(body),
|
|
1864
2094
|
signal: opts.signal ? AbortSignal.any([opts.signal, idle.signal]) : idle.signal,
|
|
1865
2095
|
...(dispatcher === undefined ? {} : { dispatcher }),
|
|
@@ -1869,7 +2099,10 @@ export class PromigenceClient {
|
|
|
1869
2099
|
clearTimeout(timer);
|
|
1870
2100
|
const e = err;
|
|
1871
2101
|
const why = silent ? `no answer in ${STREAM_IDLE_MS / 1000} s` : transportDetail(e);
|
|
1872
|
-
|
|
2102
|
+
const failed = new UnreachableError(`cannot reach ${url}: ${why}`);
|
|
2103
|
+
if (opts.signal?.aborted)
|
|
2104
|
+
return abandon(failed);
|
|
2105
|
+
throw failed;
|
|
1873
2106
|
}
|
|
1874
2107
|
if (r.ok && r.body) {
|
|
1875
2108
|
stream = r.body;
|
|
@@ -1900,87 +2133,166 @@ export class PromigenceClient {
|
|
|
1900
2133
|
const errOut = [];
|
|
1901
2134
|
let exit;
|
|
1902
2135
|
let failure;
|
|
1903
|
-
|
|
1904
|
-
|
|
2136
|
+
let holdS;
|
|
2137
|
+
const received = { stdout: 0, stderr: 0 };
|
|
1905
2138
|
const lost = (how) => new UnreachableError(`the exec stream for sandbox ${sandboxId} ${how}`, "The connection dropped before the command finished; its result is unknown. " +
|
|
1906
2139
|
`Read it by id (\`promigence exec output <exec_id> --sandbox ${hintArg(sandboxId)}\`) or re-run.`);
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
2140
|
+
const consume = async (from) => {
|
|
2141
|
+
const dec = new TextDecoder();
|
|
2142
|
+
let buf = "";
|
|
2143
|
+
const rd = from.getReader();
|
|
2144
|
+
reader = rd;
|
|
2145
|
+
try {
|
|
2146
|
+
for (;;) {
|
|
2147
|
+
arm();
|
|
2148
|
+
let chunk;
|
|
2149
|
+
try {
|
|
2150
|
+
chunk = await rd.read();
|
|
2151
|
+
}
|
|
2152
|
+
catch (err) {
|
|
2153
|
+
if (opts.signal?.aborted)
|
|
2154
|
+
return abandon(err);
|
|
2155
|
+
if (holdS !== undefined && (exit || failure))
|
|
2156
|
+
return { done: true };
|
|
2157
|
+
if (silent)
|
|
2158
|
+
return { done: false, why: `was silent for ${STREAM_IDLE_MS / 1000} s`, restart: false };
|
|
2159
|
+
const e = err;
|
|
2160
|
+
return { done: false, why: `was cut off (${e?.cause?.code ?? e?.message ?? "connection lost"})`, restart: false };
|
|
2161
|
+
}
|
|
2162
|
+
if (chunk.done)
|
|
2163
|
+
break;
|
|
2164
|
+
const value = chunk.value;
|
|
2165
|
+
buf += dec.decode(value, { stream: true });
|
|
2166
|
+
let sep;
|
|
2167
|
+
while ((sep = buf.indexOf("\n\n")) >= 0) {
|
|
2168
|
+
const block = buf.slice(0, sep);
|
|
2169
|
+
buf = buf.slice(sep + 2);
|
|
2170
|
+
let event = "";
|
|
2171
|
+
let data = "";
|
|
2172
|
+
for (const line of block.split("\n")) {
|
|
2173
|
+
if (line.startsWith("event:"))
|
|
2174
|
+
event = line.slice(6).trim();
|
|
2175
|
+
else if (line.startsWith("data:"))
|
|
2176
|
+
data = line.slice(5).replace(/^ /, "");
|
|
2177
|
+
}
|
|
2178
|
+
if (!event)
|
|
2179
|
+
continue;
|
|
2180
|
+
if (event === "heartbeat") {
|
|
2181
|
+
on.heartbeat?.();
|
|
2182
|
+
continue;
|
|
2183
|
+
}
|
|
2184
|
+
if (event === "reconnect") {
|
|
2185
|
+
rd.cancel().catch(() => { });
|
|
2186
|
+
return { done: false, why: "was ended by a service restart", restart: true };
|
|
2187
|
+
}
|
|
2188
|
+
let parsed = {};
|
|
2189
|
+
try {
|
|
2190
|
+
parsed = JSON.parse(data);
|
|
2191
|
+
}
|
|
2192
|
+
catch { }
|
|
2193
|
+
if (event === "held") {
|
|
2194
|
+
const h = parsed["hold_s"];
|
|
2195
|
+
holdS = typeof h === "number" && Number.isFinite(h) && h >= 0 ? h : EXEC_HOLD_DEFAULT_S;
|
|
2196
|
+
}
|
|
2197
|
+
else if (event === "stdout" || event === "stderr") {
|
|
2198
|
+
const bytes = b64ToBytes(String(parsed["data"] ?? ""));
|
|
2199
|
+
if (event === "stdout") {
|
|
2200
|
+
received.stdout += bytes.length;
|
|
2201
|
+
out.push(bytes);
|
|
2202
|
+
on.stdout?.(bytes);
|
|
2203
|
+
}
|
|
2204
|
+
else {
|
|
2205
|
+
received.stderr += bytes.length;
|
|
2206
|
+
errOut.push(bytes);
|
|
2207
|
+
on.stderr?.(bytes);
|
|
2208
|
+
}
|
|
2209
|
+
}
|
|
2210
|
+
else if (event === "exit") {
|
|
2211
|
+
exit = { ...(exit ?? {}), ...parsed };
|
|
2212
|
+
}
|
|
2213
|
+
else if (event === "error") {
|
|
2214
|
+
failure = parsed;
|
|
2215
|
+
}
|
|
2216
|
+
}
|
|
2217
|
+
}
|
|
2218
|
+
}
|
|
2219
|
+
finally {
|
|
2220
|
+
clearTimeout(timer);
|
|
2221
|
+
}
|
|
2222
|
+
if (exit || failure)
|
|
2223
|
+
return { done: true };
|
|
2224
|
+
if (silent)
|
|
2225
|
+
return { done: false, why: `was silent for ${STREAM_IDLE_MS / 1000} s`, restart: false };
|
|
2226
|
+
return { done: false, why: "ended without an exit event", restart: false };
|
|
2227
|
+
};
|
|
2228
|
+
const reattach = async () => {
|
|
2229
|
+
const droppedAt = Date.now();
|
|
2230
|
+
const hold = holdS ?? EXEC_HOLD_DEFAULT_S;
|
|
2231
|
+
const budgetMs = hold * 1000 + REATTACH_SLACK_MS;
|
|
2232
|
+
for (let attempt = 0;; attempt++) {
|
|
2233
|
+
if (opts.signal?.aborted)
|
|
2234
|
+
return abandon(opts.signal.reason);
|
|
2235
|
+
fresh();
|
|
2236
|
+
this.stats.requests++;
|
|
2237
|
+
this.stats.retries++;
|
|
1910
2238
|
arm();
|
|
1911
|
-
let
|
|
2239
|
+
let retryAfterMs;
|
|
2240
|
+
let r;
|
|
1912
2241
|
try {
|
|
1913
|
-
|
|
2242
|
+
r = await this.fetchImpl(`${url}/attach`, {
|
|
2243
|
+
method: "POST",
|
|
2244
|
+
headers: headers(),
|
|
2245
|
+
body: JSON.stringify({ key, stdout_offset: received.stdout, stderr_offset: received.stderr }),
|
|
2246
|
+
signal: opts.signal ? AbortSignal.any([opts.signal, idle.signal]) : idle.signal,
|
|
2247
|
+
...(dispatcher === undefined ? {} : { dispatcher }),
|
|
2248
|
+
});
|
|
1914
2249
|
}
|
|
1915
2250
|
catch (err) {
|
|
2251
|
+
clearTimeout(timer);
|
|
1916
2252
|
if (opts.signal?.aborted)
|
|
1917
|
-
|
|
1918
|
-
if (silent)
|
|
1919
|
-
throw lost(`was silent for ${STREAM_IDLE_MS / 1000} s`);
|
|
1920
|
-
const e = err;
|
|
1921
|
-
throw lost(`was cut off (${e?.cause?.code ?? e?.message ?? "connection lost"})`);
|
|
2253
|
+
return abandon(err);
|
|
1922
2254
|
}
|
|
1923
|
-
if (
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
const block = buf.slice(0, sep);
|
|
1930
|
-
buf = buf.slice(sep + 2);
|
|
1931
|
-
let event = "";
|
|
1932
|
-
let data = "";
|
|
1933
|
-
for (const line of block.split("\n")) {
|
|
1934
|
-
if (line.startsWith("event:"))
|
|
1935
|
-
event = line.slice(6).trim();
|
|
1936
|
-
else if (line.startsWith("data:"))
|
|
1937
|
-
data = line.slice(5).replace(/^ /, "");
|
|
1938
|
-
}
|
|
1939
|
-
if (!event)
|
|
1940
|
-
continue;
|
|
1941
|
-
if (event === "heartbeat") {
|
|
1942
|
-
on.heartbeat?.();
|
|
1943
|
-
continue;
|
|
1944
|
-
}
|
|
1945
|
-
if (event === "reconnect") {
|
|
1946
|
-
reader.cancel().catch(() => { });
|
|
1947
|
-
throw new ServiceRestartError(`a service restart stopped the command on sandbox ${sandboxId} while its output was streaming`, "Not billed: the time the command ran is credited back. The sandbox kept running; the command may have done part of its work before it stopped: run it again if that is safe.");
|
|
1948
|
-
}
|
|
1949
|
-
let parsed = {};
|
|
2255
|
+
if (r !== undefined) {
|
|
2256
|
+
if (r.ok && r.body)
|
|
2257
|
+
return r.body;
|
|
2258
|
+
clearTimeout(timer);
|
|
2259
|
+
const text = await r.text().catch(() => "");
|
|
2260
|
+
let payload = {};
|
|
1950
2261
|
try {
|
|
1951
|
-
|
|
2262
|
+
payload = text ? JSON.parse(text) : {};
|
|
1952
2263
|
}
|
|
1953
2264
|
catch { }
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
out.push(bytes);
|
|
1958
|
-
on.stdout?.(bytes);
|
|
1959
|
-
}
|
|
1960
|
-
else {
|
|
1961
|
-
errOut.push(bytes);
|
|
1962
|
-
on.stderr?.(bytes);
|
|
1963
|
-
}
|
|
2265
|
+
retryAfterMs = parseRetryAfter(r.headers.get("retry-after"));
|
|
2266
|
+
if (r.status === 404) {
|
|
2267
|
+
throw new ExecLostError(`the connection to the service dropped while the command on sandbox ${sandboxId} was running, and it could not be picked up again: it is no longer kept (it ended, or this service cannot keep it)`, { sandboxId }, "gone");
|
|
1964
2268
|
}
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
}
|
|
1968
|
-
else if (event === "error") {
|
|
1969
|
-
failure = parsed;
|
|
2269
|
+
if (!REATTACH_AGAIN.has(r.status)) {
|
|
2270
|
+
throw errorFor(payload.error?.code ?? codeForStatus(r.status), payload.error?.message ?? `HTTP ${r.status} from POST /sandboxes/${sandboxId}/execs/attach`, payload.error?.fix ?? "See the API response for details.", { status: r.status, ...bodyExtra(payload), ...(retryAfterMs === undefined ? {} : { retryAfterMs }) });
|
|
1970
2271
|
}
|
|
1971
2272
|
}
|
|
2273
|
+
const left = budgetMs - (Date.now() - droppedAt);
|
|
2274
|
+
if (left <= 0) {
|
|
2275
|
+
throw new ExecLostError(`the connection to the service dropped while the command on sandbox ${sandboxId} was running, and the service could not be reached again in time: the command is stopped ${hold} s after the drop`, { sandboxId }, "unreachable");
|
|
2276
|
+
}
|
|
2277
|
+
await sleepUnless(Math.min(left, reattachWait(attempt, retryAfterMs)), opts.signal);
|
|
1972
2278
|
}
|
|
1973
|
-
}
|
|
1974
|
-
|
|
1975
|
-
|
|
2279
|
+
};
|
|
2280
|
+
for (let ended = await consume(stream); !ended.done; ended = await consume(stream)) {
|
|
2281
|
+
if (holdS === undefined || key === undefined) {
|
|
2282
|
+
if (ended.restart) {
|
|
2283
|
+
throw new ServiceRestartError(`a service restart stopped the command on sandbox ${sandboxId} while its output was streaming`, "Not billed: the time the command ran is credited back. The sandbox kept running; the command may have done part of its work before it stopped: run it again if that is safe.");
|
|
2284
|
+
}
|
|
2285
|
+
throw lost(ended.why);
|
|
2286
|
+
}
|
|
2287
|
+
stream = await reattach();
|
|
1976
2288
|
}
|
|
1977
2289
|
if (failure) {
|
|
1978
2290
|
throw errorFor(failure.code ?? "internal", failure.message ?? "the exec stream failed", failure.fix ?? "Re-run; if it repeats, include the exec_id.", { status: 200, ...bodyExtra({ error: failure }) });
|
|
1979
2291
|
}
|
|
1980
|
-
if (!exit
|
|
1981
|
-
throw lost(`was silent for ${STREAM_IDLE_MS / 1000} s`);
|
|
1982
|
-
if (!exit) {
|
|
2292
|
+
if (!exit)
|
|
1983
2293
|
throw lost("ended without an exit event");
|
|
2294
|
+
if (stoppedUnattended(exit)) {
|
|
2295
|
+
throw new ExecLostError(`the command on sandbox ${sandboxId} was stopped: its connection to the service dropped and no client came back for it ${holdS === undefined ? "in time" : `within ${holdS} s`}`, { sandboxId }, "not_reattached");
|
|
1984
2296
|
}
|
|
1985
2297
|
const rawCode = exit["exit_code"];
|
|
1986
2298
|
const signal = exit["signal"];
|
|
@@ -2101,12 +2413,14 @@ export class PromigenceClient {
|
|
|
2101
2413
|
}),
|
|
2102
2414
|
},
|
|
2103
2415
|
ports: {
|
|
2104
|
-
expose: (sandboxId, port) => {
|
|
2416
|
+
expose: (sandboxId, port, opts = {}) => {
|
|
2105
2417
|
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
2106
2418
|
throw new InvalidArgumentError(`port must be an integer 1..65535, got ${port}`, "Pass the port your process listens on inside the sandbox, e.g. 3000.");
|
|
2107
2419
|
}
|
|
2420
|
+
checkPreviewLength(opts.length);
|
|
2108
2421
|
return withPreviewsFix(this.request({
|
|
2109
|
-
method: "POST", path: `/sandboxes/${encodeURIComponent(sandboxId)}/ports`,
|
|
2422
|
+
method: "POST", path: `/sandboxes/${encodeURIComponent(sandboxId)}/ports`,
|
|
2423
|
+
body: { port, ...(opts.length === undefined ? {} : { length: opts.length }) },
|
|
2110
2424
|
}), sandboxId, port);
|
|
2111
2425
|
},
|
|
2112
2426
|
list: async (sandboxId) => {
|
|
@@ -2115,10 +2429,19 @@ export class PromigenceClient {
|
|
|
2115
2429
|
}), sandboxId);
|
|
2116
2430
|
return r.ports ?? [];
|
|
2117
2431
|
},
|
|
2118
|
-
revoke: (sandboxId, tokenOrPort) =>
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2432
|
+
revoke: (sandboxId, tokenOrPort) => {
|
|
2433
|
+
const which = String(tokenOrPort).trim();
|
|
2434
|
+
return /^[0-9]+$/.test(which)
|
|
2435
|
+
? this.request({
|
|
2436
|
+
method: "DELETE",
|
|
2437
|
+
path: `/sandboxes/${encodeURIComponent(sandboxId)}/ports/${which}`,
|
|
2438
|
+
})
|
|
2439
|
+
: this.request({
|
|
2440
|
+
method: "POST",
|
|
2441
|
+
path: `/sandboxes/${encodeURIComponent(sandboxId)}/ports/revoke`,
|
|
2442
|
+
body: { token: which },
|
|
2443
|
+
});
|
|
2444
|
+
},
|
|
2122
2445
|
},
|
|
2123
2446
|
ssh: {
|
|
2124
2447
|
open: (sandboxId, opts) => {
|
|
@@ -2615,10 +2938,10 @@ export class PromigenceClient {
|
|
|
2615
2938
|
throw new InvalidArgumentError("snapshots.delete needs a snapshot", "Pass a snap_ id, an alias, or alias@commit7.");
|
|
2616
2939
|
return this.request({ method: "DELETE", path: `/snapshots/${seg(idOrAlias)}` });
|
|
2617
2940
|
},
|
|
2618
|
-
alias: (idOrAlias, alias) => this.request({
|
|
2941
|
+
alias: (idOrAlias, alias, opts = {}) => this.request({
|
|
2619
2942
|
method: "PUT",
|
|
2620
2943
|
path: `/snapshots/${seg(idOrAlias)}/alias`,
|
|
2621
|
-
body: { alias },
|
|
2944
|
+
body: { alias, ...(alias !== null && opts.moveAlias ? { move_alias: true } : {}) },
|
|
2622
2945
|
}),
|
|
2623
2946
|
waitBuild: async (buildId, opts = {}) => {
|
|
2624
2947
|
const r = await this.waitLoop(opts, BUILD_POLL_MS, { build_id: buildId, state: "running" }, (read) => this.snapshots.status(buildId, read), (b) => b.state !== "running" && b.state !== "queued");
|
|
@@ -2723,5 +3046,6 @@ export function toCommandResult(r) {
|
|
|
2723
3046
|
stderr: r.stderr ?? "",
|
|
2724
3047
|
...(r.truncated === undefined ? {} : { truncated: r.truncated }),
|
|
2725
3048
|
...(r.exec_id === undefined ? {} : { execId: r.exec_id }),
|
|
3049
|
+
timedOut: answerTimedOut(r),
|
|
2726
3050
|
};
|
|
2727
3051
|
}
|