@coder/ai-sdk-sandbox 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +89 -30
- package/dist/index.d.ts +68 -9
- package/dist/index.js +2097 -7
- package/package.json +19 -13
package/dist/index.js
CHANGED
|
@@ -496,9 +496,2095 @@ function waitForLocalPort(port, child, timeoutMs, abortSignal) {
|
|
|
496
496
|
});
|
|
497
497
|
}
|
|
498
498
|
|
|
499
|
+
// src/native-api.ts
|
|
500
|
+
import { readFile } from "fs/promises";
|
|
501
|
+
import { setTimeout as delay } from "timers/promises";
|
|
502
|
+
import { parse as parseYaml } from "yaml";
|
|
503
|
+
var DEFAULT_BUILD_POLL_INTERVAL_MS = 1e3;
|
|
504
|
+
var DEFAULT_BUILD_TIMEOUT_MS = 30 * 6e4;
|
|
505
|
+
var MAX_API_REDIRECTS = 10;
|
|
506
|
+
var CoderNativeApiError = class extends Error {
|
|
507
|
+
status;
|
|
508
|
+
method;
|
|
509
|
+
path;
|
|
510
|
+
detail;
|
|
511
|
+
constructor(options) {
|
|
512
|
+
super(
|
|
513
|
+
`Coder API ${options.method} ${options.path} failed (${options.status}): ${options.message}` + (options.detail ? `: ${options.detail}` : "")
|
|
514
|
+
);
|
|
515
|
+
this.name = "CoderNativeApiError";
|
|
516
|
+
this.status = options.status;
|
|
517
|
+
this.method = options.method;
|
|
518
|
+
this.path = options.path;
|
|
519
|
+
this.detail = options.detail;
|
|
520
|
+
}
|
|
521
|
+
};
|
|
522
|
+
function parseNativeWorkspaceRef(ref) {
|
|
523
|
+
const parts = ref.split("/");
|
|
524
|
+
if (parts.length > 2 || parts.some((part) => part === "")) {
|
|
525
|
+
throw new Error(`invalid workspace reference "${ref}"; expected [owner/]name[.agent]`);
|
|
526
|
+
}
|
|
527
|
+
const [first = "", second] = parts;
|
|
528
|
+
const owner = parts.length === 2 ? first : "me";
|
|
529
|
+
const nameAndAgent = second ?? first;
|
|
530
|
+
const dot = nameAndAgent.indexOf(".");
|
|
531
|
+
const name = dot === -1 ? nameAndAgent : nameAndAgent.slice(0, dot);
|
|
532
|
+
const agent = dot === -1 ? void 0 : nameAndAgent.slice(dot + 1);
|
|
533
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9_-]*$/.test(name) || agent === "") {
|
|
534
|
+
throw new Error(`invalid workspace reference "${ref}"; expected [owner/]name[.agent]`);
|
|
535
|
+
}
|
|
536
|
+
if (agent !== void 0 && !/^[A-Za-z0-9][A-Za-z0-9_-]*$/.test(agent)) {
|
|
537
|
+
throw new Error(`invalid workspace reference "${ref}"; expected [owner/]name[.agent]`);
|
|
538
|
+
}
|
|
539
|
+
return { owner, name, ...agent === void 0 ? {} : { agent } };
|
|
540
|
+
}
|
|
541
|
+
var CoderApiClient = class {
|
|
542
|
+
baseUrl;
|
|
543
|
+
token;
|
|
544
|
+
headers;
|
|
545
|
+
#fetch;
|
|
546
|
+
#buildPollIntervalMs;
|
|
547
|
+
#buildTimeoutMs;
|
|
548
|
+
#lifecycleTails = /* @__PURE__ */ new Map();
|
|
549
|
+
constructor(options) {
|
|
550
|
+
const parsed = new URL(options.url);
|
|
551
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
552
|
+
throw new Error(`Coder URL must use http or https, got ${parsed.protocol}`);
|
|
553
|
+
}
|
|
554
|
+
if (options.token === "") throw new Error("Coder session token must not be empty");
|
|
555
|
+
this.baseUrl = options.url.replace(/\/$/, "");
|
|
556
|
+
this.token = options.token;
|
|
557
|
+
this.headers = { ...options.headers };
|
|
558
|
+
this.#fetch = options.fetch ?? globalThis.fetch.bind(globalThis);
|
|
559
|
+
this.#buildPollIntervalMs = options.buildPollIntervalMs ?? DEFAULT_BUILD_POLL_INTERVAL_MS;
|
|
560
|
+
this.#buildTimeoutMs = options.buildTimeoutMs ?? DEFAULT_BUILD_TIMEOUT_MS;
|
|
561
|
+
}
|
|
562
|
+
websocketUrl(path2) {
|
|
563
|
+
const url = this.#url(path2);
|
|
564
|
+
url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
|
|
565
|
+
return url.toString();
|
|
566
|
+
}
|
|
567
|
+
websocketHeaders() {
|
|
568
|
+
return { ...this.headers, "Coder-Session-Token": this.token };
|
|
569
|
+
}
|
|
570
|
+
async workspace(ref, signal) {
|
|
571
|
+
const { owner, name } = parseNativeWorkspaceRef(ref);
|
|
572
|
+
try {
|
|
573
|
+
return await this.request(
|
|
574
|
+
"GET",
|
|
575
|
+
`/api/v2/users/${encodeURIComponent(owner)}/workspace/${encodeURIComponent(name)}`,
|
|
576
|
+
void 0,
|
|
577
|
+
signal
|
|
578
|
+
);
|
|
579
|
+
} catch (error) {
|
|
580
|
+
if (error instanceof CoderNativeApiError && error.status === 404) return null;
|
|
581
|
+
throw error;
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
async currentUserId(signal) {
|
|
585
|
+
const user = await this.request("GET", "/api/v2/users/me", void 0, signal);
|
|
586
|
+
return user.id;
|
|
587
|
+
}
|
|
588
|
+
async requireWorkspace(ref, signal) {
|
|
589
|
+
const workspace = await this.workspace(ref, signal);
|
|
590
|
+
if (workspace === null) throw new Error(`Coder workspace "${ref}" does not exist`);
|
|
591
|
+
return workspace;
|
|
592
|
+
}
|
|
593
|
+
async resolveAgent(ref, signal) {
|
|
594
|
+
const parsed = parseNativeWorkspaceRef(ref);
|
|
595
|
+
const workspace = await this.requireWorkspace(ref, signal);
|
|
596
|
+
const agents = workspace.latest_build.resources?.flatMap((resource) => resource.agents ?? []) ?? [];
|
|
597
|
+
const agent = parsed.agent === void 0 ? agents.length === 1 ? agents[0] : void 0 : agents.find((candidate) => candidate.name === parsed.agent);
|
|
598
|
+
if (agent !== void 0) return { workspace, agent };
|
|
599
|
+
if (agents.length === 0) {
|
|
600
|
+
throw new Error(`Coder workspace "${ref}" has no agents in its latest build`);
|
|
601
|
+
}
|
|
602
|
+
if (parsed.agent !== void 0) {
|
|
603
|
+
throw new Error(
|
|
604
|
+
`Coder workspace "${parsed.owner}/${parsed.name}" has no agent "${parsed.agent}"; available agents: ${agents.map((candidate) => candidate.name).join(", ")}`
|
|
605
|
+
);
|
|
606
|
+
}
|
|
607
|
+
throw new Error(
|
|
608
|
+
`Coder workspace "${ref}" has multiple agents (${agents.map((candidate) => candidate.name).join(", ")}); select one with workspace.agent`
|
|
609
|
+
);
|
|
610
|
+
}
|
|
611
|
+
async status(ref, options) {
|
|
612
|
+
const workspace = await this.workspace(ref, options?.abortSignal);
|
|
613
|
+
return workspace === null ? null : toWorkspaceStatus(workspace);
|
|
614
|
+
}
|
|
615
|
+
async start(ref, options) {
|
|
616
|
+
const signal = options?.abortSignal;
|
|
617
|
+
const initial = await this.requireWorkspace(ref, signal);
|
|
618
|
+
await this.#withLifecycleLock(initial.id, signal, async () => {
|
|
619
|
+
let workspace = await this.requireWorkspace(ref, signal);
|
|
620
|
+
for (; ; ) {
|
|
621
|
+
if (workspace.latest_build.status === "running") return;
|
|
622
|
+
if (isWorkspaceBuildInFlight(workspace.latest_build)) {
|
|
623
|
+
const transition = workspace.latest_build.transition;
|
|
624
|
+
const completed = await this.#waitForBuild(workspace.latest_build.id, signal, true);
|
|
625
|
+
if (transition === "start" && completed.job?.status === "succeeded") return;
|
|
626
|
+
workspace = await this.requireWorkspace(ref, signal);
|
|
627
|
+
continue;
|
|
628
|
+
}
|
|
629
|
+
if (workspace.latest_build.status === "failed" && workspace.latest_build.transition === "start") {
|
|
630
|
+
const cleanup = await this.#createBuild(workspace.id, { transition: "stop" }, signal);
|
|
631
|
+
await this.#waitForBuild(cleanup.id, signal);
|
|
632
|
+
workspace = await this.requireWorkspace(ref, signal);
|
|
633
|
+
continue;
|
|
634
|
+
}
|
|
635
|
+
if (workspace.dormant_at) {
|
|
636
|
+
await this.request(
|
|
637
|
+
"PUT",
|
|
638
|
+
`/api/v2/workspaces/${workspace.id}/dormant`,
|
|
639
|
+
{ dormant: false },
|
|
640
|
+
signal
|
|
641
|
+
);
|
|
642
|
+
}
|
|
643
|
+
const templateVersionId = workspace.automatic_updates === "always" || workspace.template_require_active_version ? workspace.template_active_version_id : workspace.latest_build.template_version_id;
|
|
644
|
+
const build = await this.#createBuild(
|
|
645
|
+
workspace.id,
|
|
646
|
+
{
|
|
647
|
+
transition: "start",
|
|
648
|
+
...templateVersionId ? { template_version_id: templateVersionId } : {}
|
|
649
|
+
},
|
|
650
|
+
signal
|
|
651
|
+
);
|
|
652
|
+
await this.#waitForBuild(build.id, signal);
|
|
653
|
+
return;
|
|
654
|
+
}
|
|
655
|
+
});
|
|
656
|
+
}
|
|
657
|
+
async stop(ref, options) {
|
|
658
|
+
const signal = options?.abortSignal;
|
|
659
|
+
const initial = await this.requireWorkspace(ref, signal);
|
|
660
|
+
await this.#withLifecycleLock(initial.id, signal, async () => {
|
|
661
|
+
let workspace = await this.requireWorkspace(ref, signal);
|
|
662
|
+
for (; ; ) {
|
|
663
|
+
if (workspace.latest_build.status === "stopped") return;
|
|
664
|
+
if (isWorkspaceBuildInFlight(workspace.latest_build)) {
|
|
665
|
+
const transition = workspace.latest_build.transition;
|
|
666
|
+
const completed = await this.#waitForBuild(workspace.latest_build.id, signal, true);
|
|
667
|
+
if (transition === "stop" && completed.job?.status === "succeeded") return;
|
|
668
|
+
workspace = await this.requireWorkspace(ref, signal);
|
|
669
|
+
continue;
|
|
670
|
+
}
|
|
671
|
+
const build = await this.#createBuild(workspace.id, { transition: "stop" }, signal);
|
|
672
|
+
await this.#waitForBuild(build.id, signal);
|
|
673
|
+
return;
|
|
674
|
+
}
|
|
675
|
+
});
|
|
676
|
+
}
|
|
677
|
+
async destroy(ref, options) {
|
|
678
|
+
const signal = options?.abortSignal;
|
|
679
|
+
const initial = await this.workspace(ref, signal);
|
|
680
|
+
if (initial === null) return;
|
|
681
|
+
await this.#withLifecycleLock(initial.id, signal, async () => {
|
|
682
|
+
let workspace = await this.workspace(ref, signal);
|
|
683
|
+
for (; ; ) {
|
|
684
|
+
if (workspace === null || workspace.latest_build.status === "deleted") return;
|
|
685
|
+
if (isWorkspaceBuildInFlight(workspace.latest_build)) {
|
|
686
|
+
const transition = workspace.latest_build.transition;
|
|
687
|
+
const completed = await this.#waitForBuild(workspace.latest_build.id, signal, true);
|
|
688
|
+
if (transition === "delete" && completed.job?.status === "succeeded") return;
|
|
689
|
+
workspace = await this.workspace(ref, signal);
|
|
690
|
+
continue;
|
|
691
|
+
}
|
|
692
|
+
const build = await this.#createBuild(workspace.id, { transition: "delete" }, signal);
|
|
693
|
+
await this.#waitForBuild(build.id, signal);
|
|
694
|
+
return;
|
|
695
|
+
}
|
|
696
|
+
});
|
|
697
|
+
}
|
|
698
|
+
async create(options) {
|
|
699
|
+
const ref = parseNativeWorkspaceRef(options.workspace);
|
|
700
|
+
if (ref.agent !== void 0) {
|
|
701
|
+
throw new Error("a workspace agent cannot be selected while creating a workspace");
|
|
702
|
+
}
|
|
703
|
+
const template = await this.#resolveTemplate(
|
|
704
|
+
options.template,
|
|
705
|
+
options.org,
|
|
706
|
+
options.abortSignal
|
|
707
|
+
);
|
|
708
|
+
const versionId = options.templateVersion ? (await this.request(
|
|
709
|
+
"GET",
|
|
710
|
+
`/api/v2/templates/${template.id}/versions/${encodeURIComponent(options.templateVersion)}`,
|
|
711
|
+
void 0,
|
|
712
|
+
options.abortSignal
|
|
713
|
+
)).id : template.active_version_id;
|
|
714
|
+
const noPreset = options.preset?.toLowerCase() === "none";
|
|
715
|
+
const presets = noPreset ? [] : await this.#presets(versionId, options.abortSignal);
|
|
716
|
+
const preset = options.preset === void 0 ? presets.find(presetDefault) : presets.find((candidate) => presetName(candidate) === options.preset);
|
|
717
|
+
if (options.preset !== void 0 && !noPreset && preset === void 0) {
|
|
718
|
+
throw new Error(
|
|
719
|
+
`preset "${options.preset}" not found for template "${options.template}"; available presets: ${presets.map(presetName).join(", ") || "none"}`
|
|
720
|
+
);
|
|
721
|
+
}
|
|
722
|
+
const fileParameters = options.parameterFile ? await readParameterFile(options.parameterFile) : {};
|
|
723
|
+
const parameterValues = {
|
|
724
|
+
...fileParameters,
|
|
725
|
+
...options.parameters,
|
|
726
|
+
...options.ephemeralParameters,
|
|
727
|
+
...preset ? presetParameterValues(preset) : {}
|
|
728
|
+
};
|
|
729
|
+
const templateParameters = await this.#templateParameters(
|
|
730
|
+
template,
|
|
731
|
+
versionId,
|
|
732
|
+
ref.owner,
|
|
733
|
+
parameterValues,
|
|
734
|
+
options.abortSignal
|
|
735
|
+
);
|
|
736
|
+
const resolvedParameterValues = resolveCreateParameterValues(
|
|
737
|
+
templateParameters,
|
|
738
|
+
parameterValues,
|
|
739
|
+
options.useParameterDefaults === true
|
|
740
|
+
);
|
|
741
|
+
const body = {
|
|
742
|
+
template_version_id: versionId,
|
|
743
|
+
name: ref.name,
|
|
744
|
+
...options.stopAfter ? { ttl_ms: parseDurationMillis(options.stopAfter) } : {},
|
|
745
|
+
...Object.keys(resolvedParameterValues).length > 0 ? {
|
|
746
|
+
rich_parameter_values: Object.entries(resolvedParameterValues).map(([name, value]) => ({
|
|
747
|
+
name,
|
|
748
|
+
value
|
|
749
|
+
}))
|
|
750
|
+
} : {},
|
|
751
|
+
...options.automaticUpdates ? { automatic_updates: options.automaticUpdates } : {},
|
|
752
|
+
...preset ? { template_version_preset_id: presetId(preset) } : {}
|
|
753
|
+
};
|
|
754
|
+
const workspace = await this.request(
|
|
755
|
+
"POST",
|
|
756
|
+
`/api/v2/users/${encodeURIComponent(ref.owner)}/workspaces`,
|
|
757
|
+
body,
|
|
758
|
+
options.abortSignal
|
|
759
|
+
);
|
|
760
|
+
await this.#waitForBuild(workspace.latest_build.id, options.abortSignal);
|
|
761
|
+
}
|
|
762
|
+
async listPresets(options) {
|
|
763
|
+
const template = await this.#resolveTemplate(
|
|
764
|
+
options.template,
|
|
765
|
+
options.org,
|
|
766
|
+
options.abortSignal
|
|
767
|
+
);
|
|
768
|
+
const versionId = options.templateVersion ? (await this.request(
|
|
769
|
+
"GET",
|
|
770
|
+
`/api/v2/templates/${template.id}/versions/${encodeURIComponent(options.templateVersion)}`,
|
|
771
|
+
void 0,
|
|
772
|
+
options.abortSignal
|
|
773
|
+
)).id : template.active_version_id;
|
|
774
|
+
const presets = await this.#presets(versionId, options.abortSignal);
|
|
775
|
+
return presets.map((preset) => {
|
|
776
|
+
const description = preset.Description ?? preset.description;
|
|
777
|
+
return {
|
|
778
|
+
name: presetName(preset),
|
|
779
|
+
default: preset.Default ?? preset.default ?? false,
|
|
780
|
+
...description ? { description } : {}
|
|
781
|
+
};
|
|
782
|
+
});
|
|
783
|
+
}
|
|
784
|
+
async request(method, path2, body, signal) {
|
|
785
|
+
const requestHeaders = {
|
|
786
|
+
...this.headers,
|
|
787
|
+
"Coder-Session-Token": this.token,
|
|
788
|
+
Accept: "application/json"
|
|
789
|
+
};
|
|
790
|
+
if (body !== void 0) requestHeaders["Content-Type"] = "application/json";
|
|
791
|
+
const initialUrl = this.#url(path2);
|
|
792
|
+
let requestUrl = initialUrl;
|
|
793
|
+
let requestMethod = method.toUpperCase();
|
|
794
|
+
let requestBody = body === void 0 ? void 0 : JSON.stringify(body);
|
|
795
|
+
let redirectCount = 0;
|
|
796
|
+
let response;
|
|
797
|
+
for (; ; ) {
|
|
798
|
+
response = await waitWithAbort(
|
|
799
|
+
this.#fetch(requestUrl, {
|
|
800
|
+
method: requestMethod,
|
|
801
|
+
headers: requestHeaders,
|
|
802
|
+
body: requestBody,
|
|
803
|
+
signal,
|
|
804
|
+
redirect: "manual"
|
|
805
|
+
}),
|
|
806
|
+
signal
|
|
807
|
+
);
|
|
808
|
+
const location = response.headers.get("location");
|
|
809
|
+
if (!isRedirectStatus(response.status) || location === null) break;
|
|
810
|
+
redirectCount += 1;
|
|
811
|
+
if (redirectCount > MAX_API_REDIRECTS) {
|
|
812
|
+
cancelResponseBody(response);
|
|
813
|
+
throw new Error(
|
|
814
|
+
`Coder API ${method} ${path2} exceeded ${MAX_API_REDIRECTS} same-origin redirects`
|
|
815
|
+
);
|
|
816
|
+
}
|
|
817
|
+
const redirectedUrl = new URL(location, requestUrl);
|
|
818
|
+
if (redirectedUrl.origin !== initialUrl.origin) {
|
|
819
|
+
cancelResponseBody(response);
|
|
820
|
+
throw new Error(
|
|
821
|
+
`Coder API ${method} ${path2} refused cross-origin redirect from ${initialUrl.origin} to ${redirectedUrl.origin}`
|
|
822
|
+
);
|
|
823
|
+
}
|
|
824
|
+
cancelResponseBody(response);
|
|
825
|
+
if (response.status === 303 && requestMethod !== "GET" && requestMethod !== "HEAD" || (response.status === 301 || response.status === 302) && requestMethod === "POST") {
|
|
826
|
+
requestMethod = "GET";
|
|
827
|
+
requestBody = void 0;
|
|
828
|
+
deleteRequestBodyHeaders(requestHeaders);
|
|
829
|
+
}
|
|
830
|
+
requestUrl = redirectedUrl;
|
|
831
|
+
}
|
|
832
|
+
const text = await readResponseText(response, signal);
|
|
833
|
+
let parsed;
|
|
834
|
+
try {
|
|
835
|
+
parsed = text === "" ? void 0 : JSON.parse(text);
|
|
836
|
+
} catch {
|
|
837
|
+
parsed = void 0;
|
|
838
|
+
}
|
|
839
|
+
if (!response.ok) {
|
|
840
|
+
const error = asRecord2(parsed);
|
|
841
|
+
const validations = error.validations?.map((validation) => [validation.field, validation.detail].filter(Boolean).join(": ")).filter(Boolean).join("; ");
|
|
842
|
+
throw new CoderNativeApiError({
|
|
843
|
+
status: response.status,
|
|
844
|
+
method,
|
|
845
|
+
path: path2,
|
|
846
|
+
message: error.message ?? (response.statusText || "request failed"),
|
|
847
|
+
detail: [error.detail, validations, parsed === void 0 ? text.slice(0, 500) : void 0].filter(Boolean).join("; ") || void 0
|
|
848
|
+
});
|
|
849
|
+
}
|
|
850
|
+
return parsed;
|
|
851
|
+
}
|
|
852
|
+
#url(path2) {
|
|
853
|
+
const base = this.baseUrl.endsWith("/") ? this.baseUrl : `${this.baseUrl}/`;
|
|
854
|
+
return new URL(path2.replace(/^\//, ""), base);
|
|
855
|
+
}
|
|
856
|
+
async #createBuild(workspaceId, body, signal) {
|
|
857
|
+
return await this.request(
|
|
858
|
+
"POST",
|
|
859
|
+
`/api/v2/workspaces/${workspaceId}/builds`,
|
|
860
|
+
body,
|
|
861
|
+
signal
|
|
862
|
+
);
|
|
863
|
+
}
|
|
864
|
+
async #withLifecycleLock(workspaceId, signal, operation) {
|
|
865
|
+
const previous = this.#lifecycleTails.get(workspaceId) ?? Promise.resolve();
|
|
866
|
+
let release;
|
|
867
|
+
const gate = new Promise((resolve) => {
|
|
868
|
+
release = resolve;
|
|
869
|
+
});
|
|
870
|
+
const tail = previous.then(() => gate);
|
|
871
|
+
this.#lifecycleTails.set(workspaceId, tail);
|
|
872
|
+
void tail.then(() => {
|
|
873
|
+
if (this.#lifecycleTails.get(workspaceId) === tail) {
|
|
874
|
+
this.#lifecycleTails.delete(workspaceId);
|
|
875
|
+
}
|
|
876
|
+
});
|
|
877
|
+
try {
|
|
878
|
+
await waitWithAbort(previous, signal);
|
|
879
|
+
return await operation();
|
|
880
|
+
} finally {
|
|
881
|
+
release();
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
async #waitForBuild(buildId, signal, allowCanceled = false) {
|
|
885
|
+
if (signal?.aborted) throw abortReason(signal);
|
|
886
|
+
const deadline = Date.now() + this.#buildTimeoutMs;
|
|
887
|
+
const timeoutError = () => new Error(`timed out after ${this.#buildTimeoutMs}ms waiting for Coder build ${buildId}`);
|
|
888
|
+
for (; ; ) {
|
|
889
|
+
const remaining = deadline - Date.now();
|
|
890
|
+
if (remaining <= 0) throw timeoutError();
|
|
891
|
+
const controller = new AbortController();
|
|
892
|
+
let rejectInterrupted;
|
|
893
|
+
const interrupted = new Promise((_resolve, reject) => {
|
|
894
|
+
rejectInterrupted = reject;
|
|
895
|
+
});
|
|
896
|
+
const onAbort = () => {
|
|
897
|
+
const error = abortReason(signal);
|
|
898
|
+
rejectInterrupted(error);
|
|
899
|
+
controller.abort(error);
|
|
900
|
+
};
|
|
901
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
902
|
+
if (signal?.aborted) onAbort();
|
|
903
|
+
const timer = setTimeout(() => {
|
|
904
|
+
const error = timeoutError();
|
|
905
|
+
rejectInterrupted(error);
|
|
906
|
+
controller.abort(error);
|
|
907
|
+
}, remaining);
|
|
908
|
+
let build;
|
|
909
|
+
try {
|
|
910
|
+
build = await Promise.race([
|
|
911
|
+
this.request(
|
|
912
|
+
"GET",
|
|
913
|
+
`/api/v2/workspacebuilds/${buildId}`,
|
|
914
|
+
void 0,
|
|
915
|
+
controller.signal
|
|
916
|
+
),
|
|
917
|
+
interrupted
|
|
918
|
+
]);
|
|
919
|
+
} finally {
|
|
920
|
+
clearTimeout(timer);
|
|
921
|
+
signal?.removeEventListener("abort", onAbort);
|
|
922
|
+
}
|
|
923
|
+
const jobStatus = build.job?.status;
|
|
924
|
+
if (jobStatus === "succeeded") return build;
|
|
925
|
+
if (jobStatus === "canceled" && allowCanceled) return build;
|
|
926
|
+
if (jobStatus === "failed" || jobStatus === "canceled") {
|
|
927
|
+
throw new Error(
|
|
928
|
+
`Coder workspace ${build.transition} build ${buildId} ${jobStatus}` + (build.job?.error ? `: ${build.job.error}` : "") + (build.job?.error_code ? ` (${build.job.error_code})` : "")
|
|
929
|
+
);
|
|
930
|
+
}
|
|
931
|
+
const wait = Math.min(this.#buildPollIntervalMs, Math.max(0, deadline - Date.now()));
|
|
932
|
+
if (wait <= 0) throw timeoutError();
|
|
933
|
+
try {
|
|
934
|
+
await delay(wait, void 0, { signal });
|
|
935
|
+
} catch (error) {
|
|
936
|
+
if (signal?.aborted) throw abortReason(signal);
|
|
937
|
+
throw error;
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
async #resolveTemplate(name, org, signal) {
|
|
942
|
+
const query = new URLSearchParams({ q: `exact_name:"${name}"` });
|
|
943
|
+
const templates = await this.request(
|
|
944
|
+
"GET",
|
|
945
|
+
`/api/v2/templates?${query.toString()}`,
|
|
946
|
+
void 0,
|
|
947
|
+
signal
|
|
948
|
+
);
|
|
949
|
+
const matches = templates.filter(
|
|
950
|
+
(template) => template.name === name && (org === void 0 || template.organization_id === org || template.organization_name === org)
|
|
951
|
+
);
|
|
952
|
+
const [onlyMatch] = matches;
|
|
953
|
+
if (matches.length === 1 && onlyMatch !== void 0) return onlyMatch;
|
|
954
|
+
if (matches.length === 0) {
|
|
955
|
+
throw new Error(
|
|
956
|
+
`Coder template "${name}"${org ? ` in organization "${org}"` : ""} was not found`
|
|
957
|
+
);
|
|
958
|
+
}
|
|
959
|
+
throw new Error(
|
|
960
|
+
`Coder template "${name}" is ambiguous across organizations (${matches.map((template) => template.organization_name).join(", ")}); set org`
|
|
961
|
+
);
|
|
962
|
+
}
|
|
963
|
+
async #presets(versionId, signal) {
|
|
964
|
+
const presets = await this.request(
|
|
965
|
+
"GET",
|
|
966
|
+
`/api/v2/templateversions/${versionId}/presets`,
|
|
967
|
+
void 0,
|
|
968
|
+
signal
|
|
969
|
+
);
|
|
970
|
+
return Array.isArray(presets) ? presets : [];
|
|
971
|
+
}
|
|
972
|
+
async #templateParameters(template, versionId, owner, initialValues, signal) {
|
|
973
|
+
if (template.use_classic_parameter_flow !== false) {
|
|
974
|
+
const parameters = await this.request(
|
|
975
|
+
"GET",
|
|
976
|
+
`/api/v2/templateversions/${versionId}/rich-parameters`,
|
|
977
|
+
void 0,
|
|
978
|
+
signal
|
|
979
|
+
);
|
|
980
|
+
return Array.isArray(parameters) ? parameters : [];
|
|
981
|
+
}
|
|
982
|
+
const ownerId = owner === "me" ? void 0 : (await this.request(
|
|
983
|
+
"GET",
|
|
984
|
+
`/api/v2/users/${encodeURIComponent(owner)}`,
|
|
985
|
+
void 0,
|
|
986
|
+
signal
|
|
987
|
+
)).id;
|
|
988
|
+
const evaluation = await this.request(
|
|
989
|
+
"POST",
|
|
990
|
+
`/api/v2/templateversions/${versionId}/dynamic-parameters/evaluate`,
|
|
991
|
+
{
|
|
992
|
+
id: 0,
|
|
993
|
+
inputs: initialValues,
|
|
994
|
+
...ownerId ? { owner_id: ownerId } : {}
|
|
995
|
+
},
|
|
996
|
+
signal
|
|
997
|
+
);
|
|
998
|
+
return (evaluation.parameters ?? []).map((parameter) => ({
|
|
999
|
+
name: parameter.name,
|
|
1000
|
+
...parameter.display_name ? { display_name: parameter.display_name } : {},
|
|
1001
|
+
default_value: parameter.default_value?.value ?? "",
|
|
1002
|
+
...parameter.default_value?.valid === void 0 ? {} : { default_valid: parameter.default_value.valid },
|
|
1003
|
+
required: parameter.required ?? false,
|
|
1004
|
+
ephemeral: parameter.ephemeral ?? false
|
|
1005
|
+
}));
|
|
1006
|
+
}
|
|
1007
|
+
};
|
|
1008
|
+
function toWorkspaceStatus(workspace) {
|
|
1009
|
+
const agents = workspace.latest_build.resources?.flatMap(
|
|
1010
|
+
(resource) => (resource.agents ?? []).map((agent) => ({
|
|
1011
|
+
name: agent.name,
|
|
1012
|
+
status: agent.status,
|
|
1013
|
+
lifecycleState: agent.lifecycle_state
|
|
1014
|
+
}))
|
|
1015
|
+
) ?? [];
|
|
1016
|
+
return {
|
|
1017
|
+
id: workspace.id,
|
|
1018
|
+
name: workspace.name,
|
|
1019
|
+
buildStatus: workspace.latest_build.status,
|
|
1020
|
+
transition: workspace.latest_build.transition,
|
|
1021
|
+
agents
|
|
1022
|
+
};
|
|
1023
|
+
}
|
|
1024
|
+
function presetName(preset) {
|
|
1025
|
+
return preset.Name ?? preset.name ?? "";
|
|
1026
|
+
}
|
|
1027
|
+
function presetId(preset) {
|
|
1028
|
+
const id = preset.ID ?? preset.id;
|
|
1029
|
+
if (!id) throw new Error(`Coder preset "${presetName(preset)}" has no id`);
|
|
1030
|
+
return id;
|
|
1031
|
+
}
|
|
1032
|
+
function presetDefault(preset) {
|
|
1033
|
+
return preset.Default ?? preset.default ?? false;
|
|
1034
|
+
}
|
|
1035
|
+
function presetParameterValues(preset) {
|
|
1036
|
+
const result = {};
|
|
1037
|
+
for (const parameter of preset.Parameters ?? preset.parameters ?? []) {
|
|
1038
|
+
const name = parameter.Name ?? parameter.name;
|
|
1039
|
+
const value = parameter.Value ?? parameter.value;
|
|
1040
|
+
if (name !== void 0 && value !== void 0) result[name] = value;
|
|
1041
|
+
}
|
|
1042
|
+
return result;
|
|
1043
|
+
}
|
|
1044
|
+
function resolveCreateParameterValues(parameters, supplied, useDefaults) {
|
|
1045
|
+
const resolved = { ...supplied };
|
|
1046
|
+
const required = [];
|
|
1047
|
+
const awaitingDefaults = [];
|
|
1048
|
+
for (const parameter of parameters) {
|
|
1049
|
+
if (Object.hasOwn(resolved, parameter.name)) continue;
|
|
1050
|
+
if (parameter.ephemeral && !parameter.required) continue;
|
|
1051
|
+
const name = parameter.display_name || parameter.name;
|
|
1052
|
+
if (parameter.required) {
|
|
1053
|
+
required.push(name);
|
|
1054
|
+
} else if (useDefaults && parameter.default_valid !== false) {
|
|
1055
|
+
resolved[parameter.name] = parameter.default_value;
|
|
1056
|
+
} else {
|
|
1057
|
+
awaitingDefaults.push(name);
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
if (required.length > 0) {
|
|
1061
|
+
const names = required.map((name) => `"${name}"`).join(", ");
|
|
1062
|
+
throw new Error(
|
|
1063
|
+
`required Coder workspace parameters have no defaults: ${names}; supply values with parameters, parameterFile, or a preset`
|
|
1064
|
+
);
|
|
1065
|
+
}
|
|
1066
|
+
if (awaitingDefaults.length > 0) {
|
|
1067
|
+
const names = awaitingDefaults.map((name) => `"${name}"`).join(", ");
|
|
1068
|
+
throw new Error(
|
|
1069
|
+
`Coder workspace parameters require explicit values: ${names}; supply values with parameters, parameterFile, or a preset` + (useDefaults ? "" : ", or set useParameterDefaults: true")
|
|
1070
|
+
);
|
|
1071
|
+
}
|
|
1072
|
+
return resolved;
|
|
1073
|
+
}
|
|
1074
|
+
function asRecord2(value) {
|
|
1075
|
+
return typeof value === "object" && value !== null ? value : {};
|
|
1076
|
+
}
|
|
1077
|
+
function isRedirectStatus(status) {
|
|
1078
|
+
return status === 301 || status === 302 || status === 303 || status === 307 || status === 308;
|
|
1079
|
+
}
|
|
1080
|
+
function isWorkspaceBuildInFlight(build) {
|
|
1081
|
+
return build.status === "pending" || build.status === "starting" || build.status === "stopping" || build.status === "deleting" || build.status === "canceling";
|
|
1082
|
+
}
|
|
1083
|
+
function deleteRequestBodyHeaders(headers) {
|
|
1084
|
+
const bodyHeaders = /* @__PURE__ */ new Set([
|
|
1085
|
+
"content-encoding",
|
|
1086
|
+
"content-language",
|
|
1087
|
+
"content-location",
|
|
1088
|
+
"content-type"
|
|
1089
|
+
]);
|
|
1090
|
+
for (const name of Object.keys(headers)) {
|
|
1091
|
+
if (bodyHeaders.has(name.toLowerCase())) delete headers[name];
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
function cancelResponseBody(response) {
|
|
1095
|
+
try {
|
|
1096
|
+
void response.body?.cancel().catch(() => {
|
|
1097
|
+
});
|
|
1098
|
+
} catch {
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
function abortReason(signal) {
|
|
1102
|
+
if (signal?.reason instanceof Error) return signal.reason;
|
|
1103
|
+
return new DOMException("The operation was aborted", "AbortError");
|
|
1104
|
+
}
|
|
1105
|
+
async function waitWithAbort(promise, signal) {
|
|
1106
|
+
if (signal === void 0) return await promise;
|
|
1107
|
+
if (signal.aborted) throw abortReason(signal);
|
|
1108
|
+
let onAbort;
|
|
1109
|
+
const aborted = new Promise((_resolve, reject) => {
|
|
1110
|
+
onAbort = () => reject(abortReason(signal));
|
|
1111
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
1112
|
+
});
|
|
1113
|
+
try {
|
|
1114
|
+
return await Promise.race([promise, aborted]);
|
|
1115
|
+
} finally {
|
|
1116
|
+
signal.removeEventListener("abort", onAbort);
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
async function readResponseText(response, signal) {
|
|
1120
|
+
if (response.body === null) return "";
|
|
1121
|
+
const reader = response.body.getReader();
|
|
1122
|
+
const decoder = new TextDecoder();
|
|
1123
|
+
let text = "";
|
|
1124
|
+
let releaseAfterCancel = false;
|
|
1125
|
+
try {
|
|
1126
|
+
for (; ; ) {
|
|
1127
|
+
const { done, value } = await waitWithAbort(reader.read(), signal);
|
|
1128
|
+
if (done) return text + decoder.decode();
|
|
1129
|
+
text += decoder.decode(value, { stream: true });
|
|
1130
|
+
}
|
|
1131
|
+
} catch (error) {
|
|
1132
|
+
if (!signal?.aborted) throw error;
|
|
1133
|
+
const reason = abortReason(signal);
|
|
1134
|
+
releaseAfterCancel = true;
|
|
1135
|
+
try {
|
|
1136
|
+
void reader.cancel(reason).catch(() => {
|
|
1137
|
+
}).then(() => {
|
|
1138
|
+
try {
|
|
1139
|
+
reader.releaseLock();
|
|
1140
|
+
} catch {
|
|
1141
|
+
}
|
|
1142
|
+
});
|
|
1143
|
+
} catch {
|
|
1144
|
+
}
|
|
1145
|
+
throw reason;
|
|
1146
|
+
} finally {
|
|
1147
|
+
if (!releaseAfterCancel) reader.releaseLock();
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
async function readParameterFile(path2) {
|
|
1151
|
+
let parsed;
|
|
1152
|
+
try {
|
|
1153
|
+
parsed = parseYaml(await readFile(path2, "utf8"));
|
|
1154
|
+
} catch (error) {
|
|
1155
|
+
throw new Error(`failed to parse Coder parameter file "${path2}"`, { cause: error });
|
|
1156
|
+
}
|
|
1157
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
1158
|
+
throw new Error(`Coder parameter file "${path2}" must contain a YAML mapping`);
|
|
1159
|
+
}
|
|
1160
|
+
const result = {};
|
|
1161
|
+
for (const [name, value] of Object.entries(parsed)) {
|
|
1162
|
+
if (typeof value === "string" || typeof value === "boolean" || typeof value === "number") {
|
|
1163
|
+
result[name] = String(value);
|
|
1164
|
+
} else if (Array.isArray(value)) {
|
|
1165
|
+
result[name] = JSON.stringify(value);
|
|
1166
|
+
} else {
|
|
1167
|
+
throw new Error(
|
|
1168
|
+
`invalid value for Coder parameter "${name}" in "${path2}": expected string, number, boolean, or list`
|
|
1169
|
+
);
|
|
1170
|
+
}
|
|
1171
|
+
}
|
|
1172
|
+
return result;
|
|
1173
|
+
}
|
|
1174
|
+
function parseDurationMillis(input) {
|
|
1175
|
+
const unitMillis = {
|
|
1176
|
+
ms: 1,
|
|
1177
|
+
s: 1e3,
|
|
1178
|
+
m: 6e4,
|
|
1179
|
+
h: 36e5
|
|
1180
|
+
};
|
|
1181
|
+
const pattern = /(\d+(?:\.\d+)?)(ms|s|m|h)/gy;
|
|
1182
|
+
let total = 0;
|
|
1183
|
+
let offset = 0;
|
|
1184
|
+
for (; ; ) {
|
|
1185
|
+
pattern.lastIndex = offset;
|
|
1186
|
+
const match = pattern.exec(input);
|
|
1187
|
+
if (match === null) break;
|
|
1188
|
+
const amount = match[1];
|
|
1189
|
+
const multiplier = match[2] === void 0 ? void 0 : unitMillis[match[2]];
|
|
1190
|
+
if (amount === void 0 || multiplier === void 0) break;
|
|
1191
|
+
total += Number(amount) * multiplier;
|
|
1192
|
+
offset = pattern.lastIndex;
|
|
1193
|
+
}
|
|
1194
|
+
if (offset !== input.length || offset === 0 || !Number.isFinite(total) || total < 0) {
|
|
1195
|
+
throw new Error(
|
|
1196
|
+
`invalid stopAfter duration "${input}"; expected a Go-style duration such as "8h" or "1h30m"`
|
|
1197
|
+
);
|
|
1198
|
+
}
|
|
1199
|
+
return Math.round(total);
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
// src/native-relay.ts
|
|
1203
|
+
import { randomUUID } from "crypto";
|
|
1204
|
+
import net2 from "net";
|
|
1205
|
+
import WebSocket from "ws";
|
|
1206
|
+
var RELAY_PROTOCOL_VERSION = 1;
|
|
1207
|
+
var CARRIER_HIGH_WATER_MARK = 1024 * 1024;
|
|
1208
|
+
var CARRIER_LOW_WATER_MARK = 256 * 1024;
|
|
1209
|
+
var CARRIER_DRAIN_POLL_INTERVAL_MS = 10;
|
|
1210
|
+
var PROCESS_STDIN_CHUNK_BYTES = 64 * 1024;
|
|
1211
|
+
var PROCESS_STDIN_BATCH_BYTES = 256 * 1024;
|
|
1212
|
+
var RELAY_CLOSE_GRACE_MS = 1e3;
|
|
1213
|
+
var NATIVE_RELAY_BOOTSTRAP_MARKER = "__CODER_AI_SDK_RELAY_BOOTSTRAP_READY_V1__";
|
|
1214
|
+
var BOOTSTRAP_DIAGNOSTIC_LIMIT = 500;
|
|
1215
|
+
var BOOTSTRAP_FRAME_LIMIT = 64 * 1024;
|
|
1216
|
+
var PROTOCOL_FRAME_LIMIT = 1024 * 1024;
|
|
1217
|
+
var NATIVE_RELAY_SOURCE = String.raw`'use strict';
|
|
1218
|
+
const childProcess = require('node:child_process');
|
|
1219
|
+
const fs = require('node:fs');
|
|
1220
|
+
const net = require('node:net');
|
|
1221
|
+
const os = require('node:os');
|
|
1222
|
+
const path = require('node:path');
|
|
1223
|
+
const readline = require('node:readline');
|
|
1224
|
+
const processes = new Map();
|
|
1225
|
+
const discardedProcessOutputs = new Map();
|
|
1226
|
+
const processOutputPauses = new Map();
|
|
1227
|
+
const processInputPauses = new Set();
|
|
1228
|
+
const sockets = new Map();
|
|
1229
|
+
const socketOutputPauses = new Map();
|
|
1230
|
+
const signalNumbers = os.constants.signals;
|
|
1231
|
+
const childShutdownGraceMs = 500;
|
|
1232
|
+
let outputCarrierPaused = false;
|
|
1233
|
+
function resolveExecutable(name) {
|
|
1234
|
+
for (const directory of String(process.env.PATH || '/usr/local/bin:/usr/bin:/bin').split(path.delimiter)) {
|
|
1235
|
+
const candidate = path.resolve(directory || '.', name);
|
|
1236
|
+
try { fs.accessSync(candidate, fs.constants.X_OK); return candidate; } catch (_) {}
|
|
1237
|
+
}
|
|
1238
|
+
return name;
|
|
1239
|
+
}
|
|
1240
|
+
const bashPath = resolveExecutable('bash');
|
|
1241
|
+
function processStream(child, streamName) {
|
|
1242
|
+
return streamName === 'stdout' ? child && child.stdout : streamName === 'stderr' ? child && child.stderr : undefined;
|
|
1243
|
+
}
|
|
1244
|
+
function setProcessOutputPaused(id, streamName, reason, paused) {
|
|
1245
|
+
const child = processes.get(id);
|
|
1246
|
+
const pauses = processOutputPauses.get(id);
|
|
1247
|
+
const stream = processStream(child, streamName);
|
|
1248
|
+
const reasons = pauses && pauses[streamName];
|
|
1249
|
+
if (!stream || !reasons) return;
|
|
1250
|
+
if (discardedProcessOutputs.get(id)?.has(streamName)) {
|
|
1251
|
+
reasons.clear();
|
|
1252
|
+
stream.resume();
|
|
1253
|
+
return;
|
|
1254
|
+
}
|
|
1255
|
+
if (paused) {
|
|
1256
|
+
reasons.add(reason);
|
|
1257
|
+
stream.pause();
|
|
1258
|
+
} else if (reasons.delete(reason) && reasons.size === 0) {
|
|
1259
|
+
stream.resume();
|
|
1260
|
+
}
|
|
1261
|
+
}
|
|
1262
|
+
function setSocketOutputPaused(id, reason, paused) {
|
|
1263
|
+
const socket = sockets.get(id);
|
|
1264
|
+
const reasons = socketOutputPauses.get(id);
|
|
1265
|
+
if (!socket || !reasons) return;
|
|
1266
|
+
if (paused) {
|
|
1267
|
+
reasons.add(reason);
|
|
1268
|
+
socket.pause();
|
|
1269
|
+
} else if (reasons.delete(reason) && reasons.size === 0) {
|
|
1270
|
+
socket.resume();
|
|
1271
|
+
}
|
|
1272
|
+
}
|
|
1273
|
+
function pauseOutputCarrier() {
|
|
1274
|
+
if (outputCarrierPaused) return;
|
|
1275
|
+
outputCarrierPaused = true;
|
|
1276
|
+
for (const id of processes.keys()) {
|
|
1277
|
+
setProcessOutputPaused(id, 'stdout', 'carrier', true);
|
|
1278
|
+
setProcessOutputPaused(id, 'stderr', 'carrier', true);
|
|
1279
|
+
}
|
|
1280
|
+
for (const id of sockets.keys()) setSocketOutputPaused(id, 'carrier', true);
|
|
1281
|
+
}
|
|
1282
|
+
function resumeOutputCarrier() {
|
|
1283
|
+
if (!outputCarrierPaused) return;
|
|
1284
|
+
outputCarrierPaused = false;
|
|
1285
|
+
for (const id of processes.keys()) {
|
|
1286
|
+
setProcessOutputPaused(id, 'stdout', 'carrier', false);
|
|
1287
|
+
if (outputCarrierPaused) return;
|
|
1288
|
+
setProcessOutputPaused(id, 'stderr', 'carrier', false);
|
|
1289
|
+
if (outputCarrierPaused) return;
|
|
1290
|
+
}
|
|
1291
|
+
for (const id of sockets.keys()) {
|
|
1292
|
+
setSocketOutputPaused(id, 'carrier', false);
|
|
1293
|
+
if (outputCarrierPaused) return;
|
|
1294
|
+
}
|
|
1295
|
+
}
|
|
1296
|
+
function emit(message) {
|
|
1297
|
+
if (!process.stdout.write(JSON.stringify(Object.assign({ v: 1 }, message)) + '\n')) pauseOutputCarrier();
|
|
1298
|
+
}
|
|
1299
|
+
function bytes(value) {
|
|
1300
|
+
return Buffer.from(value || '', 'base64');
|
|
1301
|
+
}
|
|
1302
|
+
function shellQuote(value) {
|
|
1303
|
+
return "'" + String(value).replace(/'/g, "'\\''") + "'";
|
|
1304
|
+
}
|
|
1305
|
+
function commandScript(message) {
|
|
1306
|
+
const directory = message.cwd ? 'cd ' + shellQuote(message.cwd) + ' && ' : '';
|
|
1307
|
+
const entries = Object.entries(message.env || {});
|
|
1308
|
+
if (entries.length === 0) return directory + message.command;
|
|
1309
|
+
const assignments = entries.map(([key, value]) => shellQuote(key + '=' + String(value))).join(' ');
|
|
1310
|
+
return directory + 'exec env ' + assignments + ' ' + shellQuote(bashPath) + ' -c ' + shellQuote(message.command);
|
|
1311
|
+
}
|
|
1312
|
+
function processExitCode(code, signal) {
|
|
1313
|
+
if (typeof code === 'number') return code;
|
|
1314
|
+
return 128 + (signalNumbers[signal] || 0);
|
|
1315
|
+
}
|
|
1316
|
+
function start(message) {
|
|
1317
|
+
if (processes.has(message.id)) {
|
|
1318
|
+
emit({ type: 'proc-error', id: message.id, message: 'duplicate process id' });
|
|
1319
|
+
return;
|
|
1320
|
+
}
|
|
1321
|
+
const args = [message.loginShell === false ? '-c' : '-lc', commandScript(message)];
|
|
1322
|
+
let child;
|
|
1323
|
+
try {
|
|
1324
|
+
child = childProcess.spawn(bashPath, args, {
|
|
1325
|
+
env: process.env,
|
|
1326
|
+
detached: process.platform !== 'win32',
|
|
1327
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
1328
|
+
});
|
|
1329
|
+
} catch (error) {
|
|
1330
|
+
emit({ type: 'proc-error', id: message.id, message: String(error && error.message || error) });
|
|
1331
|
+
return;
|
|
1332
|
+
}
|
|
1333
|
+
const discardedOutputs = new Set();
|
|
1334
|
+
processes.set(message.id, child);
|
|
1335
|
+
discardedProcessOutputs.set(message.id, discardedOutputs);
|
|
1336
|
+
processOutputPauses.set(message.id, { stdout: new Set(), stderr: new Set() });
|
|
1337
|
+
child.once('spawn', () => emit({ type: 'started', id: message.id, pid: child.pid }));
|
|
1338
|
+
child.stdout.on('data', (data) => {
|
|
1339
|
+
if (!discardedOutputs.has('stdout')) emit({ type: 'stdout', id: message.id, data: data.toString('base64') });
|
|
1340
|
+
});
|
|
1341
|
+
child.stderr.on('data', (data) => {
|
|
1342
|
+
if (!discardedOutputs.has('stderr')) emit({ type: 'stderr', id: message.id, data: data.toString('base64') });
|
|
1343
|
+
});
|
|
1344
|
+
if (outputCarrierPaused) {
|
|
1345
|
+
setProcessOutputPaused(message.id, 'stdout', 'carrier', true);
|
|
1346
|
+
setProcessOutputPaused(message.id, 'stderr', 'carrier', true);
|
|
1347
|
+
}
|
|
1348
|
+
child.once('error', (error) => {
|
|
1349
|
+
processes.delete(message.id);
|
|
1350
|
+
discardedProcessOutputs.delete(message.id);
|
|
1351
|
+
processOutputPauses.delete(message.id);
|
|
1352
|
+
processInputPauses.delete(message.id);
|
|
1353
|
+
emit({ type: 'proc-error', id: message.id, message: String(error && error.message || error) });
|
|
1354
|
+
});
|
|
1355
|
+
child.once('close', (code, signal) => {
|
|
1356
|
+
if (!processes.delete(message.id)) return;
|
|
1357
|
+
discardedProcessOutputs.delete(message.id);
|
|
1358
|
+
processOutputPauses.delete(message.id);
|
|
1359
|
+
processInputPauses.delete(message.id);
|
|
1360
|
+
emit({ type: 'exit', id: message.id, code: processExitCode(code, signal), signal: signal || undefined });
|
|
1361
|
+
});
|
|
1362
|
+
// Child exit is authoritative; a stdin EPIPE only means it stopped reading.
|
|
1363
|
+
child.stdin.on('error', () => { processInputPauses.delete(message.id); });
|
|
1364
|
+
child.stdin.once('close', () => {
|
|
1365
|
+
processInputPauses.delete(message.id);
|
|
1366
|
+
if (processes.has(message.id)) emit({ type: 'proc-stdin-close', id: message.id });
|
|
1367
|
+
});
|
|
1368
|
+
child.stdin.on('drain', () => {
|
|
1369
|
+
if (processInputPauses.delete(message.id) && processes.has(message.id)) {
|
|
1370
|
+
emit({ type: 'proc-stdin-resume', id: message.id });
|
|
1371
|
+
}
|
|
1372
|
+
});
|
|
1373
|
+
if (message.stdinMode !== 'stream') {
|
|
1374
|
+
if (message.stdin) child.stdin.write(bytes(message.stdin));
|
|
1375
|
+
child.stdin.end();
|
|
1376
|
+
}
|
|
1377
|
+
}
|
|
1378
|
+
function processStdinData(message) {
|
|
1379
|
+
const child = processes.get(message.id);
|
|
1380
|
+
if (!child || child.stdin.destroyed || child.stdin.writableEnded) return;
|
|
1381
|
+
try {
|
|
1382
|
+
if (!child.stdin.write(bytes(message.data)) && !processInputPauses.has(message.id)) {
|
|
1383
|
+
processInputPauses.add(message.id);
|
|
1384
|
+
emit({ type: 'proc-stdin-pause', id: message.id });
|
|
1385
|
+
}
|
|
1386
|
+
} catch (_) {}
|
|
1387
|
+
}
|
|
1388
|
+
function processStdinEnd(message) {
|
|
1389
|
+
const child = processes.get(message.id);
|
|
1390
|
+
if (!child || child.stdin.destroyed || child.stdin.writableEnded) return;
|
|
1391
|
+
try { child.stdin.end(); } catch (_) {}
|
|
1392
|
+
}
|
|
1393
|
+
function signalProcessGroup(pid, signal) {
|
|
1394
|
+
try {
|
|
1395
|
+
process.kill(process.platform !== 'win32' ? -pid : pid, signal);
|
|
1396
|
+
return true;
|
|
1397
|
+
} catch (_) {
|
|
1398
|
+
return false;
|
|
1399
|
+
}
|
|
1400
|
+
}
|
|
1401
|
+
function processGroupAlive(pid) {
|
|
1402
|
+
return signalProcessGroup(pid, 0);
|
|
1403
|
+
}
|
|
1404
|
+
function terminate(child, signal) {
|
|
1405
|
+
if (!child || !child.pid) return;
|
|
1406
|
+
if (signalProcessGroup(child.pid, signal)) return;
|
|
1407
|
+
try { child.kill(signal); } catch (_) {}
|
|
1408
|
+
}
|
|
1409
|
+
function discardProcessOutput(id, streamName) {
|
|
1410
|
+
const child = processes.get(id);
|
|
1411
|
+
const discardedOutputs = discardedProcessOutputs.get(id);
|
|
1412
|
+
const stream = processStream(child, streamName);
|
|
1413
|
+
if (!stream || !discardedOutputs) return;
|
|
1414
|
+
discardedOutputs.add(streamName);
|
|
1415
|
+
processOutputPauses.get(id)?.[streamName]?.clear();
|
|
1416
|
+
stream.resume();
|
|
1417
|
+
}
|
|
1418
|
+
function kill(message) {
|
|
1419
|
+
if (message.discardOutput) {
|
|
1420
|
+
discardProcessOutput(message.id, 'stdout');
|
|
1421
|
+
discardProcessOutput(message.id, 'stderr');
|
|
1422
|
+
}
|
|
1423
|
+
terminate(processes.get(message.id), message.signal || 'SIGTERM');
|
|
1424
|
+
}
|
|
1425
|
+
function tcpOpen(message) {
|
|
1426
|
+
if (sockets.has(message.id)) {
|
|
1427
|
+
emit({ type: 'tcp-error', id: message.id, message: 'duplicate socket id' });
|
|
1428
|
+
return;
|
|
1429
|
+
}
|
|
1430
|
+
let socket;
|
|
1431
|
+
try { socket = net.createConnection({ host: '127.0.0.1', port: message.port, allowHalfOpen: true }); }
|
|
1432
|
+
catch (error) {
|
|
1433
|
+
emit({ type: 'tcp-error', id: message.id, message: String(error && error.message || error) });
|
|
1434
|
+
emit({ type: 'tcp-close', id: message.id });
|
|
1435
|
+
return;
|
|
1436
|
+
}
|
|
1437
|
+
sockets.set(message.id, socket);
|
|
1438
|
+
socketOutputPauses.set(message.id, new Set());
|
|
1439
|
+
socket.once('connect', () => emit({ type: 'tcp-opened', id: message.id }));
|
|
1440
|
+
socket.on('data', (data) => emit({ type: 'tcp-data', id: message.id, data: data.toString('base64') }));
|
|
1441
|
+
socket.on('drain', () => emit({ type: 'tcp-resume', id: message.id }));
|
|
1442
|
+
socket.once('end', () => emit({ type: 'tcp-end', id: message.id }));
|
|
1443
|
+
socket.once('error', (error) => emit({ type: 'tcp-error', id: message.id, message: String(error && error.message || error) }));
|
|
1444
|
+
socket.once('close', () => {
|
|
1445
|
+
sockets.delete(message.id);
|
|
1446
|
+
socketOutputPauses.delete(message.id);
|
|
1447
|
+
emit({ type: 'tcp-close', id: message.id });
|
|
1448
|
+
});
|
|
1449
|
+
if (outputCarrierPaused) setSocketOutputPaused(message.id, 'carrier', true);
|
|
1450
|
+
}
|
|
1451
|
+
function receive(line) {
|
|
1452
|
+
if (!line) return;
|
|
1453
|
+
let message;
|
|
1454
|
+
try { message = JSON.parse(line); }
|
|
1455
|
+
catch (error) { emit({ type: 'error', message: 'invalid JSON: ' + String(error && error.message || error) }); return; }
|
|
1456
|
+
if (message.v !== 1) { emit({ type: 'error', message: 'unsupported protocol version' }); return; }
|
|
1457
|
+
switch (message.type) {
|
|
1458
|
+
case 'start': start(message); break;
|
|
1459
|
+
case 'proc-stdin': processStdinData(message); break;
|
|
1460
|
+
case 'proc-stdin-end': processStdinEnd(message); break;
|
|
1461
|
+
case 'kill': kill(message); break;
|
|
1462
|
+
case 'proc-pause': {
|
|
1463
|
+
setProcessOutputPaused(message.id, message.stream, 'host', true);
|
|
1464
|
+
break;
|
|
1465
|
+
}
|
|
1466
|
+
case 'proc-resume': {
|
|
1467
|
+
setProcessOutputPaused(message.id, message.stream, 'host', false);
|
|
1468
|
+
break;
|
|
1469
|
+
}
|
|
1470
|
+
case 'proc-discard': {
|
|
1471
|
+
discardProcessOutput(message.id, message.stream);
|
|
1472
|
+
break;
|
|
1473
|
+
}
|
|
1474
|
+
case 'tcp-open': tcpOpen(message); break;
|
|
1475
|
+
case 'tcp-data': {
|
|
1476
|
+
const socket = sockets.get(message.id);
|
|
1477
|
+
if (socket && !socket.write(bytes(message.data))) emit({ type: 'tcp-pause', id: message.id });
|
|
1478
|
+
break;
|
|
1479
|
+
}
|
|
1480
|
+
case 'tcp-end': { const socket = sockets.get(message.id); if (socket) socket.end(); break; }
|
|
1481
|
+
case 'tcp-close': { const socket = sockets.get(message.id); if (socket) socket.destroy(); break; }
|
|
1482
|
+
case 'tcp-pause': { setSocketOutputPaused(message.id, 'host', true); break; }
|
|
1483
|
+
case 'tcp-resume': { setSocketOutputPaused(message.id, 'host', false); break; }
|
|
1484
|
+
case 'ping': emit({ type: 'pong' }); break;
|
|
1485
|
+
default: emit({ type: 'error', id: message.id, message: 'unknown message type: ' + message.type });
|
|
1486
|
+
}
|
|
1487
|
+
}
|
|
1488
|
+
let shuttingDown = false;
|
|
1489
|
+
function shutdown(exitCode) {
|
|
1490
|
+
if (shuttingDown) return;
|
|
1491
|
+
shuttingDown = true;
|
|
1492
|
+
const groupPids = [];
|
|
1493
|
+
for (const [id, child] of processes) {
|
|
1494
|
+
discardProcessOutput(id, 'stdout');
|
|
1495
|
+
discardProcessOutput(id, 'stderr');
|
|
1496
|
+
if (child.pid) groupPids.push(child.pid);
|
|
1497
|
+
terminate(child, 'SIGTERM');
|
|
1498
|
+
}
|
|
1499
|
+
for (const socket of sockets.values()) socket.destroy();
|
|
1500
|
+
if (groupPids.length === 0) {
|
|
1501
|
+
process.exit(exitCode);
|
|
1502
|
+
return;
|
|
1503
|
+
}
|
|
1504
|
+
let pollTimer;
|
|
1505
|
+
let forceTimer;
|
|
1506
|
+
const finish = () => {
|
|
1507
|
+
if (pollTimer) clearInterval(pollTimer);
|
|
1508
|
+
if (forceTimer) clearTimeout(forceTimer);
|
|
1509
|
+
process.exit(exitCode);
|
|
1510
|
+
};
|
|
1511
|
+
pollTimer = setInterval(() => {
|
|
1512
|
+
if (groupPids.every((pid) => !processGroupAlive(pid))) finish();
|
|
1513
|
+
}, 10);
|
|
1514
|
+
forceTimer = setTimeout(() => {
|
|
1515
|
+
for (const pid of groupPids) signalProcessGroup(pid, 'SIGKILL');
|
|
1516
|
+
finish();
|
|
1517
|
+
}, childShutdownGraceMs);
|
|
1518
|
+
}
|
|
1519
|
+
const input = readline.createInterface({ input: process.stdin, crlfDelay: Infinity, terminal: false });
|
|
1520
|
+
process.stdout.on('drain', resumeOutputCarrier);
|
|
1521
|
+
input.on('line', receive);
|
|
1522
|
+
input.once('close', () => shutdown(0));
|
|
1523
|
+
for (const signal of ['SIGHUP', 'SIGINT', 'SIGQUIT', 'SIGTERM']) {
|
|
1524
|
+
process.once(signal, () => shutdown(processExitCode(null, signal)));
|
|
1525
|
+
}
|
|
1526
|
+
process.once('exit', () => {
|
|
1527
|
+
for (const child of processes.values()) terminate(child, 'SIGKILL');
|
|
1528
|
+
});
|
|
1529
|
+
emit({ type: 'ready', protocol: 1, pid: process.pid });
|
|
1530
|
+
`;
|
|
1531
|
+
var NativeRelay = class _NativeRelay {
|
|
1532
|
+
#websocket;
|
|
1533
|
+
#bootstrapReady = deferred();
|
|
1534
|
+
#ready = deferred();
|
|
1535
|
+
#websocketClosed = deferred();
|
|
1536
|
+
#processes = /* @__PURE__ */ new Map();
|
|
1537
|
+
#processInputs = /* @__PURE__ */ new Map();
|
|
1538
|
+
#sockets = /* @__PURE__ */ new Map();
|
|
1539
|
+
#closeListeners = /* @__PURE__ */ new Set();
|
|
1540
|
+
#buffer = "";
|
|
1541
|
+
#bootstrapOutput = "";
|
|
1542
|
+
#bootstrapReadySeen = false;
|
|
1543
|
+
#carrierPaused = false;
|
|
1544
|
+
#carrierDrainTimer;
|
|
1545
|
+
#closed = false;
|
|
1546
|
+
#readySeen = false;
|
|
1547
|
+
#closeError;
|
|
1548
|
+
#websocketClosePromise;
|
|
1549
|
+
constructor(websocket) {
|
|
1550
|
+
this.#websocket = websocket;
|
|
1551
|
+
websocket.on("message", (data) => this.#onData(data));
|
|
1552
|
+
websocket.on("error", (error) => this.#fail(toError(error)));
|
|
1553
|
+
websocket.on("close", (code, reason) => {
|
|
1554
|
+
this.#websocketClosed.resolve();
|
|
1555
|
+
const detail = Buffer.from(reason).toString("utf8");
|
|
1556
|
+
this.#fail(
|
|
1557
|
+
this.#closeError ?? new Error(`Coder native relay WebSocket closed (${code})${detail ? `: ${detail}` : ""}`)
|
|
1558
|
+
);
|
|
1559
|
+
});
|
|
1560
|
+
}
|
|
1561
|
+
static async connect(options) {
|
|
1562
|
+
if (options.signal?.aborted) throw abortError(options.signal);
|
|
1563
|
+
const query = new URLSearchParams({
|
|
1564
|
+
reconnect: randomUUID(),
|
|
1565
|
+
width: "80",
|
|
1566
|
+
height: "24",
|
|
1567
|
+
command: relayBootstrapCommand(options.nodeCommand),
|
|
1568
|
+
backend_type: "buffered"
|
|
1569
|
+
});
|
|
1570
|
+
const url = options.api.websocketUrl(
|
|
1571
|
+
`/api/v2/workspaceagents/${encodeURIComponent(options.agentId)}/pty?${query.toString()}`
|
|
1572
|
+
);
|
|
1573
|
+
const websocketOrigin = new URL(url).origin;
|
|
1574
|
+
const websocket = new WebSocket(url, {
|
|
1575
|
+
headers: options.api.websocketHeaders(),
|
|
1576
|
+
followRedirects: true,
|
|
1577
|
+
perMessageDeflate: false,
|
|
1578
|
+
finishRequest: (request, candidate) => {
|
|
1579
|
+
const candidateOrigin = new URL(candidate.url).origin;
|
|
1580
|
+
if (candidateOrigin !== websocketOrigin) {
|
|
1581
|
+
request.destroy(
|
|
1582
|
+
new Error(
|
|
1583
|
+
`Coder native relay refused cross-origin WebSocket redirect from ${websocketOrigin} to ${candidateOrigin}`
|
|
1584
|
+
)
|
|
1585
|
+
);
|
|
1586
|
+
return;
|
|
1587
|
+
}
|
|
1588
|
+
request.end();
|
|
1589
|
+
}
|
|
1590
|
+
});
|
|
1591
|
+
const relay = new _NativeRelay(websocket);
|
|
1592
|
+
const open = deferred();
|
|
1593
|
+
const onOpen = () => open.resolve();
|
|
1594
|
+
const onError = (error) => open.reject(error);
|
|
1595
|
+
websocket.once("open", onOpen);
|
|
1596
|
+
websocket.once("error", onError);
|
|
1597
|
+
const abort = () => {
|
|
1598
|
+
const error = abortError(options.signal);
|
|
1599
|
+
open.reject(error);
|
|
1600
|
+
relay.#bootstrapReady.reject(error);
|
|
1601
|
+
relay.#ready.reject(error);
|
|
1602
|
+
relay.#fail(error);
|
|
1603
|
+
};
|
|
1604
|
+
options.signal?.addEventListener("abort", abort, { once: true });
|
|
1605
|
+
const timer = setTimeout(() => {
|
|
1606
|
+
const error = relay.#connectTimeoutError(options.connectTimeoutMs);
|
|
1607
|
+
open.reject(error);
|
|
1608
|
+
relay.#bootstrapReady.reject(error);
|
|
1609
|
+
relay.#ready.reject(error);
|
|
1610
|
+
relay.#fail(error);
|
|
1611
|
+
}, options.connectTimeoutMs);
|
|
1612
|
+
timer.unref?.();
|
|
1613
|
+
try {
|
|
1614
|
+
await open.promise;
|
|
1615
|
+
websocket.off("error", onError);
|
|
1616
|
+
await relay.#bootstrapReady.promise;
|
|
1617
|
+
relay.#sendPtyData(`${Buffer.from(NATIVE_RELAY_SOURCE).toString("base64")}
|
|
1618
|
+
`);
|
|
1619
|
+
await relay.#ready.promise;
|
|
1620
|
+
return relay;
|
|
1621
|
+
} catch (error) {
|
|
1622
|
+
relay.#fail(toError(error));
|
|
1623
|
+
await relay.#closeWebSocket();
|
|
1624
|
+
throw error;
|
|
1625
|
+
} finally {
|
|
1626
|
+
clearTimeout(timer);
|
|
1627
|
+
options.signal?.removeEventListener("abort", abort);
|
|
1628
|
+
}
|
|
1629
|
+
}
|
|
1630
|
+
get closed() {
|
|
1631
|
+
return this.#closed;
|
|
1632
|
+
}
|
|
1633
|
+
startProcess(id, options, sink, loginShell) {
|
|
1634
|
+
this.#assertOpen();
|
|
1635
|
+
this.#processes.set(id, sink);
|
|
1636
|
+
if (options.stdin !== void 0) {
|
|
1637
|
+
this.#processInputs.set(id, {
|
|
1638
|
+
data: options.stdin,
|
|
1639
|
+
offset: 0,
|
|
1640
|
+
remotePaused: false,
|
|
1641
|
+
scheduled: false
|
|
1642
|
+
});
|
|
1643
|
+
}
|
|
1644
|
+
try {
|
|
1645
|
+
this.#send({
|
|
1646
|
+
type: "start",
|
|
1647
|
+
id,
|
|
1648
|
+
command: options.command,
|
|
1649
|
+
...options.workingDirectory ? { cwd: options.workingDirectory } : {},
|
|
1650
|
+
...options.env ? { env: options.env } : {},
|
|
1651
|
+
...options.stdin !== void 0 ? { stdinMode: "stream" } : {},
|
|
1652
|
+
loginShell
|
|
1653
|
+
});
|
|
1654
|
+
this.#pumpProcessInput(id);
|
|
1655
|
+
} catch (error) {
|
|
1656
|
+
this.#processes.delete(id);
|
|
1657
|
+
this.#processInputs.delete(id);
|
|
1658
|
+
throw error;
|
|
1659
|
+
}
|
|
1660
|
+
}
|
|
1661
|
+
unregisterProcess(id) {
|
|
1662
|
+
this.#processes.delete(id);
|
|
1663
|
+
this.#processInputs.delete(id);
|
|
1664
|
+
}
|
|
1665
|
+
killProcess(id, signal = "SIGTERM") {
|
|
1666
|
+
if (this.#closed) return;
|
|
1667
|
+
this.#send({ type: "kill", id, signal, discardOutput: true });
|
|
1668
|
+
}
|
|
1669
|
+
pauseProcessOutput(id, stream) {
|
|
1670
|
+
if (this.#closed) return;
|
|
1671
|
+
this.#send({ type: "proc-pause", id, stream });
|
|
1672
|
+
}
|
|
1673
|
+
resumeProcessOutput(id, stream) {
|
|
1674
|
+
if (this.#closed) return;
|
|
1675
|
+
this.#send({ type: "proc-resume", id, stream });
|
|
1676
|
+
}
|
|
1677
|
+
discardProcessOutput(id, stream) {
|
|
1678
|
+
if (this.#closed) return;
|
|
1679
|
+
this.#send({ type: "proc-discard", id, stream });
|
|
1680
|
+
}
|
|
1681
|
+
openTcp(id, port, sink) {
|
|
1682
|
+
this.#assertOpen();
|
|
1683
|
+
this.#sockets.set(id, sink);
|
|
1684
|
+
try {
|
|
1685
|
+
if (this.#carrierPaused) sink.pause("carrier");
|
|
1686
|
+
this.#send({ type: "tcp-open", id, port });
|
|
1687
|
+
} catch (error) {
|
|
1688
|
+
this.#sockets.delete(id);
|
|
1689
|
+
throw error;
|
|
1690
|
+
}
|
|
1691
|
+
}
|
|
1692
|
+
tcpData(id, data) {
|
|
1693
|
+
if (this.#closed) return;
|
|
1694
|
+
this.#send({ type: "tcp-data", id, data: Buffer.from(data).toString("base64") });
|
|
1695
|
+
}
|
|
1696
|
+
tcpEnd(id) {
|
|
1697
|
+
if (this.#closed) return;
|
|
1698
|
+
this.#send({ type: "tcp-end", id });
|
|
1699
|
+
}
|
|
1700
|
+
pauseTcp(id) {
|
|
1701
|
+
if (this.#closed) return;
|
|
1702
|
+
this.#send({ type: "tcp-pause", id });
|
|
1703
|
+
}
|
|
1704
|
+
resumeTcp(id) {
|
|
1705
|
+
if (this.#closed) return;
|
|
1706
|
+
this.#send({ type: "tcp-resume", id });
|
|
1707
|
+
}
|
|
1708
|
+
closeTcp(id) {
|
|
1709
|
+
const existed = this.#sockets.delete(id);
|
|
1710
|
+
if (existed && !this.#closed) this.#send({ type: "tcp-close", id });
|
|
1711
|
+
}
|
|
1712
|
+
onClose(listener) {
|
|
1713
|
+
if (this.#closed) {
|
|
1714
|
+
queueMicrotask(() => listener(this.#closeError));
|
|
1715
|
+
return () => {
|
|
1716
|
+
};
|
|
1717
|
+
}
|
|
1718
|
+
this.#closeListeners.add(listener);
|
|
1719
|
+
return () => this.#closeListeners.delete(listener);
|
|
1720
|
+
}
|
|
1721
|
+
async close() {
|
|
1722
|
+
if (!this.#closed) {
|
|
1723
|
+
this.#closeError = new Error("Coder native relay closed");
|
|
1724
|
+
this.#fail(this.#closeError, 1e3, "transport closed");
|
|
1725
|
+
}
|
|
1726
|
+
await this.#closeWebSocket(1e3, "transport closed");
|
|
1727
|
+
}
|
|
1728
|
+
#send(message) {
|
|
1729
|
+
this.#sendPtyData(`${JSON.stringify({ v: RELAY_PROTOCOL_VERSION, ...message })}
|
|
1730
|
+
`);
|
|
1731
|
+
}
|
|
1732
|
+
#sendPtyData(data) {
|
|
1733
|
+
if (this.#websocket.readyState !== WebSocket.OPEN) {
|
|
1734
|
+
throw this.#closeError ?? new Error("Coder native relay WebSocket is not open");
|
|
1735
|
+
}
|
|
1736
|
+
this.#websocket.send(Buffer.from(JSON.stringify({ data }), "utf8"), { binary: true });
|
|
1737
|
+
this.#updateCarrierBackpressure();
|
|
1738
|
+
}
|
|
1739
|
+
#assertOpen() {
|
|
1740
|
+
if (this.#closed || !this.#readySeen) {
|
|
1741
|
+
throw this.#closeError ?? new Error("Coder native relay is not open");
|
|
1742
|
+
}
|
|
1743
|
+
}
|
|
1744
|
+
#connectTimeoutError(timeoutMs) {
|
|
1745
|
+
const phase = this.#websocket.readyState === WebSocket.CONNECTING ? "opening the authenticated PTY WebSocket" : !this.#bootstrapReadySeen ? "waiting for the remote PTY bootstrap" : "starting the workspace relay";
|
|
1746
|
+
const output = (this.#bootstrapReadySeen ? this.#bootstrapOutput : `${this.#bootstrapOutput}${this.#buffer}`.slice(-BOOTSTRAP_DIAGNOSTIC_LIMIT)).trim();
|
|
1747
|
+
return new Error(
|
|
1748
|
+
`timed out after ${timeoutMs}ms ${phase}${output ? `; PTY output: ${output}` : ""}`
|
|
1749
|
+
);
|
|
1750
|
+
}
|
|
1751
|
+
#onData(data) {
|
|
1752
|
+
const chunk = rawDataBuffer(data).toString("utf8");
|
|
1753
|
+
this.#buffer += chunk;
|
|
1754
|
+
if (!this.#bootstrapReadySeen) {
|
|
1755
|
+
const marker = this.#buffer.indexOf(NATIVE_RELAY_BOOTSTRAP_MARKER);
|
|
1756
|
+
if (marker !== -1) {
|
|
1757
|
+
this.#bootstrapOutput = `${this.#bootstrapOutput}${this.#buffer.slice(0, marker)}`.slice(
|
|
1758
|
+
-BOOTSTRAP_DIAGNOSTIC_LIMIT
|
|
1759
|
+
);
|
|
1760
|
+
this.#buffer = this.#buffer.slice(marker + NATIVE_RELAY_BOOTSTRAP_MARKER.length);
|
|
1761
|
+
if (this.#buffer.startsWith("\r\n")) this.#buffer = this.#buffer.slice(2);
|
|
1762
|
+
else if (this.#buffer.startsWith("\n")) this.#buffer = this.#buffer.slice(1);
|
|
1763
|
+
this.#bootstrapReadySeen = true;
|
|
1764
|
+
this.#bootstrapReady.resolve();
|
|
1765
|
+
} else {
|
|
1766
|
+
const markerOverlap = NATIVE_RELAY_BOOTSTRAP_MARKER.length - 1;
|
|
1767
|
+
const consumedLength = Math.max(0, this.#buffer.length - markerOverlap);
|
|
1768
|
+
if (consumedLength > 0) {
|
|
1769
|
+
this.#bootstrapOutput = `${this.#bootstrapOutput}${this.#buffer.slice(
|
|
1770
|
+
0,
|
|
1771
|
+
consumedLength
|
|
1772
|
+
)}`.slice(-BOOTSTRAP_DIAGNOSTIC_LIMIT);
|
|
1773
|
+
this.#buffer = this.#buffer.slice(consumedLength);
|
|
1774
|
+
}
|
|
1775
|
+
return;
|
|
1776
|
+
}
|
|
1777
|
+
}
|
|
1778
|
+
for (; ; ) {
|
|
1779
|
+
const newline = this.#buffer.indexOf("\n");
|
|
1780
|
+
const frameLength = newline === -1 ? this.#buffer.length : newline;
|
|
1781
|
+
const frameLimit = this.#readySeen ? PROTOCOL_FRAME_LIMIT : BOOTSTRAP_FRAME_LIMIT;
|
|
1782
|
+
if (frameLength > frameLimit) {
|
|
1783
|
+
if (!this.#readySeen) {
|
|
1784
|
+
this.#bootstrapOutput = `${this.#bootstrapOutput}${this.#buffer.slice(
|
|
1785
|
+
-BOOTSTRAP_DIAGNOSTIC_LIMIT
|
|
1786
|
+
)}`.slice(-BOOTSTRAP_DIAGNOSTIC_LIMIT);
|
|
1787
|
+
}
|
|
1788
|
+
this.#buffer = "";
|
|
1789
|
+
this.#fail(
|
|
1790
|
+
new Error(
|
|
1791
|
+
this.#readySeen ? `Coder native relay protocol frame exceeded ${PROTOCOL_FRAME_LIMIT} characters` : `Coder native relay bootstrap frame exceeded ${BOOTSTRAP_FRAME_LIMIT} characters`
|
|
1792
|
+
)
|
|
1793
|
+
);
|
|
1794
|
+
return;
|
|
1795
|
+
}
|
|
1796
|
+
if (newline === -1) return;
|
|
1797
|
+
const line = this.#buffer.slice(0, newline).replace(/\r$/, "");
|
|
1798
|
+
this.#buffer = this.#buffer.slice(newline + 1);
|
|
1799
|
+
let message;
|
|
1800
|
+
try {
|
|
1801
|
+
message = JSON.parse(line);
|
|
1802
|
+
} catch {
|
|
1803
|
+
if (!this.#readySeen) {
|
|
1804
|
+
this.#bootstrapOutput = `${this.#bootstrapOutput}${line}
|
|
1805
|
+
`.slice(
|
|
1806
|
+
-BOOTSTRAP_DIAGNOSTIC_LIMIT
|
|
1807
|
+
);
|
|
1808
|
+
continue;
|
|
1809
|
+
}
|
|
1810
|
+
this.#fail(new Error(`invalid data from Coder native relay: ${line.slice(0, 200)}`));
|
|
1811
|
+
return;
|
|
1812
|
+
}
|
|
1813
|
+
this.#dispatch(message);
|
|
1814
|
+
}
|
|
1815
|
+
}
|
|
1816
|
+
#dispatch(message) {
|
|
1817
|
+
if (message.v !== RELAY_PROTOCOL_VERSION || typeof message.type !== "string") {
|
|
1818
|
+
this.#fail(new Error("Coder native relay returned an unsupported protocol frame"));
|
|
1819
|
+
return;
|
|
1820
|
+
}
|
|
1821
|
+
if (message.type === "ready") {
|
|
1822
|
+
if (message.pid === void 0) {
|
|
1823
|
+
this.#ready.reject(new Error("Coder native relay ready frame had no pid"));
|
|
1824
|
+
return;
|
|
1825
|
+
}
|
|
1826
|
+
this.#readySeen = true;
|
|
1827
|
+
this.#ready.resolve();
|
|
1828
|
+
return;
|
|
1829
|
+
}
|
|
1830
|
+
if (message.type === "error" && message.id === void 0) {
|
|
1831
|
+
this.#fail(new Error(`Coder native relay error: ${message.message ?? "unknown error"}`));
|
|
1832
|
+
return;
|
|
1833
|
+
}
|
|
1834
|
+
if (message.id === void 0) return;
|
|
1835
|
+
const process2 = this.#processes.get(message.id);
|
|
1836
|
+
if (process2 !== void 0) {
|
|
1837
|
+
switch (message.type) {
|
|
1838
|
+
case "started":
|
|
1839
|
+
if (message.pid !== void 0) process2.onStarted(message.pid);
|
|
1840
|
+
break;
|
|
1841
|
+
case "stdout":
|
|
1842
|
+
process2.onStdout(Buffer.from(message.data ?? "", "base64"));
|
|
1843
|
+
break;
|
|
1844
|
+
case "stderr":
|
|
1845
|
+
process2.onStderr(Buffer.from(message.data ?? "", "base64"));
|
|
1846
|
+
break;
|
|
1847
|
+
case "proc-stdin-pause": {
|
|
1848
|
+
const input = this.#processInputs.get(message.id);
|
|
1849
|
+
if (input !== void 0) input.remotePaused = true;
|
|
1850
|
+
break;
|
|
1851
|
+
}
|
|
1852
|
+
case "proc-stdin-resume": {
|
|
1853
|
+
const input = this.#processInputs.get(message.id);
|
|
1854
|
+
if (input !== void 0) {
|
|
1855
|
+
input.remotePaused = false;
|
|
1856
|
+
try {
|
|
1857
|
+
this.#pumpProcessInput(message.id);
|
|
1858
|
+
} catch (error) {
|
|
1859
|
+
this.#fail(toError(error));
|
|
1860
|
+
}
|
|
1861
|
+
}
|
|
1862
|
+
break;
|
|
1863
|
+
}
|
|
1864
|
+
case "proc-stdin-close":
|
|
1865
|
+
this.#processInputs.delete(message.id);
|
|
1866
|
+
break;
|
|
1867
|
+
case "exit":
|
|
1868
|
+
this.#processes.delete(message.id);
|
|
1869
|
+
this.#processInputs.delete(message.id);
|
|
1870
|
+
process2.onExit(message.code ?? 1);
|
|
1871
|
+
break;
|
|
1872
|
+
case "proc-error":
|
|
1873
|
+
this.#processes.delete(message.id);
|
|
1874
|
+
this.#processInputs.delete(message.id);
|
|
1875
|
+
process2.onError(new Error(message.message ?? "workspace process failed"));
|
|
1876
|
+
break;
|
|
1877
|
+
}
|
|
1878
|
+
return;
|
|
1879
|
+
}
|
|
1880
|
+
const socket = this.#sockets.get(message.id);
|
|
1881
|
+
if (socket === void 0) return;
|
|
1882
|
+
switch (message.type) {
|
|
1883
|
+
case "tcp-opened":
|
|
1884
|
+
socket.opened();
|
|
1885
|
+
break;
|
|
1886
|
+
case "tcp-data":
|
|
1887
|
+
socket.data(Buffer.from(message.data ?? "", "base64"));
|
|
1888
|
+
break;
|
|
1889
|
+
case "tcp-end":
|
|
1890
|
+
socket.end();
|
|
1891
|
+
break;
|
|
1892
|
+
case "tcp-pause":
|
|
1893
|
+
socket.pause("remote");
|
|
1894
|
+
break;
|
|
1895
|
+
case "tcp-resume":
|
|
1896
|
+
socket.resume("remote");
|
|
1897
|
+
break;
|
|
1898
|
+
case "tcp-close":
|
|
1899
|
+
this.#sockets.delete(message.id);
|
|
1900
|
+
socket.close();
|
|
1901
|
+
break;
|
|
1902
|
+
case "tcp-error":
|
|
1903
|
+
socket.error(new Error(message.message ?? "workspace TCP connection failed"));
|
|
1904
|
+
break;
|
|
1905
|
+
}
|
|
1906
|
+
}
|
|
1907
|
+
#fail(error, closeCode, closeReason) {
|
|
1908
|
+
if (this.#closed) return;
|
|
1909
|
+
this.#closed = true;
|
|
1910
|
+
this.#stopCarrierDrainTimer();
|
|
1911
|
+
this.#closeError = error;
|
|
1912
|
+
this.#bootstrapReady.reject(error);
|
|
1913
|
+
this.#ready.reject(error);
|
|
1914
|
+
for (const process2 of this.#processes.values()) process2.onError(error);
|
|
1915
|
+
this.#processes.clear();
|
|
1916
|
+
this.#processInputs.clear();
|
|
1917
|
+
for (const socket of this.#sockets.values()) socket.error(error);
|
|
1918
|
+
this.#sockets.clear();
|
|
1919
|
+
for (const listener of this.#closeListeners) listener(error);
|
|
1920
|
+
this.#closeListeners.clear();
|
|
1921
|
+
void this.#closeWebSocket(closeCode, closeReason);
|
|
1922
|
+
}
|
|
1923
|
+
#closeWebSocket(code, reason) {
|
|
1924
|
+
if (this.#websocketClosePromise !== void 0) return this.#websocketClosePromise;
|
|
1925
|
+
this.#websocketClosePromise = (async () => {
|
|
1926
|
+
if (isWebSocketClosed(this.#websocket)) return;
|
|
1927
|
+
try {
|
|
1928
|
+
if (this.#websocket.readyState === WebSocket.CONNECTING || this.#websocket.readyState === WebSocket.OPEN) {
|
|
1929
|
+
this.#websocket.close(code, reason);
|
|
1930
|
+
}
|
|
1931
|
+
} catch {
|
|
1932
|
+
this.#websocket.terminate();
|
|
1933
|
+
}
|
|
1934
|
+
if (isWebSocketClosed(this.#websocket)) return;
|
|
1935
|
+
const forceTimer = setTimeout(() => this.#websocket.terminate(), RELAY_CLOSE_GRACE_MS);
|
|
1936
|
+
try {
|
|
1937
|
+
await this.#websocketClosed.promise;
|
|
1938
|
+
} finally {
|
|
1939
|
+
clearTimeout(forceTimer);
|
|
1940
|
+
}
|
|
1941
|
+
})();
|
|
1942
|
+
return this.#websocketClosePromise;
|
|
1943
|
+
}
|
|
1944
|
+
#updateCarrierBackpressure() {
|
|
1945
|
+
if (this.#closed) return;
|
|
1946
|
+
if (!this.#carrierPaused) {
|
|
1947
|
+
if (this.#websocket.bufferedAmount < CARRIER_HIGH_WATER_MARK) return;
|
|
1948
|
+
this.#carrierPaused = true;
|
|
1949
|
+
for (const socket of this.#sockets.values()) socket.pause("carrier");
|
|
1950
|
+
this.#carrierDrainTimer = setInterval(
|
|
1951
|
+
() => this.#updateCarrierBackpressure(),
|
|
1952
|
+
CARRIER_DRAIN_POLL_INTERVAL_MS
|
|
1953
|
+
);
|
|
1954
|
+
this.#carrierDrainTimer.unref?.();
|
|
1955
|
+
return;
|
|
1956
|
+
}
|
|
1957
|
+
if (this.#websocket.bufferedAmount > CARRIER_LOW_WATER_MARK) return;
|
|
1958
|
+
this.#carrierPaused = false;
|
|
1959
|
+
this.#stopCarrierDrainTimer();
|
|
1960
|
+
for (const id of this.#processInputs.keys()) {
|
|
1961
|
+
try {
|
|
1962
|
+
this.#pumpProcessInput(id);
|
|
1963
|
+
} catch (error) {
|
|
1964
|
+
this.#fail(toError(error));
|
|
1965
|
+
return;
|
|
1966
|
+
}
|
|
1967
|
+
if (this.#carrierPaused) return;
|
|
1968
|
+
}
|
|
1969
|
+
for (const socket of this.#sockets.values()) {
|
|
1970
|
+
socket.resume("carrier");
|
|
1971
|
+
if (this.#carrierPaused) break;
|
|
1972
|
+
}
|
|
1973
|
+
}
|
|
1974
|
+
#pumpProcessInput(id) {
|
|
1975
|
+
let input = this.#processInputs.get(id);
|
|
1976
|
+
if (input?.scheduled) return;
|
|
1977
|
+
let sentBytes = 0;
|
|
1978
|
+
while (input !== void 0 && !this.#closed && !this.#carrierPaused && !input.remotePaused && sentBytes < PROCESS_STDIN_BATCH_BYTES) {
|
|
1979
|
+
const chunk = processInputChunk(input);
|
|
1980
|
+
if (chunk === void 0) {
|
|
1981
|
+
this.#send({ type: "proc-stdin-end", id });
|
|
1982
|
+
this.#processInputs.delete(id);
|
|
1983
|
+
return;
|
|
1984
|
+
}
|
|
1985
|
+
this.#send({ type: "proc-stdin", id, data: chunk.toString("base64") });
|
|
1986
|
+
sentBytes += chunk.byteLength;
|
|
1987
|
+
input = this.#processInputs.get(id);
|
|
1988
|
+
}
|
|
1989
|
+
if (input !== void 0 && !this.#closed && !this.#carrierPaused && !input.remotePaused) {
|
|
1990
|
+
input.scheduled = true;
|
|
1991
|
+
const immediate = setImmediate(() => {
|
|
1992
|
+
const scheduled = this.#processInputs.get(id);
|
|
1993
|
+
if (scheduled === void 0) return;
|
|
1994
|
+
scheduled.scheduled = false;
|
|
1995
|
+
try {
|
|
1996
|
+
this.#pumpProcessInput(id);
|
|
1997
|
+
} catch (error) {
|
|
1998
|
+
this.#fail(toError(error));
|
|
1999
|
+
}
|
|
2000
|
+
});
|
|
2001
|
+
immediate.unref?.();
|
|
2002
|
+
}
|
|
2003
|
+
}
|
|
2004
|
+
#stopCarrierDrainTimer() {
|
|
2005
|
+
if (this.#carrierDrainTimer === void 0) return;
|
|
2006
|
+
clearInterval(this.#carrierDrainTimer);
|
|
2007
|
+
this.#carrierDrainTimer = void 0;
|
|
2008
|
+
}
|
|
2009
|
+
};
|
|
2010
|
+
var NativeSpawnedProcess = class {
|
|
2011
|
+
stdout;
|
|
2012
|
+
stderr;
|
|
2013
|
+
#id = randomUUID();
|
|
2014
|
+
#completion = deferred();
|
|
2015
|
+
#started;
|
|
2016
|
+
#abortSignal;
|
|
2017
|
+
#onAbort;
|
|
2018
|
+
#stdoutController;
|
|
2019
|
+
#stderrController;
|
|
2020
|
+
#relay;
|
|
2021
|
+
#pid;
|
|
2022
|
+
#settled = false;
|
|
2023
|
+
#stdoutCanceled = false;
|
|
2024
|
+
#stderrCanceled = false;
|
|
2025
|
+
#stdoutPaused = false;
|
|
2026
|
+
#stderrPaused = false;
|
|
2027
|
+
constructor(relay, options, loginShell) {
|
|
2028
|
+
this.stdout = new ReadableStream({
|
|
2029
|
+
start: (controller) => {
|
|
2030
|
+
this.#stdoutController = controller;
|
|
2031
|
+
},
|
|
2032
|
+
pull: () => this.#resumeOutput("stdout"),
|
|
2033
|
+
cancel: () => this.#cancelOutput("stdout")
|
|
2034
|
+
});
|
|
2035
|
+
this.stderr = new ReadableStream({
|
|
2036
|
+
start: (controller) => {
|
|
2037
|
+
this.#stderrController = controller;
|
|
2038
|
+
},
|
|
2039
|
+
pull: () => this.#resumeOutput("stderr"),
|
|
2040
|
+
cancel: () => this.#cancelOutput("stderr")
|
|
2041
|
+
});
|
|
2042
|
+
this.#abortSignal = options.abortSignal;
|
|
2043
|
+
this.#onAbort = () => {
|
|
2044
|
+
const error = abortError(this.#abortSignal);
|
|
2045
|
+
this.#relay?.killProcess(this.#id);
|
|
2046
|
+
this.onError(error);
|
|
2047
|
+
};
|
|
2048
|
+
this.#abortSignal?.addEventListener("abort", this.#onAbort, { once: true });
|
|
2049
|
+
if (this.#abortSignal?.aborted) this.#onAbort();
|
|
2050
|
+
this.#started = relay.then((connected) => {
|
|
2051
|
+
if (this.#settled) return;
|
|
2052
|
+
this.#relay = connected;
|
|
2053
|
+
connected.startProcess(this.#id, options, this, loginShell);
|
|
2054
|
+
if (this.#stdoutCanceled) connected.discardProcessOutput(this.#id, "stdout");
|
|
2055
|
+
if (this.#stderrCanceled) connected.discardProcessOutput(this.#id, "stderr");
|
|
2056
|
+
}).catch((error) => this.onError(toError(error)));
|
|
2057
|
+
}
|
|
2058
|
+
get pid() {
|
|
2059
|
+
return this.#pid;
|
|
2060
|
+
}
|
|
2061
|
+
onStarted(pid) {
|
|
2062
|
+
this.#pid = pid;
|
|
2063
|
+
}
|
|
2064
|
+
onStdout(data) {
|
|
2065
|
+
this.#enqueueOutput("stdout", data);
|
|
2066
|
+
}
|
|
2067
|
+
onStderr(data) {
|
|
2068
|
+
this.#enqueueOutput("stderr", data);
|
|
2069
|
+
}
|
|
2070
|
+
onExit(code) {
|
|
2071
|
+
if (this.#settled) return;
|
|
2072
|
+
this.#settled = true;
|
|
2073
|
+
this.#cleanup();
|
|
2074
|
+
if (!this.#stdoutCanceled) this.#stdoutController.close();
|
|
2075
|
+
if (!this.#stderrCanceled) this.#stderrController.close();
|
|
2076
|
+
this.#completion.resolve({ exitCode: code });
|
|
2077
|
+
}
|
|
2078
|
+
onError(error) {
|
|
2079
|
+
if (this.#settled) return;
|
|
2080
|
+
this.#settled = true;
|
|
2081
|
+
this.#cleanup();
|
|
2082
|
+
if (!this.#stdoutCanceled) this.#stdoutController.error(error);
|
|
2083
|
+
if (!this.#stderrCanceled) this.#stderrController.error(error);
|
|
2084
|
+
this.#completion.reject(error);
|
|
2085
|
+
}
|
|
2086
|
+
wait() {
|
|
2087
|
+
return this.#completion.promise;
|
|
2088
|
+
}
|
|
2089
|
+
async kill() {
|
|
2090
|
+
if (this.#settled) return;
|
|
2091
|
+
await this.#started;
|
|
2092
|
+
this.#relay?.killProcess(this.#id);
|
|
2093
|
+
}
|
|
2094
|
+
#enqueueOutput(stream, data) {
|
|
2095
|
+
if (this.#settled || this.#outputCanceled(stream)) return;
|
|
2096
|
+
const controller = stream === "stdout" ? this.#stdoutController : this.#stderrController;
|
|
2097
|
+
controller.enqueue(data);
|
|
2098
|
+
if ((controller.desiredSize ?? 0) > 0) return;
|
|
2099
|
+
if (stream === "stdout") {
|
|
2100
|
+
if (this.#stdoutPaused) return;
|
|
2101
|
+
this.#stdoutPaused = true;
|
|
2102
|
+
} else {
|
|
2103
|
+
if (this.#stderrPaused) return;
|
|
2104
|
+
this.#stderrPaused = true;
|
|
2105
|
+
}
|
|
2106
|
+
this.#relay?.pauseProcessOutput(this.#id, stream);
|
|
2107
|
+
}
|
|
2108
|
+
#resumeOutput(stream) {
|
|
2109
|
+
if (this.#settled || this.#outputCanceled(stream)) return;
|
|
2110
|
+
if (stream === "stdout") {
|
|
2111
|
+
if (!this.#stdoutPaused) return;
|
|
2112
|
+
this.#stdoutPaused = false;
|
|
2113
|
+
} else {
|
|
2114
|
+
if (!this.#stderrPaused) return;
|
|
2115
|
+
this.#stderrPaused = false;
|
|
2116
|
+
}
|
|
2117
|
+
this.#relay?.resumeProcessOutput(this.#id, stream);
|
|
2118
|
+
}
|
|
2119
|
+
#cancelOutput(stream) {
|
|
2120
|
+
if (this.#settled || this.#outputCanceled(stream)) return;
|
|
2121
|
+
if (stream === "stdout") {
|
|
2122
|
+
this.#stdoutCanceled = true;
|
|
2123
|
+
this.#stdoutPaused = false;
|
|
2124
|
+
} else {
|
|
2125
|
+
this.#stderrCanceled = true;
|
|
2126
|
+
this.#stderrPaused = false;
|
|
2127
|
+
}
|
|
2128
|
+
this.#relay?.discardProcessOutput(this.#id, stream);
|
|
2129
|
+
}
|
|
2130
|
+
#outputCanceled(stream) {
|
|
2131
|
+
return stream === "stdout" ? this.#stdoutCanceled : this.#stderrCanceled;
|
|
2132
|
+
}
|
|
2133
|
+
#cleanup() {
|
|
2134
|
+
this.#abortSignal?.removeEventListener("abort", this.#onAbort);
|
|
2135
|
+
this.#relay?.unregisterProcess(this.#id);
|
|
2136
|
+
}
|
|
2137
|
+
};
|
|
2138
|
+
async function openNativePortForward(relay, options) {
|
|
2139
|
+
const server = net2.createServer({ allowHalfOpen: true });
|
|
2140
|
+
const sockets = /* @__PURE__ */ new Set();
|
|
2141
|
+
let closed = false;
|
|
2142
|
+
let rejectPendingBind;
|
|
2143
|
+
const closeForward = () => {
|
|
2144
|
+
if (closed) return;
|
|
2145
|
+
closed = true;
|
|
2146
|
+
for (const socket of sockets) socket.destroy();
|
|
2147
|
+
sockets.clear();
|
|
2148
|
+
if (server.listening) server.close();
|
|
2149
|
+
else server.once("listening", () => server.close());
|
|
2150
|
+
};
|
|
2151
|
+
const removeCloseListener = relay.onClose(closeForward);
|
|
2152
|
+
server.on("error", closeForward);
|
|
2153
|
+
server.on("connection", (socket) => {
|
|
2154
|
+
const id = randomUUID();
|
|
2155
|
+
sockets.add(socket);
|
|
2156
|
+
socket.pause();
|
|
2157
|
+
let opened = false;
|
|
2158
|
+
let remoteClosed = false;
|
|
2159
|
+
let remoteEnded = false;
|
|
2160
|
+
let remotePaused = false;
|
|
2161
|
+
const uploadPauses = /* @__PURE__ */ new Set();
|
|
2162
|
+
const resumeUpload = () => {
|
|
2163
|
+
if (opened && uploadPauses.size === 0 && !remoteClosed && !socket.destroyed) socket.resume();
|
|
2164
|
+
};
|
|
2165
|
+
const closeRemote = () => {
|
|
2166
|
+
if (remoteClosed) return;
|
|
2167
|
+
remoteClosed = true;
|
|
2168
|
+
relay.closeTcp(id);
|
|
2169
|
+
};
|
|
2170
|
+
socket.on("data", (data) => {
|
|
2171
|
+
if (!remoteClosed) relay.tcpData(id, data);
|
|
2172
|
+
});
|
|
2173
|
+
socket.on("end", () => {
|
|
2174
|
+
if (!remoteClosed) relay.tcpEnd(id);
|
|
2175
|
+
});
|
|
2176
|
+
socket.on("drain", () => {
|
|
2177
|
+
if (remoteClosed || !remotePaused) return;
|
|
2178
|
+
remotePaused = false;
|
|
2179
|
+
relay.resumeTcp(id);
|
|
2180
|
+
});
|
|
2181
|
+
socket.on("close", () => {
|
|
2182
|
+
sockets.delete(socket);
|
|
2183
|
+
closeRemote();
|
|
2184
|
+
});
|
|
2185
|
+
socket.on("error", closeRemote);
|
|
2186
|
+
try {
|
|
2187
|
+
relay.openTcp(id, options.remotePort, {
|
|
2188
|
+
opened: () => {
|
|
2189
|
+
opened = true;
|
|
2190
|
+
resumeUpload();
|
|
2191
|
+
},
|
|
2192
|
+
data: (data) => {
|
|
2193
|
+
if (!socket.destroyed && !socket.write(data) && !remotePaused) {
|
|
2194
|
+
remotePaused = true;
|
|
2195
|
+
relay.pauseTcp(id);
|
|
2196
|
+
}
|
|
2197
|
+
},
|
|
2198
|
+
end: () => {
|
|
2199
|
+
remoteEnded = true;
|
|
2200
|
+
socket.end();
|
|
2201
|
+
},
|
|
2202
|
+
pause: (reason) => {
|
|
2203
|
+
uploadPauses.add(reason);
|
|
2204
|
+
socket.pause();
|
|
2205
|
+
},
|
|
2206
|
+
resume: (reason) => {
|
|
2207
|
+
uploadPauses.delete(reason);
|
|
2208
|
+
resumeUpload();
|
|
2209
|
+
},
|
|
2210
|
+
close: () => {
|
|
2211
|
+
remoteClosed = true;
|
|
2212
|
+
if (socket.destroyed) return;
|
|
2213
|
+
if (remoteEnded) socket.destroySoon();
|
|
2214
|
+
else socket.destroy();
|
|
2215
|
+
},
|
|
2216
|
+
error: (error) => {
|
|
2217
|
+
remoteClosed = true;
|
|
2218
|
+
socket.destroy(error);
|
|
2219
|
+
}
|
|
2220
|
+
});
|
|
2221
|
+
} catch (error) {
|
|
2222
|
+
socket.destroy(toError(error));
|
|
2223
|
+
}
|
|
2224
|
+
});
|
|
2225
|
+
const onAbort = () => {
|
|
2226
|
+
removeCloseListener();
|
|
2227
|
+
rejectPendingBind?.(abortError(options.abortSignal));
|
|
2228
|
+
closeForward();
|
|
2229
|
+
};
|
|
2230
|
+
options.abortSignal?.addEventListener("abort", onAbort, { once: true });
|
|
2231
|
+
if (options.abortSignal?.aborted) {
|
|
2232
|
+
removeCloseListener();
|
|
2233
|
+
throw abortError(options.abortSignal);
|
|
2234
|
+
}
|
|
2235
|
+
try {
|
|
2236
|
+
await new Promise((resolve, reject) => {
|
|
2237
|
+
const onError = (error) => {
|
|
2238
|
+
rejectPendingBind = void 0;
|
|
2239
|
+
reject(error);
|
|
2240
|
+
};
|
|
2241
|
+
rejectPendingBind = (error) => {
|
|
2242
|
+
server.off("error", onError);
|
|
2243
|
+
rejectPendingBind = void 0;
|
|
2244
|
+
reject(error);
|
|
2245
|
+
};
|
|
2246
|
+
server.once("error", onError);
|
|
2247
|
+
server.listen(0, "127.0.0.1", () => {
|
|
2248
|
+
server.off("error", onError);
|
|
2249
|
+
rejectPendingBind = void 0;
|
|
2250
|
+
resolve();
|
|
2251
|
+
});
|
|
2252
|
+
});
|
|
2253
|
+
} catch (error) {
|
|
2254
|
+
closeForward();
|
|
2255
|
+
removeCloseListener();
|
|
2256
|
+
options.abortSignal?.removeEventListener("abort", onAbort);
|
|
2257
|
+
throw error;
|
|
2258
|
+
}
|
|
2259
|
+
const address = server.address();
|
|
2260
|
+
if (address === null || typeof address === "string") {
|
|
2261
|
+
closeForward();
|
|
2262
|
+
removeCloseListener();
|
|
2263
|
+
throw new Error("failed to allocate a local port for the Coder native forward");
|
|
2264
|
+
}
|
|
2265
|
+
return {
|
|
2266
|
+
localHost: "127.0.0.1",
|
|
2267
|
+
localPort: address.port,
|
|
2268
|
+
get closed() {
|
|
2269
|
+
return closed || relay.closed;
|
|
2270
|
+
},
|
|
2271
|
+
close: async () => {
|
|
2272
|
+
options.abortSignal?.removeEventListener("abort", onAbort);
|
|
2273
|
+
removeCloseListener();
|
|
2274
|
+
closeForward();
|
|
2275
|
+
}
|
|
2276
|
+
};
|
|
2277
|
+
}
|
|
2278
|
+
function relayBootstrapCommand(nodeCommand) {
|
|
2279
|
+
const script = `stty raw -echo; printf '%s\\n' ${shellQuote(NATIVE_RELAY_BOOTSTRAP_MARKER)}; IFS= read -r CODER_AI_SDK_RELAY_PAYLOAD; exec ${shellQuote(nodeCommand)} -e "$(printf %s "$CODER_AI_SDK_RELAY_PAYLOAD" | base64 -d)"`;
|
|
2280
|
+
return `exec bash -lc ${shellQuote(script)}`;
|
|
2281
|
+
}
|
|
2282
|
+
function rawDataBuffer(data) {
|
|
2283
|
+
if (Buffer.isBuffer(data)) return data;
|
|
2284
|
+
if (Array.isArray(data)) return Buffer.concat(data);
|
|
2285
|
+
return Buffer.from(data);
|
|
2286
|
+
}
|
|
2287
|
+
function processInputChunk(input) {
|
|
2288
|
+
if (typeof input.data === "string") {
|
|
2289
|
+
if (input.offset >= input.data.length) return void 0;
|
|
2290
|
+
const maxCodeUnits = Math.max(1, Math.floor(PROCESS_STDIN_CHUNK_BYTES / 4));
|
|
2291
|
+
let end2 = Math.min(input.data.length, input.offset + maxCodeUnits);
|
|
2292
|
+
if (end2 < input.data.length && isHighSurrogate(input.data.charCodeAt(end2 - 1)) && isLowSurrogate(input.data.charCodeAt(end2))) {
|
|
2293
|
+
end2 -= 1;
|
|
2294
|
+
}
|
|
2295
|
+
const chunk2 = Buffer.from(input.data.slice(input.offset, end2));
|
|
2296
|
+
input.offset = end2;
|
|
2297
|
+
return chunk2;
|
|
2298
|
+
}
|
|
2299
|
+
if (input.offset >= input.data.byteLength) return void 0;
|
|
2300
|
+
const end = Math.min(input.data.byteLength, input.offset + PROCESS_STDIN_CHUNK_BYTES);
|
|
2301
|
+
const chunk = Buffer.from(
|
|
2302
|
+
input.data.buffer,
|
|
2303
|
+
input.data.byteOffset + input.offset,
|
|
2304
|
+
end - input.offset
|
|
2305
|
+
);
|
|
2306
|
+
input.offset = end;
|
|
2307
|
+
return chunk;
|
|
2308
|
+
}
|
|
2309
|
+
function isHighSurrogate(value) {
|
|
2310
|
+
return value >= 55296 && value <= 56319;
|
|
2311
|
+
}
|
|
2312
|
+
function isLowSurrogate(value) {
|
|
2313
|
+
return value >= 56320 && value <= 57343;
|
|
2314
|
+
}
|
|
2315
|
+
function isWebSocketClosed(websocket) {
|
|
2316
|
+
return websocket.readyState === WebSocket.CLOSED;
|
|
2317
|
+
}
|
|
2318
|
+
function deferred() {
|
|
2319
|
+
let resolve;
|
|
2320
|
+
let reject;
|
|
2321
|
+
const promise = new Promise((res, rej) => {
|
|
2322
|
+
resolve = res;
|
|
2323
|
+
reject = rej;
|
|
2324
|
+
});
|
|
2325
|
+
void promise.catch(() => {
|
|
2326
|
+
});
|
|
2327
|
+
return { promise, resolve, reject };
|
|
2328
|
+
}
|
|
2329
|
+
function toError(error) {
|
|
2330
|
+
return error instanceof Error ? error : new Error(String(error));
|
|
2331
|
+
}
|
|
2332
|
+
function abortError(signal) {
|
|
2333
|
+
if (signal?.reason instanceof Error) return signal.reason;
|
|
2334
|
+
return new DOMException("The operation was aborted", "AbortError");
|
|
2335
|
+
}
|
|
2336
|
+
|
|
2337
|
+
// src/native-transport.ts
|
|
2338
|
+
var DEFAULT_RELAY_CONNECT_TIMEOUT_MS = 3e4;
|
|
2339
|
+
var CoderNativeTransport = class {
|
|
2340
|
+
#api;
|
|
2341
|
+
#loginShell;
|
|
2342
|
+
#relayNodeCommand;
|
|
2343
|
+
#relayConnectTimeoutMs;
|
|
2344
|
+
#relays = /* @__PURE__ */ new Map();
|
|
2345
|
+
constructor(options = {}) {
|
|
2346
|
+
const url = options.url ?? process.env.CODER_URL;
|
|
2347
|
+
const token = options.token ?? process.env.CODER_SESSION_TOKEN;
|
|
2348
|
+
if (!url) {
|
|
2349
|
+
throw new Error("CoderNativeTransport requires a Coder URL; pass { url } or set CODER_URL");
|
|
2350
|
+
}
|
|
2351
|
+
if (!token) {
|
|
2352
|
+
throw new Error(
|
|
2353
|
+
"CoderNativeTransport requires a session token; pass { token } or set CODER_SESSION_TOKEN"
|
|
2354
|
+
);
|
|
2355
|
+
}
|
|
2356
|
+
this.#api = new CoderApiClient({
|
|
2357
|
+
url,
|
|
2358
|
+
token,
|
|
2359
|
+
fetch: options.fetch,
|
|
2360
|
+
headers: options.headers,
|
|
2361
|
+
buildPollIntervalMs: options.buildPollIntervalMs,
|
|
2362
|
+
buildTimeoutMs: options.buildTimeoutMs
|
|
2363
|
+
});
|
|
2364
|
+
this.#loginShell = options.loginShell ?? true;
|
|
2365
|
+
this.#relayNodeCommand = options.relayNodeCommand ?? "node";
|
|
2366
|
+
this.#relayConnectTimeoutMs = options.relayConnectTimeoutMs ?? DEFAULT_RELAY_CONNECT_TIMEOUT_MS;
|
|
2367
|
+
}
|
|
2368
|
+
async exec(options) {
|
|
2369
|
+
const process2 = this.spawn(options);
|
|
2370
|
+
const [stdout, stderr, result] = await Promise.all([
|
|
2371
|
+
drain(process2.stdout),
|
|
2372
|
+
drain(process2.stderr),
|
|
2373
|
+
process2.wait()
|
|
2374
|
+
]);
|
|
2375
|
+
return { exitCode: result.exitCode, stdout, stderr };
|
|
2376
|
+
}
|
|
2377
|
+
spawn(options) {
|
|
2378
|
+
return new NativeSpawnedProcess(
|
|
2379
|
+
this.#relayFor(options.workspace, options.abortSignal),
|
|
2380
|
+
options,
|
|
2381
|
+
this.#loginShell
|
|
2382
|
+
);
|
|
2383
|
+
}
|
|
2384
|
+
async forwardPort(options) {
|
|
2385
|
+
if (!Number.isInteger(options.remotePort) || options.remotePort < 1 || options.remotePort > 65535) {
|
|
2386
|
+
throw new Error(
|
|
2387
|
+
`invalid Coder workspace port ${options.remotePort}; expected an integer from 1 to 65535`
|
|
2388
|
+
);
|
|
2389
|
+
}
|
|
2390
|
+
const relay = await this.#relayFor(options.workspace, options.abortSignal);
|
|
2391
|
+
return await openNativePortForward(relay, options);
|
|
2392
|
+
}
|
|
2393
|
+
async start(workspace, options) {
|
|
2394
|
+
const current = await this.#api.workspace(workspace, options?.abortSignal);
|
|
2395
|
+
if (current?.latest_build.status !== "running") {
|
|
2396
|
+
await this.#closeWorkspaceRelays(
|
|
2397
|
+
workspace,
|
|
2398
|
+
current?.id,
|
|
2399
|
+
current?.owner_name,
|
|
2400
|
+
current?.owner_id,
|
|
2401
|
+
options?.abortSignal
|
|
2402
|
+
);
|
|
2403
|
+
}
|
|
2404
|
+
await this.#api.start(workspace, options);
|
|
2405
|
+
}
|
|
2406
|
+
async stop(workspace, options) {
|
|
2407
|
+
const current = await this.#api.workspace(workspace, options?.abortSignal);
|
|
2408
|
+
await this.#closeWorkspaceRelays(
|
|
2409
|
+
workspace,
|
|
2410
|
+
current?.id,
|
|
2411
|
+
current?.owner_name,
|
|
2412
|
+
current?.owner_id,
|
|
2413
|
+
options?.abortSignal
|
|
2414
|
+
);
|
|
2415
|
+
await this.#api.stop(workspace, options);
|
|
2416
|
+
}
|
|
2417
|
+
async destroy(workspace, options) {
|
|
2418
|
+
const current = await this.#api.workspace(workspace, options?.abortSignal);
|
|
2419
|
+
await this.#closeWorkspaceRelays(
|
|
2420
|
+
workspace,
|
|
2421
|
+
current?.id,
|
|
2422
|
+
current?.owner_name,
|
|
2423
|
+
current?.owner_id,
|
|
2424
|
+
options?.abortSignal
|
|
2425
|
+
);
|
|
2426
|
+
await this.#api.destroy(workspace, options);
|
|
2427
|
+
}
|
|
2428
|
+
status(workspace, options) {
|
|
2429
|
+
return this.#api.status(workspace, options);
|
|
2430
|
+
}
|
|
2431
|
+
create(options) {
|
|
2432
|
+
return this.#api.create(options);
|
|
2433
|
+
}
|
|
2434
|
+
listPresets(options) {
|
|
2435
|
+
return this.#api.listPresets(options);
|
|
2436
|
+
}
|
|
2437
|
+
/** Close every cached workspace relay. Existing local port-forwards close too. */
|
|
2438
|
+
async close() {
|
|
2439
|
+
const setups = [...new Set(this.#relays.values())];
|
|
2440
|
+
this.#relays.clear();
|
|
2441
|
+
const error = new Error("Coder native transport closed");
|
|
2442
|
+
for (const setup of setups) setup.controller.abort(error);
|
|
2443
|
+
const settled = await Promise.allSettled(setups.map((setup) => setup.promise));
|
|
2444
|
+
await Promise.all(
|
|
2445
|
+
settled.filter(
|
|
2446
|
+
(result) => result.status === "fulfilled"
|
|
2447
|
+
).map((result) => result.value.relay.close())
|
|
2448
|
+
);
|
|
2449
|
+
}
|
|
2450
|
+
async #relayFor(workspace, signal) {
|
|
2451
|
+
if (signal?.aborted) throw abortError2(signal);
|
|
2452
|
+
const referenceKey = relayReferenceKey(workspace);
|
|
2453
|
+
const existing = this.#relays.get(referenceKey);
|
|
2454
|
+
if (existing !== void 0) {
|
|
2455
|
+
const entry = await waitWithAbort2(existing.promise, signal);
|
|
2456
|
+
if (!entry.relay.closed) return entry.relay;
|
|
2457
|
+
this.#removeRelaySetup(existing);
|
|
2458
|
+
}
|
|
2459
|
+
const controller = new AbortController();
|
|
2460
|
+
const workspaceKey = canonicalWorkspaceKey(workspace);
|
|
2461
|
+
let setup;
|
|
2462
|
+
const promise = (async () => {
|
|
2463
|
+
try {
|
|
2464
|
+
const resolved = await waitWithAbort2(
|
|
2465
|
+
this.#api.resolveAgent(workspace, controller.signal),
|
|
2466
|
+
controller.signal
|
|
2467
|
+
);
|
|
2468
|
+
setup.workspaceId = resolved.workspace.id;
|
|
2469
|
+
setup.workspaceKey = canonicalWorkspaceKey(workspace, resolved.workspace.owner_name);
|
|
2470
|
+
const agentKey = relayAgentKey(resolved.workspace.id, resolved.agent.id);
|
|
2471
|
+
const shared = this.#relays.get(agentKey);
|
|
2472
|
+
if (shared !== void 0 && shared !== setup) {
|
|
2473
|
+
this.#relays.set(referenceKey, shared);
|
|
2474
|
+
const entry = await waitWithAbort2(shared.promise, controller.signal);
|
|
2475
|
+
if (!entry.relay.closed) return entry;
|
|
2476
|
+
this.#removeRelaySetup(shared);
|
|
2477
|
+
this.#relays.set(referenceKey, setup);
|
|
2478
|
+
}
|
|
2479
|
+
this.#relays.set(agentKey, setup);
|
|
2480
|
+
if (resolved.workspace.latest_build.status !== "running") {
|
|
2481
|
+
throw new Error(
|
|
2482
|
+
`Coder workspace "${workspace}" is ${resolved.workspace.latest_build.status}; start it before connecting`
|
|
2483
|
+
);
|
|
2484
|
+
}
|
|
2485
|
+
if (resolved.agent.status !== "connected") {
|
|
2486
|
+
throw new Error(
|
|
2487
|
+
`Coder workspace agent "${resolved.agent.name}" is ${resolved.agent.status}; wait for it to connect`
|
|
2488
|
+
);
|
|
2489
|
+
}
|
|
2490
|
+
const relay = await NativeRelay.connect({
|
|
2491
|
+
api: this.#api,
|
|
2492
|
+
agentId: resolved.agent.id,
|
|
2493
|
+
nodeCommand: this.#relayNodeCommand,
|
|
2494
|
+
connectTimeoutMs: this.#relayConnectTimeoutMs,
|
|
2495
|
+
signal: controller.signal
|
|
2496
|
+
});
|
|
2497
|
+
relay.onClose(() => {
|
|
2498
|
+
this.#removeRelaySetup(setup);
|
|
2499
|
+
});
|
|
2500
|
+
return { workspaceId: resolved.workspace.id, relay };
|
|
2501
|
+
} catch (error) {
|
|
2502
|
+
this.#removeRelaySetup(setup);
|
|
2503
|
+
throw error;
|
|
2504
|
+
}
|
|
2505
|
+
})();
|
|
2506
|
+
setup = { controller, promise, workspaceKey };
|
|
2507
|
+
this.#relays.set(referenceKey, setup);
|
|
2508
|
+
return (await waitWithAbort2(promise, signal)).relay;
|
|
2509
|
+
}
|
|
2510
|
+
async #closeWorkspaceRelays(workspace, workspaceId, workspaceOwner, workspaceOwnerId, signal) {
|
|
2511
|
+
const parsedWorkspace = parseNativeWorkspaceRef(workspace);
|
|
2512
|
+
const requestedWorkspaceKey = canonicalWorkspaceKey(workspace);
|
|
2513
|
+
const workspaceKey = canonicalWorkspaceKey(workspace, workspaceOwner);
|
|
2514
|
+
const workspaceKeys = /* @__PURE__ */ new Set([requestedWorkspaceKey, workspaceKey]);
|
|
2515
|
+
const meWorkspaceKey = `me/${parsedWorkspace.name}`;
|
|
2516
|
+
const setups = [...new Set(this.#relays.values())];
|
|
2517
|
+
const hasUnresolvedMeSetup = setups.some(
|
|
2518
|
+
(setup) => setup.workspaceId === void 0 && setup.workspaceKey === meWorkspaceKey
|
|
2519
|
+
);
|
|
2520
|
+
if (parsedWorkspace.owner !== "me" && workspaceOwnerId !== void 0 && hasUnresolvedMeSetup && await waitWithAbort2(this.#api.currentUserId(signal), signal) === workspaceOwnerId) {
|
|
2521
|
+
workspaceKeys.add(meWorkspaceKey);
|
|
2522
|
+
}
|
|
2523
|
+
const entries = setups.filter(
|
|
2524
|
+
(setup) => workspaceKeys.has(setup.workspaceKey) || workspaceId !== void 0 && setup.workspaceId === workspaceId
|
|
2525
|
+
);
|
|
2526
|
+
const error = new Error(`Coder native relay closed for workspace "${workspace}" lifecycle`);
|
|
2527
|
+
for (const setup of entries) {
|
|
2528
|
+
this.#removeRelaySetup(setup);
|
|
2529
|
+
setup.controller.abort(error);
|
|
2530
|
+
}
|
|
2531
|
+
const closing = Promise.allSettled(entries.map((setup) => setup.promise)).then(
|
|
2532
|
+
async (settled) => await Promise.all(
|
|
2533
|
+
settled.filter(
|
|
2534
|
+
(result) => result.status === "fulfilled"
|
|
2535
|
+
).map((result) => result.value.relay.close())
|
|
2536
|
+
)
|
|
2537
|
+
);
|
|
2538
|
+
await waitWithAbort2(closing, signal);
|
|
2539
|
+
}
|
|
2540
|
+
#removeRelaySetup(setup) {
|
|
2541
|
+
for (const [key, candidate] of this.#relays) {
|
|
2542
|
+
if (candidate === setup) this.#relays.delete(key);
|
|
2543
|
+
}
|
|
2544
|
+
}
|
|
2545
|
+
};
|
|
2546
|
+
function relayReferenceKey(workspace) {
|
|
2547
|
+
return `reference:${workspace}`;
|
|
2548
|
+
}
|
|
2549
|
+
function relayAgentKey(workspaceId, agentId) {
|
|
2550
|
+
return `agent:${workspaceId}:${agentId}`;
|
|
2551
|
+
}
|
|
2552
|
+
function canonicalWorkspaceKey(workspace, resolvedOwner) {
|
|
2553
|
+
const { owner, name } = parseNativeWorkspaceRef(workspace);
|
|
2554
|
+
return `${resolvedOwner ?? owner}/${name}`;
|
|
2555
|
+
}
|
|
2556
|
+
async function drain(stream) {
|
|
2557
|
+
const reader = stream.getReader();
|
|
2558
|
+
const chunks = [];
|
|
2559
|
+
for (; ; ) {
|
|
2560
|
+
const { done, value } = await reader.read();
|
|
2561
|
+
if (done) break;
|
|
2562
|
+
if (value !== void 0) chunks.push(value);
|
|
2563
|
+
}
|
|
2564
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
2565
|
+
}
|
|
2566
|
+
async function waitWithAbort2(promise, signal) {
|
|
2567
|
+
if (signal === void 0) return await promise;
|
|
2568
|
+
if (signal.aborted) throw abortError2(signal);
|
|
2569
|
+
let onAbort;
|
|
2570
|
+
const aborted = new Promise((_resolve, reject) => {
|
|
2571
|
+
onAbort = () => reject(abortError2(signal));
|
|
2572
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
2573
|
+
});
|
|
2574
|
+
try {
|
|
2575
|
+
return await Promise.race([promise, aborted]);
|
|
2576
|
+
} finally {
|
|
2577
|
+
signal.removeEventListener("abort", onAbort);
|
|
2578
|
+
}
|
|
2579
|
+
}
|
|
2580
|
+
function abortError2(signal) {
|
|
2581
|
+
if (signal.reason instanceof Error) return signal.reason;
|
|
2582
|
+
return new DOMException("The operation was aborted", "AbortError");
|
|
2583
|
+
}
|
|
2584
|
+
|
|
499
2585
|
// src/coder-workspace-provider.ts
|
|
500
2586
|
import { createHash } from "crypto";
|
|
501
|
-
import { setTimeout as
|
|
2587
|
+
import { setTimeout as delay2 } from "timers/promises";
|
|
502
2588
|
|
|
503
2589
|
// src/file-io.ts
|
|
504
2590
|
import path from "path";
|
|
@@ -522,7 +2608,7 @@ async function readBinaryFile(ctx, options) {
|
|
|
522
2608
|
const base64 = result.stdout.replace(/\s+/g, "");
|
|
523
2609
|
return new Uint8Array(Buffer.from(base64, "base64"));
|
|
524
2610
|
}
|
|
525
|
-
async function
|
|
2611
|
+
async function readFile2(ctx, options) {
|
|
526
2612
|
const bytes = await readBinaryFile(ctx, options);
|
|
527
2613
|
if (bytes === null) return null;
|
|
528
2614
|
return new ReadableStream({
|
|
@@ -648,7 +2734,7 @@ var CoderWorkspaceSession = class {
|
|
|
648
2734
|
this.#ports = [...config.ports];
|
|
649
2735
|
this.id = config.id;
|
|
650
2736
|
this.defaultWorkingDirectory = config.defaultWorkingDirectory;
|
|
651
|
-
this.description = `Coder workspace "${config.workspace}". Default working directory: ${config.defaultWorkingDirectory}. Exposed ports: ${this.#ports.length > 0 ? this.#ports.join(", ") : "none"}
|
|
2737
|
+
this.description = `Coder workspace "${config.workspace}". Default working directory: ${config.defaultWorkingDirectory}. Exposed ports: ${this.#ports.length > 0 ? this.#ports.join(", ") : "none"}.`;
|
|
652
2738
|
}
|
|
653
2739
|
get ports() {
|
|
654
2740
|
return this.#ports;
|
|
@@ -673,14 +2759,14 @@ var CoderWorkspaceSession = class {
|
|
|
673
2759
|
run = (options) => this.#transport.exec(this.#execOptions(options));
|
|
674
2760
|
spawn = async (options) => this.#transport.spawn(this.#execOptions(options));
|
|
675
2761
|
// --- file I/O surface -----------------------------------------------------
|
|
676
|
-
readFile = (options) =>
|
|
2762
|
+
readFile = (options) => readFile2(this.#fileIoContext(), options);
|
|
677
2763
|
readBinaryFile = (options) => readBinaryFile(this.#fileIoContext(), options);
|
|
678
2764
|
readTextFile = (options) => readTextFile(this.#fileIoContext(), options);
|
|
679
2765
|
writeFile = (options) => writeFile(this.#fileIoContext(), options);
|
|
680
2766
|
writeBinaryFile = (options) => writeBinaryFile(this.#fileIoContext(), options);
|
|
681
2767
|
writeTextFile = (options) => writeTextFile(this.#fileIoContext(), options);
|
|
682
2768
|
// --- network surface ------------------------------------------------------
|
|
683
|
-
|
|
2769
|
+
getPortEndpoint = async (options) => {
|
|
684
2770
|
if (this.#stopped) {
|
|
685
2771
|
throw new Error("cannot resolve a port URL: the sandbox session is stopped");
|
|
686
2772
|
}
|
|
@@ -704,8 +2790,10 @@ var CoderWorkspaceSession = class {
|
|
|
704
2790
|
}
|
|
705
2791
|
const resolved = await forward;
|
|
706
2792
|
const scheme = localScheme(options.protocol ?? "ws");
|
|
707
|
-
return `${scheme}://${resolved.localHost}:${resolved.localPort}
|
|
2793
|
+
return { url: `${scheme}://${resolved.localHost}:${resolved.localPort}` };
|
|
708
2794
|
};
|
|
2795
|
+
/** @deprecated Kept for the `HarnessV1NetworkSandboxSession` contract; use `getPortEndpoint`. */
|
|
2796
|
+
getPortUrl = async (options) => (await this.getPortEndpoint(options)).url;
|
|
709
2797
|
setPorts = async (ports, _options) => {
|
|
710
2798
|
const next = [...ports];
|
|
711
2799
|
for (const [port, forward] of this.#forwards) {
|
|
@@ -970,7 +3058,7 @@ async function waitForReady(transport, workspace, timeoutMs, caller, abortSignal
|
|
|
970
3058
|
`${caller}: timed out after ${timeoutMs}ms waiting for workspace "${workspace}" to become ready (last status: ${last}).`
|
|
971
3059
|
);
|
|
972
3060
|
}
|
|
973
|
-
await
|
|
3061
|
+
await delay2(READY_POLL_INTERVAL_MS, void 0, { signal: abortSignal });
|
|
974
3062
|
}
|
|
975
3063
|
}
|
|
976
3064
|
function deriveWorkspaceName(prefix, sessionId) {
|
|
@@ -1004,6 +3092,8 @@ async function resolveHomeDirectory(transport, workspace, abortSignal) {
|
|
|
1004
3092
|
export {
|
|
1005
3093
|
CODER_WORKSPACE_PROVIDER_ID,
|
|
1006
3094
|
CoderCliTransport,
|
|
3095
|
+
CoderNativeApiError,
|
|
3096
|
+
CoderNativeTransport,
|
|
1007
3097
|
CoderWorkspaceSession,
|
|
1008
3098
|
createCoderWorkspace,
|
|
1009
3099
|
ensureCoderWorkspace
|