@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/cjs/client.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.PromigenceClient = exports.LONG_POLL_MS = exports.isKeyedCreate = exports.REPO_CREDENTIALS_FIX = exports.CONTEXT_GIT_RE = exports.IMAGE_DIGEST_RE = exports.MAX_SOURCES = exports.NOTICE_HEADER = exports.OUTPUT_RETENTION_MAX_DAYS = exports.isWaiting = exports.FILE_TIMEOUT_MS = void 0;
|
|
3
|
+
exports.PromigenceClient = exports.RUN_SETTLE_MS = exports.LONG_POLL_MS = exports.isKeyedCreate = exports.REPO_CREDENTIALS_FIX = exports.CONTEXT_GIT_RE = exports.IMAGE_DIGEST_RE = exports.NO_KEY_FIX = exports.NO_KEY_MESSAGE = exports.MAX_SOURCES = exports.NOTICE_HEADER = exports.OUTPUT_RETENTION_MAX_DAYS = exports.isWaiting = exports.FILE_TIMEOUT_MS = void 0;
|
|
4
4
|
exports.execOutputBytes = execOutputBytes;
|
|
5
5
|
exports.configDir = configDir;
|
|
6
6
|
exports.credentialsPath = credentialsPath;
|
|
@@ -10,9 +10,6 @@ exports.storedCredentials = storedCredentials;
|
|
|
10
10
|
exports.storedCredentialsText = storedCredentialsText;
|
|
11
11
|
exports.readCredentialsWithStore = readCredentialsWithStore;
|
|
12
12
|
exports.readCredentials = readCredentials;
|
|
13
|
-
exports.apiUrlFromCompatUrl = apiUrlFromCompatUrl;
|
|
14
|
-
exports.apiUrlFromCompatDomain = apiUrlFromCompatDomain;
|
|
15
|
-
exports.compatApiKey = compatApiKey;
|
|
16
13
|
exports.keyOrigin = keyOrigin;
|
|
17
14
|
exports.imageRefProblem = imageRefProblem;
|
|
18
15
|
exports.checkImageRef = checkImageRef;
|
|
@@ -27,6 +24,7 @@ exports.errorFor = errorFor;
|
|
|
27
24
|
exports.checkSandboxId = checkSandboxId;
|
|
28
25
|
exports.checkSpendCap = checkSpendCap;
|
|
29
26
|
exports.newIdempotencyKey = newIdempotencyKey;
|
|
27
|
+
exports.runWorkDone = runWorkDone;
|
|
30
28
|
exports.serverAppMs = serverAppMs;
|
|
31
29
|
exports.toCommandResult = toCommandResult;
|
|
32
30
|
const node_fs_1 = require("node:fs");
|
|
@@ -48,6 +46,7 @@ Object.defineProperty(exports, "isKeyedCreate", { enumerable: true, get: functio
|
|
|
48
46
|
const dispatcher_js_1 = require("./dispatcher.js");
|
|
49
47
|
const ca_guard_js_1 = require("./ca-guard.js");
|
|
50
48
|
const hint_js_1 = require("./hint.js");
|
|
49
|
+
const command_errors_js_1 = require("./command-errors.js");
|
|
51
50
|
const github_js_1 = require("./github.js");
|
|
52
51
|
const gitlab_js_1 = require("./gitlab.js");
|
|
53
52
|
exports.FILE_TIMEOUT_MS = 15 * 60_000;
|
|
@@ -126,27 +125,46 @@ function pageQuery(limit, max, cursor) {
|
|
|
126
125
|
}
|
|
127
126
|
exports.OUTPUT_RETENTION_MAX_DAYS = 30;
|
|
128
127
|
function projectUpdateBody(u) {
|
|
128
|
+
return settingsBody(u, "projects.update", "the organization's");
|
|
129
|
+
}
|
|
130
|
+
function orgUpdateBody(u) {
|
|
131
|
+
return settingsBody(u, "orgs.update", "the default");
|
|
132
|
+
}
|
|
133
|
+
function settingsBody(u, what, fallback) {
|
|
129
134
|
const body = {};
|
|
130
135
|
if (u.name !== undefined)
|
|
131
136
|
body["name"] = u.name;
|
|
132
137
|
if (u.outputRetentionDays !== undefined) {
|
|
133
138
|
const d = u.outputRetentionDays;
|
|
134
139
|
if (d !== null && (!Number.isInteger(d) || d < 1 || d > exports.OUTPUT_RETENTION_MAX_DAYS)) {
|
|
135
|
-
throw new core_js_1.InvalidArgumentError(`outputRetentionDays must be a whole number from 1 to ${exports.OUTPUT_RETENTION_MAX_DAYS}, or null, not ${String(d)}`,
|
|
140
|
+
throw new core_js_1.InvalidArgumentError(`outputRetentionDays must be a whole number from 1 to ${exports.OUTPUT_RETENTION_MAX_DAYS}, or null, not ${String(d)}`, `Pass null for ${fallback} setting.`);
|
|
136
141
|
}
|
|
137
142
|
body["output_retention_days"] = d;
|
|
138
143
|
}
|
|
139
144
|
if (u.failureCaptures !== undefined) {
|
|
140
145
|
if (u.failureCaptures !== null && typeof u.failureCaptures !== "boolean") {
|
|
141
|
-
throw new core_js_1.InvalidArgumentError("failureCaptures must be true, false or null",
|
|
146
|
+
throw new core_js_1.InvalidArgumentError("failureCaptures must be true, false or null", `Pass null for ${fallback} setting.`);
|
|
142
147
|
}
|
|
143
148
|
if (Object.keys(body).length) {
|
|
144
149
|
throw new core_js_1.InvalidArgumentError("failureCaptures is set on its own", "Change it in a call of its own, without name or outputRetentionDays.");
|
|
145
150
|
}
|
|
146
151
|
body["failure_captures"] = u.failureCaptures;
|
|
147
152
|
}
|
|
153
|
+
if (u.previewLinkLength !== undefined) {
|
|
154
|
+
const l = u.previewLinkLength;
|
|
155
|
+
if (l === "custom") {
|
|
156
|
+
throw new core_js_1.InvalidArgumentError("custom domains for preview links are coming soon", `Pass "short", "long" or null (${fallback}) for now.`);
|
|
157
|
+
}
|
|
158
|
+
if (l !== null && l !== "short" && l !== "long") {
|
|
159
|
+
throw new core_js_1.InvalidArgumentError(`previewLinkLength must be "short", "long" or null, not ${JSON.stringify(l)}`, `Pass null for ${fallback} setting.`);
|
|
160
|
+
}
|
|
161
|
+
if (Object.keys(body).length) {
|
|
162
|
+
throw new core_js_1.InvalidArgumentError("previewLinkLength is set on its own", "Change it in a call of its own, without the other settings.");
|
|
163
|
+
}
|
|
164
|
+
body["preview_link_length"] = l;
|
|
165
|
+
}
|
|
148
166
|
if (!Object.keys(body).length) {
|
|
149
|
-
throw new core_js_1.InvalidArgumentError(
|
|
167
|
+
throw new core_js_1.InvalidArgumentError(`${what} needs something to change`, "Pass name, outputRetentionDays, failureCaptures or previewLinkLength.");
|
|
150
168
|
}
|
|
151
169
|
return body;
|
|
152
170
|
}
|
|
@@ -158,6 +176,14 @@ function expiryFields(o) {
|
|
|
158
176
|
body["expires_at"] = o.expiresAt;
|
|
159
177
|
return body;
|
|
160
178
|
}
|
|
179
|
+
function checkPreviewLength(length) {
|
|
180
|
+
if (length === undefined || length === "short" || length === "long")
|
|
181
|
+
return;
|
|
182
|
+
if (length === "custom") {
|
|
183
|
+
throw new core_js_1.InvalidArgumentError("custom domains for preview links are coming soon", "Pass length: \"short\" (the default) or \"long\" for now.");
|
|
184
|
+
}
|
|
185
|
+
throw new core_js_1.InvalidArgumentError(`length must be "short" or "long", got ${JSON.stringify(length)}`, "Omit it for the length your project or organization chose.");
|
|
186
|
+
}
|
|
161
187
|
exports.NOTICE_HEADER = "promigence-notice";
|
|
162
188
|
exports.MAX_SOURCES = 8;
|
|
163
189
|
function configDir(env = process.env) {
|
|
@@ -309,30 +335,6 @@ function readCredentials(env = process.env) {
|
|
|
309
335
|
function storedName(store, env) {
|
|
310
336
|
return store === undefined || store === "file" ? `the credentials file (${credentialsPath(env)})` : (0, credstore_js_1.storeLabel)(store);
|
|
311
337
|
}
|
|
312
|
-
const COMPAT_REFUSED_HOSTS = /(^|\.)e2b(-staging)?\.(app|dev|pro)$/i;
|
|
313
|
-
function apiUrlFromCompatUrl(url) {
|
|
314
|
-
const u = (url ?? "").trim();
|
|
315
|
-
if (!u)
|
|
316
|
-
return undefined;
|
|
317
|
-
let host = "";
|
|
318
|
-
try {
|
|
319
|
-
host = new URL(u.includes("://") ? u : `https://${u}`).hostname;
|
|
320
|
-
}
|
|
321
|
-
catch {
|
|
322
|
-
return undefined;
|
|
323
|
-
}
|
|
324
|
-
return COMPAT_REFUSED_HOSTS.test(host) ? undefined : u;
|
|
325
|
-
}
|
|
326
|
-
function apiUrlFromCompatDomain(domain) {
|
|
327
|
-
const d = (domain ?? "").trim().replace(/^https?:\/\//, "").replace(/\/+$/, "");
|
|
328
|
-
if (!d || COMPAT_REFUSED_HOSTS.test(d))
|
|
329
|
-
return undefined;
|
|
330
|
-
return `https://api.${d}/v1`;
|
|
331
|
-
}
|
|
332
|
-
function compatApiKey(key) {
|
|
333
|
-
const k = key?.trim();
|
|
334
|
-
return k && !k.startsWith("e2b_") ? k : undefined;
|
|
335
|
-
}
|
|
336
338
|
function keyOrigin(env = process.env) {
|
|
337
339
|
if (env["PROMIGENCE_API_KEY"]?.trim())
|
|
338
340
|
return "PROMIGENCE_API_KEY";
|
|
@@ -359,12 +361,13 @@ function resolveCredentials(opts, env = process.env) {
|
|
|
359
361
|
const inputs = optKey === undefined && envKey === undefined
|
|
360
362
|
? undefined
|
|
361
363
|
: `${optKey ?? "\0"}\n${envKey ?? "\0"}\n${envUrl ?? "\0"}\n${opts.apiUrl ?? "\0"}`;
|
|
362
|
-
if (inputs !== undefined && inputs === credsLastInputs && credsLast !== undefined)
|
|
363
|
-
return { apiKey: credsLast.apiKey, apiUrl: credsLast.apiUrl };
|
|
364
|
+
if (inputs !== undefined && inputs === credsLastInputs && credsLast !== undefined) {
|
|
365
|
+
return { apiKey: credsLast.apiKey, apiUrl: credsLast.apiUrl, ...(credsLast.origin ? { origin: credsLast.origin } : {}) };
|
|
366
|
+
}
|
|
364
367
|
const worked = resolveCredentialsFrom(opts, env, set, trim, optKey, envKey, envUrl);
|
|
365
368
|
if (inputs !== undefined && worked.refusal === undefined) {
|
|
366
369
|
credsLastInputs = inputs;
|
|
367
|
-
credsLast = { apiKey: worked.apiKey, apiUrl: worked.apiUrl };
|
|
370
|
+
credsLast = { apiKey: worked.apiKey, apiUrl: worked.apiUrl, ...(worked.origin ? { origin: worked.origin } : {}) };
|
|
368
371
|
}
|
|
369
372
|
return worked;
|
|
370
373
|
}
|
|
@@ -398,8 +401,15 @@ function resolveCredentialsFrom(opts, env, set, trim, optKey, envKey, envUrl) {
|
|
|
398
401
|
if (refusal)
|
|
399
402
|
return { apiKey: undefined, apiUrl, refusal };
|
|
400
403
|
}
|
|
401
|
-
|
|
404
|
+
const origin = !key ? undefined
|
|
405
|
+
: source === "opts" ? { source: "option" }
|
|
406
|
+
: source === "env" ? { source: "env" }
|
|
407
|
+
: source === "file" ? { source: "stored", ...(store === undefined ? {} : { store }) }
|
|
408
|
+
: { source: "fallback" };
|
|
409
|
+
return { apiKey: key, apiUrl, ...(origin ? { origin } : {}) };
|
|
402
410
|
}
|
|
411
|
+
exports.NO_KEY_MESSAGE = "no API key: none was given, PROMIGENCE_API_KEY is not set, and none is stored";
|
|
412
|
+
exports.NO_KEY_FIX = "Pass { apiKey } or set PROMIGENCE_API_KEY. No key yet: `promigence login` signs you up or in and stores one.";
|
|
403
413
|
function keyRefusal(key, from) {
|
|
404
414
|
if (/^[\x21-\x7E]+$/.test(key))
|
|
405
415
|
return undefined;
|
|
@@ -679,6 +689,8 @@ function snapshotCreateBody(opts) {
|
|
|
679
689
|
}
|
|
680
690
|
if (o.alias)
|
|
681
691
|
body["alias"] = o.alias;
|
|
692
|
+
if (o.alias && o.moveAlias)
|
|
693
|
+
body["move_alias"] = true;
|
|
682
694
|
if (o.grade) {
|
|
683
695
|
if (!o.grade.cmd)
|
|
684
696
|
throw new core_js_1.InvalidArgumentError("snapshots.create grade needs a cmd", "Pass { grade: { cmd: 'pytest -q tests/' } }.");
|
|
@@ -713,6 +725,9 @@ const REFUSED_503_CODES = new Set([
|
|
|
713
725
|
"builder_unavailable", "builder_busy", "overloaded", "tier_unavailable", "registry_unavailable",
|
|
714
726
|
"secrets_unavailable",
|
|
715
727
|
]);
|
|
728
|
+
const SDK_FIXES = {
|
|
729
|
+
cap_exceeded: "Raise `spendCap`, lower `timeoutMs` or `count`, or pass `allowPartial` to launch what fits.",
|
|
730
|
+
};
|
|
716
731
|
function withoutCliHelp(fix) {
|
|
717
732
|
const out = fix.replace(/\s*See `promigence [^`]*--help`\.?\s*$/, "").trim();
|
|
718
733
|
if (out === fix.trim())
|
|
@@ -722,7 +737,7 @@ function withoutCliHelp(fix) {
|
|
|
722
737
|
function errorFor(code, message, fix, extra = {}) {
|
|
723
738
|
const attach = (e) => {
|
|
724
739
|
const w = e;
|
|
725
|
-
w.docs = extra.docs
|
|
740
|
+
w.docs = extra.docs !== undefined && e.code === code ? extra.docs : (0, core_js_1.errorsDocsFor)(e.code);
|
|
726
741
|
if (extra.status !== undefined)
|
|
727
742
|
w.status = extra.status;
|
|
728
743
|
if (extra.requestId !== undefined)
|
|
@@ -738,6 +753,8 @@ function errorFor(code, message, fix, extra = {}) {
|
|
|
738
753
|
if ((code === "usage" || code === "invalid_argument" || code === "unsupported") && (0, core_js_1.previewsOff)(message)) {
|
|
739
754
|
return attach(new core_js_1.PreviewsUnavailableError());
|
|
740
755
|
}
|
|
756
|
+
if (!extra.cli && Object.prototype.hasOwnProperty.call(SDK_FIXES, code))
|
|
757
|
+
fix = SDK_FIXES[code];
|
|
741
758
|
switch (code) {
|
|
742
759
|
case "auth": return attach(new core_js_1.AuthError(message, fix));
|
|
743
760
|
case "not_found": return attach(new core_js_1.NotFoundError(message, fix));
|
|
@@ -751,8 +768,13 @@ function errorFor(code, message, fix, extra = {}) {
|
|
|
751
768
|
case "not_verified": return attach(new core_js_1.NotVerifiedError(message, fix));
|
|
752
769
|
case "timeout": return attach(new core_js_1.TimeoutError(message, fix));
|
|
753
770
|
case "usage":
|
|
754
|
-
case "invalid_argument":
|
|
771
|
+
case "invalid_argument": {
|
|
772
|
+
const ended = code === "usage" ? (0, core_js_1.sandboxEndedIn)(message) : undefined;
|
|
773
|
+
if (ended !== undefined) {
|
|
774
|
+
return attach(new core_js_1.SandboxEndedError(message, ended.sandboxId, ended.state, extra.cli ? fix : core_js_1.SANDBOX_ENDED_FIX));
|
|
775
|
+
}
|
|
755
776
|
return attach(new core_js_1.InvalidArgumentError(message, extra.cli ? fix : withoutCliHelp(fix), code));
|
|
777
|
+
}
|
|
756
778
|
case "not_released": return attach(new core_js_1.NotReleasedError(message));
|
|
757
779
|
case "service_restart": return attach(new core_js_1.ServiceRestartError(message, fix));
|
|
758
780
|
default:
|
|
@@ -804,6 +826,76 @@ function parseRetryAfter(header) {
|
|
|
804
826
|
const MAX_RETRY_SLEEP_MS = 10_000;
|
|
805
827
|
const STREAM_IDLE_MS = 90_000;
|
|
806
828
|
const sleep = (ms) => new Promise((r) => { setTimeout(r, ms); });
|
|
829
|
+
const EXEC_KEY_HEADER = "promigence-exec-key";
|
|
830
|
+
const EXEC_HOLD_DEFAULT_S = 120;
|
|
831
|
+
const REATTACH_SLACK_MS = 30_000;
|
|
832
|
+
const REATTACH_BACKOFF_MS = 250;
|
|
833
|
+
const REATTACH_BACKOFF_MAX_MS = 2_000;
|
|
834
|
+
const REATTACH_RETRY_AFTER_MAX_MS = 5_000;
|
|
835
|
+
const REATTACH_TAKEN_MS = 10_000;
|
|
836
|
+
const EXEC_CANCEL_TIMEOUT_MS = 3_000;
|
|
837
|
+
const REATTACH_AGAIN = new Set([429, 502, 503, 504]);
|
|
838
|
+
function newExecKey() {
|
|
839
|
+
const bytes = globalThis.crypto.getRandomValues(new Uint8Array(16));
|
|
840
|
+
let s = "";
|
|
841
|
+
for (const b of bytes)
|
|
842
|
+
s += b.toString(16).padStart(2, "0");
|
|
843
|
+
return s;
|
|
844
|
+
}
|
|
845
|
+
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)$/;
|
|
846
|
+
function provedUnsent(err) {
|
|
847
|
+
if ((0, ca_guard_js_1.compromisedKeyCause)(err) !== undefined)
|
|
848
|
+
return true;
|
|
849
|
+
const top = err;
|
|
850
|
+
if (top?.name === "TypeError" && /header/i.test(top.message ?? ""))
|
|
851
|
+
return true;
|
|
852
|
+
const unsent = (x) => {
|
|
853
|
+
const code = x?.code;
|
|
854
|
+
return typeof code === "string" && UNSENT_CODES.test(code);
|
|
855
|
+
};
|
|
856
|
+
let c = err;
|
|
857
|
+
for (let depth = 0; c !== null && typeof c === "object" && depth < 6; depth++) {
|
|
858
|
+
if (unsent(c))
|
|
859
|
+
return true;
|
|
860
|
+
const each = c.errors;
|
|
861
|
+
if (Array.isArray(each) && each.length > 0 && each.every(unsent))
|
|
862
|
+
return true;
|
|
863
|
+
c = c.cause;
|
|
864
|
+
}
|
|
865
|
+
return false;
|
|
866
|
+
}
|
|
867
|
+
class HeldDrop extends Error {
|
|
868
|
+
key;
|
|
869
|
+
constructor(key, why) {
|
|
870
|
+
super(why);
|
|
871
|
+
this.key = key;
|
|
872
|
+
this.name = "HeldDrop";
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
function sleepUnless(ms, signal) {
|
|
876
|
+
if (signal === undefined)
|
|
877
|
+
return sleep(ms);
|
|
878
|
+
return new Promise((resolve) => {
|
|
879
|
+
if (signal.aborted) {
|
|
880
|
+
resolve();
|
|
881
|
+
return;
|
|
882
|
+
}
|
|
883
|
+
const done = () => { clearTimeout(t); signal.removeEventListener("abort", done); resolve(); };
|
|
884
|
+
const t = setTimeout(done, ms);
|
|
885
|
+
signal.addEventListener("abort", done, { once: true });
|
|
886
|
+
});
|
|
887
|
+
}
|
|
888
|
+
function reattachWait(attempt, retryAfterMs) {
|
|
889
|
+
if (retryAfterMs !== undefined)
|
|
890
|
+
return Math.min(retryAfterMs, REATTACH_RETRY_AFTER_MAX_MS);
|
|
891
|
+
return Math.min(REATTACH_BACKOFF_MAX_MS, REATTACH_BACKOFF_MS * 2 ** Math.min(attempt, 8)) * (0.5 + Math.random() * 0.5);
|
|
892
|
+
}
|
|
893
|
+
function stoppedUnattended(answer) {
|
|
894
|
+
return answer?.reason === "not_reattached";
|
|
895
|
+
}
|
|
896
|
+
function abortedByCaller(method, path) {
|
|
897
|
+
return new core_js_1.TimeoutError(`request ${method} ${path} was aborted by the caller's signal`, "Nothing to fix: the caller aborted it.");
|
|
898
|
+
}
|
|
807
899
|
const POLLING_TICK_MS = 5;
|
|
808
900
|
const PARKED_TICK_MS = 50;
|
|
809
901
|
async function keepingLoopAwake(everyMs, fn) {
|
|
@@ -822,6 +914,21 @@ const WAIT_MS_CAP = 30_000;
|
|
|
822
914
|
exports.LONG_POLL_MS = 5_000;
|
|
823
915
|
const BUILD_POLL_MS = 10_000;
|
|
824
916
|
const PROGRESS_POLL_MS = 200;
|
|
917
|
+
exports.RUN_SETTLE_MS = 5_000;
|
|
918
|
+
function runWorkDone(run, endedSince, now = Date.now()) {
|
|
919
|
+
if (run.state === "completed" || run.state === "cap_killed" || run.state === "aborted")
|
|
920
|
+
return true;
|
|
921
|
+
const sbs = Array.isArray(run.sandboxes) ? run.sandboxes : [];
|
|
922
|
+
const allDone = sbs.length > 0 && sbs.every((s) => s.state === "ended" || s.command_outcome != null);
|
|
923
|
+
if (!allDone) {
|
|
924
|
+
delete endedSince.at;
|
|
925
|
+
return false;
|
|
926
|
+
}
|
|
927
|
+
if (sbs.some((s) => s.state !== "ended"))
|
|
928
|
+
return true;
|
|
929
|
+
endedSince.at ??= now;
|
|
930
|
+
return now - endedSince.at >= exports.RUN_SETTLE_MS;
|
|
931
|
+
}
|
|
825
932
|
const WAITED_HEADER = "promigence-waited-ms";
|
|
826
933
|
const seg = (s) => encodeURIComponent(s).replace(/%40/g, "@");
|
|
827
934
|
function waitQuery(waitMs) {
|
|
@@ -859,6 +966,9 @@ class PromigenceClient {
|
|
|
859
966
|
apiUrl;
|
|
860
967
|
#apiKey;
|
|
861
968
|
#keyRefusal;
|
|
969
|
+
#keyOrigin;
|
|
970
|
+
#onKeyUse;
|
|
971
|
+
#keyUseSaid = false;
|
|
862
972
|
#bearer;
|
|
863
973
|
fetchImpl;
|
|
864
974
|
ownFetch;
|
|
@@ -880,6 +990,8 @@ class PromigenceClient {
|
|
|
880
990
|
const resolved = resolveCredentials(opts);
|
|
881
991
|
this.#apiKey = resolved.apiKey;
|
|
882
992
|
this.#keyRefusal = resolved.refusal;
|
|
993
|
+
this.#keyOrigin = resolved.origin;
|
|
994
|
+
this.#onKeyUse = opts.onKeyUse;
|
|
883
995
|
this.apiUrl = resolved.apiUrl;
|
|
884
996
|
const env = opts.fetch === undefined ? (0, proxy_js_1.transportEnv)() : undefined;
|
|
885
997
|
const proxy = env !== undefined ? (0, proxy_js_1.proxyFor)(this.apiUrl, env) : undefined;
|
|
@@ -945,11 +1057,18 @@ class PromigenceClient {
|
|
|
945
1057
|
catch {
|
|
946
1058
|
}
|
|
947
1059
|
}
|
|
1060
|
+
get keyOrigin() {
|
|
1061
|
+
return this.#keyOrigin;
|
|
1062
|
+
}
|
|
948
1063
|
requireKey() {
|
|
949
1064
|
if (this.#keyRefusal)
|
|
950
1065
|
throw this.#keyRefusal;
|
|
951
1066
|
if (!this.#apiKey)
|
|
952
|
-
throw new core_js_1.AuthError();
|
|
1067
|
+
throw this.cliErrors ? new core_js_1.AuthError(exports.NO_KEY_MESSAGE) : new core_js_1.AuthError(exports.NO_KEY_MESSAGE, exports.NO_KEY_FIX);
|
|
1068
|
+
if (!this.#keyUseSaid && this.#onKeyUse !== undefined && this.#keyOrigin !== undefined) {
|
|
1069
|
+
this.#keyUseSaid = true;
|
|
1070
|
+
this.#onKeyUse(this.#keyOrigin);
|
|
1071
|
+
}
|
|
953
1072
|
return this.#apiKey;
|
|
954
1073
|
}
|
|
955
1074
|
bearer() {
|
|
@@ -997,6 +1116,82 @@ class PromigenceClient {
|
|
|
997
1116
|
await sleep(backoff * (0.5 + Math.random() * 0.5));
|
|
998
1117
|
return true;
|
|
999
1118
|
}
|
|
1119
|
+
async heldExec(sandboxId, req) {
|
|
1120
|
+
const signal = req.signal;
|
|
1121
|
+
const abortedBefore = signal?.aborted === true;
|
|
1122
|
+
let key;
|
|
1123
|
+
try {
|
|
1124
|
+
let res;
|
|
1125
|
+
try {
|
|
1126
|
+
res = await this.request({ ...req, held: { onKey: (k) => { key = k; } } });
|
|
1127
|
+
}
|
|
1128
|
+
catch (err) {
|
|
1129
|
+
if (!(err instanceof HeldDrop))
|
|
1130
|
+
throw err;
|
|
1131
|
+
res = await this.reattachBuffered(sandboxId, err.key, req);
|
|
1132
|
+
}
|
|
1133
|
+
if (stoppedUnattended(res)) {
|
|
1134
|
+
throw new core_js_1.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");
|
|
1135
|
+
}
|
|
1136
|
+
return res;
|
|
1137
|
+
}
|
|
1138
|
+
catch (err) {
|
|
1139
|
+
if (key !== undefined && signal?.aborted && !abortedBefore)
|
|
1140
|
+
await this.cancelHeldExec(sandboxId, key);
|
|
1141
|
+
throw err;
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1144
|
+
async reattachBuffered(sandboxId, key, req) {
|
|
1145
|
+
const budgetMs = EXEC_HOLD_DEFAULT_S * 1000 + REATTACH_SLACK_MS;
|
|
1146
|
+
let droppedAt = Date.now();
|
|
1147
|
+
for (let attempt = 0;; attempt++) {
|
|
1148
|
+
if (req.signal?.aborted)
|
|
1149
|
+
throw abortedByCaller(req.method, req.path);
|
|
1150
|
+
this.stats.retries++;
|
|
1151
|
+
const sentAt = Date.now();
|
|
1152
|
+
try {
|
|
1153
|
+
return await this.request({
|
|
1154
|
+
method: "POST",
|
|
1155
|
+
path: `/sandboxes/${encodeURIComponent(sandboxId)}/execs/attach`,
|
|
1156
|
+
body: { key },
|
|
1157
|
+
once: true,
|
|
1158
|
+
...(req.timeoutMs === undefined ? {} : { timeoutMs: req.timeoutMs }),
|
|
1159
|
+
...(req.signal === undefined ? {} : { signal: req.signal }),
|
|
1160
|
+
});
|
|
1161
|
+
}
|
|
1162
|
+
catch (err) {
|
|
1163
|
+
if (req.signal?.aborted)
|
|
1164
|
+
throw abortedByCaller(req.method, req.path);
|
|
1165
|
+
const status = err instanceof core_js_1.PromigenceError ? err.status : undefined;
|
|
1166
|
+
if (status === 404) {
|
|
1167
|
+
throw new core_js_1.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");
|
|
1168
|
+
}
|
|
1169
|
+
const lostConnection = status === undefined && (err instanceof core_js_1.UnreachableError || err instanceof core_js_1.TimeoutError);
|
|
1170
|
+
if (!lostConnection && !(status !== undefined && REATTACH_AGAIN.has(status)))
|
|
1171
|
+
throw err;
|
|
1172
|
+
if (Date.now() - sentAt >= REATTACH_TAKEN_MS)
|
|
1173
|
+
droppedAt = Date.now();
|
|
1174
|
+
const left = budgetMs - (Date.now() - droppedAt);
|
|
1175
|
+
if (left <= 0) {
|
|
1176
|
+
throw new core_js_1.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");
|
|
1177
|
+
}
|
|
1178
|
+
await sleepUnless(Math.min(left, reattachWait(attempt, err instanceof core_js_1.PromigenceError ? err.retryAfterMs : undefined)), req.signal);
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
1182
|
+
async cancelHeldExec(sandboxId, key) {
|
|
1183
|
+
try {
|
|
1184
|
+
await this.request({
|
|
1185
|
+
method: "POST",
|
|
1186
|
+
path: `/sandboxes/${encodeURIComponent(sandboxId)}/execs/cancel`,
|
|
1187
|
+
body: { key },
|
|
1188
|
+
timeoutMs: EXEC_CANCEL_TIMEOUT_MS,
|
|
1189
|
+
once: true,
|
|
1190
|
+
});
|
|
1191
|
+
}
|
|
1192
|
+
catch {
|
|
1193
|
+
}
|
|
1194
|
+
}
|
|
1000
1195
|
async request(opts) {
|
|
1001
1196
|
const tr = (0, trace_js_1.traceBegin)(opts.method, opts.path);
|
|
1002
1197
|
const url = `${this.apiUrl}${opts.path}`;
|
|
@@ -1027,9 +1222,16 @@ class PromigenceClient {
|
|
|
1027
1222
|
const timeoutMs = opts.timeoutMs ?? this.requestTimeoutMs;
|
|
1028
1223
|
let dispatcher;
|
|
1029
1224
|
let resentDelete = false;
|
|
1225
|
+
const maxRetries = opts.once === true ? 0 : this.maxRetries;
|
|
1226
|
+
let execKey;
|
|
1030
1227
|
for (let attempt = 0;; attempt++) {
|
|
1031
1228
|
let res;
|
|
1032
1229
|
this.stats.requests++;
|
|
1230
|
+
if (opts.held !== undefined) {
|
|
1231
|
+
execKey = newExecKey();
|
|
1232
|
+
headers[EXEC_KEY_HEADER] = execKey;
|
|
1233
|
+
opts.held.onKey(execKey);
|
|
1234
|
+
}
|
|
1033
1235
|
const sentAt = performance.now();
|
|
1034
1236
|
try {
|
|
1035
1237
|
if (this.h2) {
|
|
@@ -1072,11 +1274,14 @@ class PromigenceClient {
|
|
|
1072
1274
|
}
|
|
1073
1275
|
catch (err) {
|
|
1074
1276
|
const e = err;
|
|
1277
|
+
if (execKey !== undefined && !opts.signal?.aborted && !provedUnsent(err)) {
|
|
1278
|
+
throw new HeldDrop(execKey, transportDetail(e));
|
|
1279
|
+
}
|
|
1075
1280
|
if (keyedCreate && idempotencyKey && await this.againKeyed(attempt, opts.signal))
|
|
1076
1281
|
continue;
|
|
1077
1282
|
const cutOff = e?.cause?.code === "UND_ERR_HEADERS_TIMEOUT" || e?.cause?.code === "UND_ERR_BODY_TIMEOUT";
|
|
1078
1283
|
const timedOut = e?.name === "TimeoutError" || e?.name === "AbortError" || cutOff;
|
|
1079
|
-
if (!timedOut && replayable && attempt <
|
|
1284
|
+
if (!timedOut && replayable && attempt < maxRetries && !opts.signal?.aborted) {
|
|
1080
1285
|
this.stats.retries++;
|
|
1081
1286
|
if (opts.method === "DELETE")
|
|
1082
1287
|
resentDelete = true;
|
|
@@ -1084,9 +1289,8 @@ class PromigenceClient {
|
|
|
1084
1289
|
await sleep(backoff * (0.5 + Math.random() * 0.5));
|
|
1085
1290
|
continue;
|
|
1086
1291
|
}
|
|
1087
|
-
if (opts.signal?.aborted)
|
|
1088
|
-
throw
|
|
1089
|
-
}
|
|
1292
|
+
if (opts.signal?.aborted)
|
|
1293
|
+
throw abortedByCaller(opts.method, opts.path);
|
|
1090
1294
|
if (e?.name === "TimeoutError" || e?.name === "AbortError") {
|
|
1091
1295
|
throw new core_js_1.TimeoutError(`request ${opts.method} ${opts.path} timed out after ${timeoutMs} ms`, "Retry. To allow longer: promigence --request-timeout 120s <command>, " +
|
|
1092
1296
|
"PROMIGENCE_REQUEST_TIMEOUT=120s, or requestTimeoutMs in the SDK. A --wait is bounded " +
|
|
@@ -1103,6 +1307,11 @@ class PromigenceClient {
|
|
|
1103
1307
|
const gateway = (res.status === 502 || res.status === 504) && replayable;
|
|
1104
1308
|
let retryable = (res.status === 429 || res.status === 503 || gateway) && (retryAfterMs ?? 0) <= MAX_RETRY_SLEEP_MS;
|
|
1105
1309
|
let body;
|
|
1310
|
+
if (execKey !== undefined && (res.status === 502 || res.status === 504)) {
|
|
1311
|
+
body = await res.text().catch(() => "");
|
|
1312
|
+
if (errorCodeOf(body) === undefined)
|
|
1313
|
+
throw new HeldDrop(execKey, `HTTP ${res.status}`);
|
|
1314
|
+
}
|
|
1106
1315
|
if (retryable && res.status === 503 && !replayable && retryAfterMs === undefined) {
|
|
1107
1316
|
body = await res.text().catch(() => "");
|
|
1108
1317
|
retryable = REFUSED_503_CODES.has(errorCodeOf(body) ?? "");
|
|
@@ -1111,7 +1320,7 @@ class PromigenceClient {
|
|
|
1111
1320
|
body = await res.text().catch(() => "");
|
|
1112
1321
|
retryable = errorCodeOf(body) === undefined;
|
|
1113
1322
|
}
|
|
1114
|
-
if (retryable && attempt <
|
|
1323
|
+
if (retryable && attempt < maxRetries) {
|
|
1115
1324
|
const backoff = Math.min(MAX_RETRY_SLEEP_MS, this.retryBaseMs * 2 ** attempt);
|
|
1116
1325
|
const jitter = Math.random();
|
|
1117
1326
|
const waitMs = retryAfterMs !== undefined
|
|
@@ -1144,8 +1353,10 @@ class PromigenceClient {
|
|
|
1144
1353
|
text = "";
|
|
1145
1354
|
else {
|
|
1146
1355
|
const e = err;
|
|
1356
|
+
if (execKey !== undefined && !opts.signal?.aborted)
|
|
1357
|
+
throw new HeldDrop(execKey, transportDetail(e));
|
|
1147
1358
|
const timedOut = e?.name === "TimeoutError" || e?.name === "AbortError";
|
|
1148
|
-
if (!timedOut && replayable && attempt <
|
|
1359
|
+
if (!timedOut && replayable && attempt < maxRetries && !opts.signal?.aborted) {
|
|
1149
1360
|
this.stats.retries++;
|
|
1150
1361
|
if (opts.method === "DELETE")
|
|
1151
1362
|
resentDelete = true;
|
|
@@ -1271,10 +1482,12 @@ class PromigenceClient {
|
|
|
1271
1482
|
continue;
|
|
1272
1483
|
}
|
|
1273
1484
|
const code = payload.error?.code ?? codeForStatus(res.status);
|
|
1485
|
+
const rid = headerOf(res, "request-id") ?? headerOf(res, "x-request-id");
|
|
1274
1486
|
throw errorFor(code, payload.error?.message ?? `HTTP ${res.status} from ${method} ${path}`, payload.error?.fix ?? "See the API response for details.", {
|
|
1275
1487
|
status: res.status,
|
|
1276
1488
|
...bodyExtra(payload),
|
|
1277
1489
|
...(retryAfterMs === undefined ? {} : { retryAfterMs }),
|
|
1490
|
+
...(rid ? { requestId: rid } : {}),
|
|
1278
1491
|
});
|
|
1279
1492
|
}
|
|
1280
1493
|
}
|
|
@@ -1354,10 +1567,12 @@ class PromigenceClient {
|
|
|
1354
1567
|
continue;
|
|
1355
1568
|
}
|
|
1356
1569
|
const code = payload.error?.code ?? codeForStatus(res.status);
|
|
1570
|
+
const rid = headerOf(res, "request-id") ?? headerOf(res, "x-request-id");
|
|
1357
1571
|
throw errorFor(code, payload.error?.message ?? `HTTP ${res.status} from ${method} ${path}`, payload.error?.fix ?? "See the API response for details.", {
|
|
1358
1572
|
status: res.status,
|
|
1359
1573
|
...bodyExtra(payload),
|
|
1360
1574
|
...(retryAfterMs === undefined ? {} : { retryAfterMs }),
|
|
1575
|
+
...(rid ? { requestId: rid } : {}),
|
|
1361
1576
|
});
|
|
1362
1577
|
}
|
|
1363
1578
|
}
|
|
@@ -1518,6 +1733,7 @@ class PromigenceClient {
|
|
|
1518
1733
|
method: "PATCH", path: `/orgs/${orgSeg(org)}/projects/${encodeURIComponent(project)}`, body: projectUpdateBody(update),
|
|
1519
1734
|
}),
|
|
1520
1735
|
},
|
|
1736
|
+
update: async (org, update) => this.request({ method: "PATCH", path: `/orgs/${orgSeg(org)}`, body: orgUpdateBody(update) }),
|
|
1521
1737
|
};
|
|
1522
1738
|
}
|
|
1523
1739
|
group_credits() {
|
|
@@ -1597,6 +1813,8 @@ class PromigenceClient {
|
|
|
1597
1813
|
body["network"] = opts.network;
|
|
1598
1814
|
if (opts.optimizeFor !== undefined)
|
|
1599
1815
|
body["optimize_for"] = opts.optimizeFor;
|
|
1816
|
+
if (opts.capture !== undefined)
|
|
1817
|
+
body["capture"] = opts.capture;
|
|
1600
1818
|
if (opts.secrets?.length)
|
|
1601
1819
|
body["secrets"] = opts.secrets;
|
|
1602
1820
|
if (opts.labels && Object.keys(opts.labels).length)
|
|
@@ -1733,12 +1951,8 @@ class PromigenceClient {
|
|
|
1733
1951
|
return { sandboxes: r.value, timedOut: r.timedOut, waitedMs: r.waitedMs };
|
|
1734
1952
|
},
|
|
1735
1953
|
waitDone: async (runId, opts = {}) => {
|
|
1736
|
-
const
|
|
1737
|
-
|
|
1738
|
-
const allDone = Array.isArray(run.sandboxes) && run.sandboxes.length > 0 &&
|
|
1739
|
-
run.sandboxes.every((s) => s.state === "ended" || s.command_outcome != null);
|
|
1740
|
-
return terminal || allDone;
|
|
1741
|
-
});
|
|
1954
|
+
const endedSince = {};
|
|
1955
|
+
const r = await this.waitLoop(opts, exports.LONG_POLL_MS, { run_id: runId, state: "running" }, (read) => this.runs.get(runId, read), (run) => runWorkDone(run, endedSince));
|
|
1742
1956
|
return { run: r.value, timedOut: r.timedOut, waitedMs: r.waitedMs };
|
|
1743
1957
|
},
|
|
1744
1958
|
};
|
|
@@ -1834,7 +2048,9 @@ class PromigenceClient {
|
|
|
1834
2048
|
req.idempotencyKey = opts.idempotencyKey;
|
|
1835
2049
|
if (opts.signal)
|
|
1836
2050
|
req.signal = opts.signal;
|
|
1837
|
-
|
|
2051
|
+
if (opts.terminal)
|
|
2052
|
+
return this.request(req);
|
|
2053
|
+
return this.heldExec(sandboxId, req);
|
|
1838
2054
|
},
|
|
1839
2055
|
execStream: async (sandboxId, opts, on = {}) => {
|
|
1840
2056
|
if (!opts.argv?.length && !opts.sh) {
|
|
@@ -1863,7 +2079,9 @@ class PromigenceClient {
|
|
|
1863
2079
|
body["timeout_ms"] = commandTimeoutMs(opts.timeoutMs) ?? 15 * 60_000;
|
|
1864
2080
|
body["max_output_bytes"] = opts.maxOutputBytes ?? core_js_1.MAX_OUTPUT_BYTES_DEFAULT;
|
|
1865
2081
|
const url = `${this.apiUrl}/sandboxes/${encodeURIComponent(sandboxId)}/execs`;
|
|
1866
|
-
const
|
|
2082
|
+
const keyed = opts.terminal !== true;
|
|
2083
|
+
let key;
|
|
2084
|
+
let idle = new AbortController();
|
|
1867
2085
|
let silent = false;
|
|
1868
2086
|
let reader;
|
|
1869
2087
|
let timer;
|
|
@@ -1875,23 +2093,33 @@ class PromigenceClient {
|
|
|
1875
2093
|
reader?.cancel().catch(() => { });
|
|
1876
2094
|
}, STREAM_IDLE_MS);
|
|
1877
2095
|
};
|
|
2096
|
+
const fresh = () => { idle = new AbortController(); silent = false; reader = undefined; };
|
|
2097
|
+
const abortedBefore = opts.signal?.aborted === true;
|
|
2098
|
+
const abandon = async (err) => {
|
|
2099
|
+
if (key !== undefined && !abortedBefore)
|
|
2100
|
+
await this.cancelHeldExec(sandboxId, key);
|
|
2101
|
+
throw err;
|
|
2102
|
+
};
|
|
1878
2103
|
let stream;
|
|
1879
2104
|
const bearer = `Bearer ${this.requireKey()}`;
|
|
1880
2105
|
const dispatcher = this.guardDispatcher();
|
|
2106
|
+
const headers = () => ({
|
|
2107
|
+
accept: "text/event-stream",
|
|
2108
|
+
"content-type": "application/json",
|
|
2109
|
+
"user-agent": this.userAgent,
|
|
2110
|
+
authorization: bearer,
|
|
2111
|
+
...(this.caller ? { [caller_js_1.CALLER_HEADER]: this.caller } : {}),
|
|
2112
|
+
});
|
|
1881
2113
|
for (let attempt = 0; stream === undefined; attempt++) {
|
|
1882
2114
|
let r;
|
|
1883
2115
|
this.stats.requests++;
|
|
2116
|
+
if (keyed)
|
|
2117
|
+
key = newExecKey();
|
|
1884
2118
|
arm();
|
|
1885
2119
|
try {
|
|
1886
2120
|
r = await this.fetchImpl(url, {
|
|
1887
2121
|
method: "POST",
|
|
1888
|
-
headers: {
|
|
1889
|
-
accept: "text/event-stream",
|
|
1890
|
-
"content-type": "application/json",
|
|
1891
|
-
"user-agent": this.userAgent,
|
|
1892
|
-
authorization: bearer,
|
|
1893
|
-
...(this.caller ? { [caller_js_1.CALLER_HEADER]: this.caller } : {}),
|
|
1894
|
-
},
|
|
2122
|
+
headers: { ...headers(), ...(key === undefined ? {} : { [EXEC_KEY_HEADER]: key }) },
|
|
1895
2123
|
body: JSON.stringify(body),
|
|
1896
2124
|
signal: opts.signal ? AbortSignal.any([opts.signal, idle.signal]) : idle.signal,
|
|
1897
2125
|
...(dispatcher === undefined ? {} : { dispatcher }),
|
|
@@ -1901,7 +2129,10 @@ class PromigenceClient {
|
|
|
1901
2129
|
clearTimeout(timer);
|
|
1902
2130
|
const e = err;
|
|
1903
2131
|
const why = silent ? `no answer in ${STREAM_IDLE_MS / 1000} s` : transportDetail(e);
|
|
1904
|
-
|
|
2132
|
+
const failed = new core_js_1.UnreachableError(`cannot reach ${url}: ${why}`);
|
|
2133
|
+
if (opts.signal?.aborted)
|
|
2134
|
+
return abandon(failed);
|
|
2135
|
+
throw failed;
|
|
1905
2136
|
}
|
|
1906
2137
|
if (r.ok && r.body) {
|
|
1907
2138
|
stream = r.body;
|
|
@@ -1932,87 +2163,166 @@ class PromigenceClient {
|
|
|
1932
2163
|
const errOut = [];
|
|
1933
2164
|
let exit;
|
|
1934
2165
|
let failure;
|
|
1935
|
-
|
|
1936
|
-
|
|
2166
|
+
let holdS;
|
|
2167
|
+
const received = { stdout: 0, stderr: 0 };
|
|
1937
2168
|
const lost = (how) => new core_js_1.UnreachableError(`the exec stream for sandbox ${sandboxId} ${how}`, "The connection dropped before the command finished; its result is unknown. " +
|
|
1938
2169
|
`Read it by id (\`promigence exec output <exec_id> --sandbox ${(0, hint_js_1.hintArg)(sandboxId)}\`) or re-run.`);
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
2170
|
+
const consume = async (from) => {
|
|
2171
|
+
const dec = new TextDecoder();
|
|
2172
|
+
let buf = "";
|
|
2173
|
+
const rd = from.getReader();
|
|
2174
|
+
reader = rd;
|
|
2175
|
+
try {
|
|
2176
|
+
for (;;) {
|
|
2177
|
+
arm();
|
|
2178
|
+
let chunk;
|
|
2179
|
+
try {
|
|
2180
|
+
chunk = await rd.read();
|
|
2181
|
+
}
|
|
2182
|
+
catch (err) {
|
|
2183
|
+
if (opts.signal?.aborted)
|
|
2184
|
+
return abandon(err);
|
|
2185
|
+
if (holdS !== undefined && (exit || failure))
|
|
2186
|
+
return { done: true };
|
|
2187
|
+
if (silent)
|
|
2188
|
+
return { done: false, why: `was silent for ${STREAM_IDLE_MS / 1000} s`, restart: false };
|
|
2189
|
+
const e = err;
|
|
2190
|
+
return { done: false, why: `was cut off (${e?.cause?.code ?? e?.message ?? "connection lost"})`, restart: false };
|
|
2191
|
+
}
|
|
2192
|
+
if (chunk.done)
|
|
2193
|
+
break;
|
|
2194
|
+
const value = chunk.value;
|
|
2195
|
+
buf += dec.decode(value, { stream: true });
|
|
2196
|
+
let sep;
|
|
2197
|
+
while ((sep = buf.indexOf("\n\n")) >= 0) {
|
|
2198
|
+
const block = buf.slice(0, sep);
|
|
2199
|
+
buf = buf.slice(sep + 2);
|
|
2200
|
+
let event = "";
|
|
2201
|
+
let data = "";
|
|
2202
|
+
for (const line of block.split("\n")) {
|
|
2203
|
+
if (line.startsWith("event:"))
|
|
2204
|
+
event = line.slice(6).trim();
|
|
2205
|
+
else if (line.startsWith("data:"))
|
|
2206
|
+
data = line.slice(5).replace(/^ /, "");
|
|
2207
|
+
}
|
|
2208
|
+
if (!event)
|
|
2209
|
+
continue;
|
|
2210
|
+
if (event === "heartbeat") {
|
|
2211
|
+
on.heartbeat?.();
|
|
2212
|
+
continue;
|
|
2213
|
+
}
|
|
2214
|
+
if (event === "reconnect") {
|
|
2215
|
+
rd.cancel().catch(() => { });
|
|
2216
|
+
return { done: false, why: "was ended by a service restart", restart: true };
|
|
2217
|
+
}
|
|
2218
|
+
let parsed = {};
|
|
2219
|
+
try {
|
|
2220
|
+
parsed = JSON.parse(data);
|
|
2221
|
+
}
|
|
2222
|
+
catch { }
|
|
2223
|
+
if (event === "held") {
|
|
2224
|
+
const h = parsed["hold_s"];
|
|
2225
|
+
holdS = typeof h === "number" && Number.isFinite(h) && h >= 0 ? h : EXEC_HOLD_DEFAULT_S;
|
|
2226
|
+
}
|
|
2227
|
+
else if (event === "stdout" || event === "stderr") {
|
|
2228
|
+
const bytes = b64ToBytes(String(parsed["data"] ?? ""));
|
|
2229
|
+
if (event === "stdout") {
|
|
2230
|
+
received.stdout += bytes.length;
|
|
2231
|
+
out.push(bytes);
|
|
2232
|
+
on.stdout?.(bytes);
|
|
2233
|
+
}
|
|
2234
|
+
else {
|
|
2235
|
+
received.stderr += bytes.length;
|
|
2236
|
+
errOut.push(bytes);
|
|
2237
|
+
on.stderr?.(bytes);
|
|
2238
|
+
}
|
|
2239
|
+
}
|
|
2240
|
+
else if (event === "exit") {
|
|
2241
|
+
exit = { ...(exit ?? {}), ...parsed };
|
|
2242
|
+
}
|
|
2243
|
+
else if (event === "error") {
|
|
2244
|
+
failure = parsed;
|
|
2245
|
+
}
|
|
2246
|
+
}
|
|
2247
|
+
}
|
|
2248
|
+
}
|
|
2249
|
+
finally {
|
|
2250
|
+
clearTimeout(timer);
|
|
2251
|
+
}
|
|
2252
|
+
if (exit || failure)
|
|
2253
|
+
return { done: true };
|
|
2254
|
+
if (silent)
|
|
2255
|
+
return { done: false, why: `was silent for ${STREAM_IDLE_MS / 1000} s`, restart: false };
|
|
2256
|
+
return { done: false, why: "ended without an exit event", restart: false };
|
|
2257
|
+
};
|
|
2258
|
+
const reattach = async () => {
|
|
2259
|
+
const droppedAt = Date.now();
|
|
2260
|
+
const hold = holdS ?? EXEC_HOLD_DEFAULT_S;
|
|
2261
|
+
const budgetMs = hold * 1000 + REATTACH_SLACK_MS;
|
|
2262
|
+
for (let attempt = 0;; attempt++) {
|
|
2263
|
+
if (opts.signal?.aborted)
|
|
2264
|
+
return abandon(opts.signal.reason);
|
|
2265
|
+
fresh();
|
|
2266
|
+
this.stats.requests++;
|
|
2267
|
+
this.stats.retries++;
|
|
1942
2268
|
arm();
|
|
1943
|
-
let
|
|
2269
|
+
let retryAfterMs;
|
|
2270
|
+
let r;
|
|
1944
2271
|
try {
|
|
1945
|
-
|
|
2272
|
+
r = await this.fetchImpl(`${url}/attach`, {
|
|
2273
|
+
method: "POST",
|
|
2274
|
+
headers: headers(),
|
|
2275
|
+
body: JSON.stringify({ key, stdout_offset: received.stdout, stderr_offset: received.stderr }),
|
|
2276
|
+
signal: opts.signal ? AbortSignal.any([opts.signal, idle.signal]) : idle.signal,
|
|
2277
|
+
...(dispatcher === undefined ? {} : { dispatcher }),
|
|
2278
|
+
});
|
|
1946
2279
|
}
|
|
1947
2280
|
catch (err) {
|
|
2281
|
+
clearTimeout(timer);
|
|
1948
2282
|
if (opts.signal?.aborted)
|
|
1949
|
-
|
|
1950
|
-
if (silent)
|
|
1951
|
-
throw lost(`was silent for ${STREAM_IDLE_MS / 1000} s`);
|
|
1952
|
-
const e = err;
|
|
1953
|
-
throw lost(`was cut off (${e?.cause?.code ?? e?.message ?? "connection lost"})`);
|
|
2283
|
+
return abandon(err);
|
|
1954
2284
|
}
|
|
1955
|
-
if (
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
const block = buf.slice(0, sep);
|
|
1962
|
-
buf = buf.slice(sep + 2);
|
|
1963
|
-
let event = "";
|
|
1964
|
-
let data = "";
|
|
1965
|
-
for (const line of block.split("\n")) {
|
|
1966
|
-
if (line.startsWith("event:"))
|
|
1967
|
-
event = line.slice(6).trim();
|
|
1968
|
-
else if (line.startsWith("data:"))
|
|
1969
|
-
data = line.slice(5).replace(/^ /, "");
|
|
1970
|
-
}
|
|
1971
|
-
if (!event)
|
|
1972
|
-
continue;
|
|
1973
|
-
if (event === "heartbeat") {
|
|
1974
|
-
on.heartbeat?.();
|
|
1975
|
-
continue;
|
|
1976
|
-
}
|
|
1977
|
-
if (event === "reconnect") {
|
|
1978
|
-
reader.cancel().catch(() => { });
|
|
1979
|
-
throw new core_js_1.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.");
|
|
1980
|
-
}
|
|
1981
|
-
let parsed = {};
|
|
2285
|
+
if (r !== undefined) {
|
|
2286
|
+
if (r.ok && r.body)
|
|
2287
|
+
return r.body;
|
|
2288
|
+
clearTimeout(timer);
|
|
2289
|
+
const text = await r.text().catch(() => "");
|
|
2290
|
+
let payload = {};
|
|
1982
2291
|
try {
|
|
1983
|
-
|
|
2292
|
+
payload = text ? JSON.parse(text) : {};
|
|
1984
2293
|
}
|
|
1985
2294
|
catch { }
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
out.push(bytes);
|
|
1990
|
-
on.stdout?.(bytes);
|
|
1991
|
-
}
|
|
1992
|
-
else {
|
|
1993
|
-
errOut.push(bytes);
|
|
1994
|
-
on.stderr?.(bytes);
|
|
1995
|
-
}
|
|
2295
|
+
retryAfterMs = parseRetryAfter(r.headers.get("retry-after"));
|
|
2296
|
+
if (r.status === 404) {
|
|
2297
|
+
throw new core_js_1.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");
|
|
1996
2298
|
}
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
}
|
|
2000
|
-
else if (event === "error") {
|
|
2001
|
-
failure = parsed;
|
|
2299
|
+
if (!REATTACH_AGAIN.has(r.status)) {
|
|
2300
|
+
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 }) });
|
|
2002
2301
|
}
|
|
2003
2302
|
}
|
|
2303
|
+
const left = budgetMs - (Date.now() - droppedAt);
|
|
2304
|
+
if (left <= 0) {
|
|
2305
|
+
throw new core_js_1.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");
|
|
2306
|
+
}
|
|
2307
|
+
await sleepUnless(Math.min(left, reattachWait(attempt, retryAfterMs)), opts.signal);
|
|
2004
2308
|
}
|
|
2005
|
-
}
|
|
2006
|
-
|
|
2007
|
-
|
|
2309
|
+
};
|
|
2310
|
+
for (let ended = await consume(stream); !ended.done; ended = await consume(stream)) {
|
|
2311
|
+
if (holdS === undefined || key === undefined) {
|
|
2312
|
+
if (ended.restart) {
|
|
2313
|
+
throw new core_js_1.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.");
|
|
2314
|
+
}
|
|
2315
|
+
throw lost(ended.why);
|
|
2316
|
+
}
|
|
2317
|
+
stream = await reattach();
|
|
2008
2318
|
}
|
|
2009
2319
|
if (failure) {
|
|
2010
2320
|
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 }) });
|
|
2011
2321
|
}
|
|
2012
|
-
if (!exit
|
|
2013
|
-
throw lost(`was silent for ${STREAM_IDLE_MS / 1000} s`);
|
|
2014
|
-
if (!exit) {
|
|
2322
|
+
if (!exit)
|
|
2015
2323
|
throw lost("ended without an exit event");
|
|
2324
|
+
if (stoppedUnattended(exit)) {
|
|
2325
|
+
throw new core_js_1.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");
|
|
2016
2326
|
}
|
|
2017
2327
|
const rawCode = exit["exit_code"];
|
|
2018
2328
|
const signal = exit["signal"];
|
|
@@ -2133,12 +2443,14 @@ class PromigenceClient {
|
|
|
2133
2443
|
}),
|
|
2134
2444
|
},
|
|
2135
2445
|
ports: {
|
|
2136
|
-
expose: (sandboxId, port) => {
|
|
2446
|
+
expose: (sandboxId, port, opts = {}) => {
|
|
2137
2447
|
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
2138
2448
|
throw new core_js_1.InvalidArgumentError(`port must be an integer 1..65535, got ${port}`, "Pass the port your process listens on inside the sandbox, e.g. 3000.");
|
|
2139
2449
|
}
|
|
2450
|
+
checkPreviewLength(opts.length);
|
|
2140
2451
|
return (0, core_js_1.withPreviewsFix)(this.request({
|
|
2141
|
-
method: "POST", path: `/sandboxes/${encodeURIComponent(sandboxId)}/ports`,
|
|
2452
|
+
method: "POST", path: `/sandboxes/${encodeURIComponent(sandboxId)}/ports`,
|
|
2453
|
+
body: { port, ...(opts.length === undefined ? {} : { length: opts.length }) },
|
|
2142
2454
|
}), sandboxId, port);
|
|
2143
2455
|
},
|
|
2144
2456
|
list: async (sandboxId) => {
|
|
@@ -2147,10 +2459,19 @@ class PromigenceClient {
|
|
|
2147
2459
|
}), sandboxId);
|
|
2148
2460
|
return r.ports ?? [];
|
|
2149
2461
|
},
|
|
2150
|
-
revoke: (sandboxId, tokenOrPort) =>
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2462
|
+
revoke: (sandboxId, tokenOrPort) => {
|
|
2463
|
+
const which = String(tokenOrPort).trim();
|
|
2464
|
+
return /^[0-9]+$/.test(which)
|
|
2465
|
+
? this.request({
|
|
2466
|
+
method: "DELETE",
|
|
2467
|
+
path: `/sandboxes/${encodeURIComponent(sandboxId)}/ports/${which}`,
|
|
2468
|
+
})
|
|
2469
|
+
: this.request({
|
|
2470
|
+
method: "POST",
|
|
2471
|
+
path: `/sandboxes/${encodeURIComponent(sandboxId)}/ports/revoke`,
|
|
2472
|
+
body: { token: which },
|
|
2473
|
+
});
|
|
2474
|
+
},
|
|
2154
2475
|
},
|
|
2155
2476
|
ssh: {
|
|
2156
2477
|
open: (sandboxId, opts) => {
|
|
@@ -2647,10 +2968,10 @@ class PromigenceClient {
|
|
|
2647
2968
|
throw new core_js_1.InvalidArgumentError("snapshots.delete needs a snapshot", "Pass a snap_ id, an alias, or alias@commit7.");
|
|
2648
2969
|
return this.request({ method: "DELETE", path: `/snapshots/${seg(idOrAlias)}` });
|
|
2649
2970
|
},
|
|
2650
|
-
alias: (idOrAlias, alias) => this.request({
|
|
2971
|
+
alias: (idOrAlias, alias, opts = {}) => this.request({
|
|
2651
2972
|
method: "PUT",
|
|
2652
2973
|
path: `/snapshots/${seg(idOrAlias)}/alias`,
|
|
2653
|
-
body: { alias },
|
|
2974
|
+
body: { alias, ...(alias !== null && opts.moveAlias ? { move_alias: true } : {}) },
|
|
2654
2975
|
}),
|
|
2655
2976
|
waitBuild: async (buildId, opts = {}) => {
|
|
2656
2977
|
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");
|
|
@@ -2756,5 +3077,6 @@ function toCommandResult(r) {
|
|
|
2756
3077
|
stderr: r.stderr ?? "",
|
|
2757
3078
|
...(r.truncated === undefined ? {} : { truncated: r.truncated }),
|
|
2758
3079
|
...(r.exec_id === undefined ? {} : { execId: r.exec_id }),
|
|
3080
|
+
timedOut: (0, command_errors_js_1.answerTimedOut)(r),
|
|
2759
3081
|
};
|
|
2760
3082
|
}
|