@stim-cli/server 1.21.0 → 1.22.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 +153 -11
- package/dist/{protocol-D21Ab4EI.mjs → protocol-ZY9j2h32.mjs} +632 -8
- package/dist/protocol.mjs +1 -1
- package/dist/protocol.schema.json +1667 -9
- package/dist/stim-frames/SimulatorHID.swift +33 -7
- package/dist/{stim-server-BQKNg9Jp.mjs → stim-server-BKcEacbU.mjs} +1426 -227
- package/dist/stim-server.mjs +1 -1
- package/package.json +8 -8
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { n as protocol_exports } from "./protocol-
|
|
1
|
+
import { n as protocol_exports } from "./protocol-ZY9j2h32.mjs";
|
|
2
2
|
import { createRequire } from "node:module";
|
|
3
3
|
import { execFile, execFileSync, spawn } from "node:child_process";
|
|
4
4
|
import { appendFileSync, chmodSync, closeSync, constants, copyFileSync, cpSync, createReadStream, existsSync, futimesSync, lstatSync, mkdirSync, mkdtempSync, openSync, readFileSync, readSync, readdirSync, readlinkSync, realpathSync, renameSync, rmSync, rmdirSync, statSync, statfsSync, watch, writeFileSync, writeSync } from "node:fs";
|
|
@@ -6,15 +6,15 @@ import { basename, delimiter, dirname, isAbsolute, join, posix, relative, resolv
|
|
|
6
6
|
import { setTimeout as setTimeout$1 } from "node:timers/promises";
|
|
7
7
|
import { DEVICE_HOST_LIMITS, HOSTED_APP_CHUNK_BYTES, HOSTED_MACOS_APP_SLOTS, HOSTED_PARKED_CANDIDATES, KeyedEntities, RECORDING_CAP_BYTES, RECORDING_FOOTAGE_MS, RECORDING_PLATFORMS, SETTING_SCOPES, archiveDir, archivedUsage, assertHostedDeviceLedger, buildWorkerRoot, capDrops, clientIdentity, closedSegmentName, compileGrep, createDebugLog, deviceHostArea, deviceHostRoot, deviceViewersDir, footageDrops, getConcurrencyLimits, getParkedMax, hostedAndroidPlacements, hostedAppArea, hostedAppAttempt, hostedAppBlobs, hostedDeviceId, hostedIosPlacements, hostedMacosBundleId, hostedMacosLogsDir, hostedNativeLogsDir, hostedWorkerSettleMs, isJournalExpired, isJsonObject, isSetupJournalPrunable, listRecordedDevices, listSegments, loadConfig, machineCapacity, machineEndpoint, macosAppState, openSegmentName, parseHostedAppOffer, parseHostedLocalDeviceCount, parseHostedLogsCursor, parseHostedNativeOffer, parseHostedOfferRequest, parseHostedPlatformDevice, parseHostedRequest, queryJsonLogs, readArchive, readArchiveState, readBuildHistory, readBuildMachines, readDeviceHostMachines, readHostedApp, readHostedAppMetadata, readHostedDevice, readHostedDeviceLedger, readHostedMacosApp, readHostedNativeLogsCheckpoint, readHostedSessions, readJsonObject, readLogTimeline, readLogsSince, readMacosRecord, readSetupJournal, readWorkspaceState, recordingDeviceName, rememberHostedAppBlobRename, saturation, serverLocalSocket, serverSetupDir, settingDefinition, settingValueError, setupJournalFile, sortByTs, statusViews, stimBuildDigest, tryAcquireBuildSlotClaim, validHostedAppBlob, validRunId, windowsShortDir, windowsShortDirKey, workspaceRecordingsDir } from "@stim-cli/core/state";
|
|
8
8
|
import { availableParallelism, cpus, freemem, homedir, hostname, loadavg, tmpdir, totalmem, userInfo } from "node:os";
|
|
9
|
-
import { parseArgs } from "node:util";
|
|
9
|
+
import { isDeepStrictEqual, parseArgs } from "node:util";
|
|
10
10
|
import { discoverToolchain } from "@stim-cli/core/toolchain";
|
|
11
11
|
import { copyFile, lstat, mkdir, mkdtemp, open, readFile, readdir, realpath, rename, rm, writeFile } from "node:fs/promises";
|
|
12
12
|
import { createHash, createHmac, randomBytes, randomUUID, timingSafeEqual } from "node:crypto";
|
|
13
|
-
import { compiledHelper, configDir, createMetroBridge, quoteUnitWord, unitLines, unitWords, validMacosResourceDestination, withDirLock, workspaceStateDir } from "@stim-cli/core";
|
|
13
|
+
import { compiledHelper, configDir, createMetroBridge, quoteUnitWord, reactNativeArtifactIdentity, reactNativeArtifactQuery, reusableDependencies, swiftpmArtifactQuery, unitLines, unitWords, validMacosResourceDestination, withDirLock, workspaceStateDir } from "@stim-cli/core";
|
|
14
14
|
import { CLOSE_REFUSED, HandshakeRefusal, MAX_CLIENT_MESSAGE_BYTES, MAX_FIRST_MESSAGE_BYTES, MAX_SERVER_MESSAGE_BYTES, MODE_FIRST_CONTACT, TRANSPORT_OVERHEAD_BYTES, acceptNoise, decodeSecret, initiateNoise, keyFingerprint, newSecret, readOrCreateKeyFile, secretValues } from "@stim-cli/core/noise-session";
|
|
15
15
|
import { DEFAULT_STUCK_MINUTES, OVERSIGHT_CATEGORIES, inQuietHours, oversee, oversightTitle } from "@stim-cli/core/oversight";
|
|
16
16
|
import { connect, createServer, isIP } from "node:net";
|
|
17
|
-
import { ClaimRefusedError, claimRemoveCommand, clearClaimChild, clearFreeClaimSet, exclusiveClaimDir, markClaimChildPending, processGroupAlive, releaseClaim, setClaimChild, sharedClaimDir, tryAcquireClaim } from "@stim-cli/core/ownership-claim";
|
|
17
|
+
import { ClaimRefusedError, claimRemoveCommand, clearClaimChild, clearFreeClaimSet, exclusiveClaimDir, markClaimChildPending, processGroupAlive, readClaimSet, releaseClaim, setClaimChild, sharedClaimDir, tryAcquireClaim } from "@stim-cli/core/ownership-claim";
|
|
18
18
|
import { findTailscale, isTailscaleTransport, planServe, readRawTailscaleStatus, readTailscaleStatus, serveCommand, serveRoute, tailnetEndpoint, tailscaleInstall, tailscaleTransport } from "@stim-cli/transport-tailscale";
|
|
19
19
|
import { fileURLToPath } from "node:url";
|
|
20
20
|
import { createInterface } from "node:readline/promises";
|
|
@@ -127,6 +127,54 @@ function devicesFile() {
|
|
|
127
127
|
function clientsFile(capability) {
|
|
128
128
|
return join(serverDir(), `${capability}-clients.json`);
|
|
129
129
|
}
|
|
130
|
+
/** Registry events are hints; callers must reread grants and retain missed-event repair. */
|
|
131
|
+
function watchClientRegistries(changed) {
|
|
132
|
+
const files = [clientsFile("build"), clientsFile("device-host")];
|
|
133
|
+
const names = new Set(files.map((file) => basename(file)));
|
|
134
|
+
const watchers = /* @__PURE__ */ new Map();
|
|
135
|
+
let stopped = false;
|
|
136
|
+
const clearFiles = () => {
|
|
137
|
+
const previous = [...watchers.values()];
|
|
138
|
+
watchers.clear();
|
|
139
|
+
for (const observer of previous) observer.close();
|
|
140
|
+
};
|
|
141
|
+
const refresh = () => {
|
|
142
|
+
clearFiles();
|
|
143
|
+
for (const file of files) try {
|
|
144
|
+
const observer = watch(file, { persistent: false }, (event) => {
|
|
145
|
+
if (stopped || watchers.get(file) !== observer) return;
|
|
146
|
+
if (event === "rename") refresh();
|
|
147
|
+
changed();
|
|
148
|
+
});
|
|
149
|
+
watchers.set(file, observer);
|
|
150
|
+
observer.once("error", () => {
|
|
151
|
+
if (watchers.get(file) !== observer) return;
|
|
152
|
+
watchers.delete(file);
|
|
153
|
+
observer.close();
|
|
154
|
+
changed();
|
|
155
|
+
});
|
|
156
|
+
} catch {}
|
|
157
|
+
};
|
|
158
|
+
refresh();
|
|
159
|
+
let directory;
|
|
160
|
+
try {
|
|
161
|
+
directory = watch(serverDir(), { persistent: false }, (_event, filename) => {
|
|
162
|
+
if (stopped || filename !== null && !names.has(filename.toString())) return;
|
|
163
|
+
refresh();
|
|
164
|
+
changed();
|
|
165
|
+
});
|
|
166
|
+
directory.once("error", () => {
|
|
167
|
+
directory?.close();
|
|
168
|
+
directory = void 0;
|
|
169
|
+
if (!stopped) changed();
|
|
170
|
+
});
|
|
171
|
+
} catch {}
|
|
172
|
+
return () => {
|
|
173
|
+
stopped = true;
|
|
174
|
+
directory?.close();
|
|
175
|
+
clearFiles();
|
|
176
|
+
};
|
|
177
|
+
}
|
|
130
178
|
function secretsFile() {
|
|
131
179
|
return join(serverDir(), "secrets.json");
|
|
132
180
|
}
|
|
@@ -1602,15 +1650,48 @@ const taskScheduler = {
|
|
|
1602
1650
|
]);
|
|
1603
1651
|
if (!started.ok) return started;
|
|
1604
1652
|
const deadline = Date.now() + SUPERVISOR_WAIT_MS;
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1653
|
+
let observer;
|
|
1654
|
+
let changed = false;
|
|
1655
|
+
let timer;
|
|
1656
|
+
let wake;
|
|
1657
|
+
const notify = () => {
|
|
1658
|
+
changed = true;
|
|
1659
|
+
wake?.();
|
|
1660
|
+
};
|
|
1661
|
+
try {
|
|
1662
|
+
try {
|
|
1663
|
+
observer = watch(taskScheduler.root(label), { persistent: false }, (_event, filename) => {
|
|
1664
|
+
if (filename === null || filename.toString() === "supervisor.json") notify();
|
|
1665
|
+
});
|
|
1666
|
+
observer.once("error", () => {
|
|
1667
|
+
observer?.close();
|
|
1668
|
+
observer = void 0;
|
|
1669
|
+
notify();
|
|
1670
|
+
});
|
|
1671
|
+
} catch {}
|
|
1672
|
+
for (;;) {
|
|
1673
|
+
changed = false;
|
|
1674
|
+
if (await runningSupervisor(label)) return started;
|
|
1675
|
+
const remaining = deadline - Date.now();
|
|
1676
|
+
if (remaining <= 0) return {
|
|
1677
|
+
ok: false,
|
|
1678
|
+
stdout: "",
|
|
1679
|
+
stderr: `${taskName(label)} did not start its supervisor within ${SUPERVISOR_WAIT_MS / 1e3} s; see ${join(taskScheduler.root(label), "supervisor.log")} and \`schtasks /Query /TN ${taskName(label)} /V /FO LIST\`.`
|
|
1680
|
+
};
|
|
1681
|
+
if (changed) continue;
|
|
1682
|
+
await new Promise((resolve) => {
|
|
1683
|
+
wake = () => {
|
|
1684
|
+
clearTimeout(timer);
|
|
1685
|
+
wake = void 0;
|
|
1686
|
+
resolve();
|
|
1687
|
+
};
|
|
1688
|
+
timer = setTimeout(wake, Math.min(remaining, observer ? 1e3 : 250));
|
|
1689
|
+
});
|
|
1690
|
+
}
|
|
1691
|
+
} finally {
|
|
1692
|
+
clearTimeout(timer);
|
|
1693
|
+
observer?.close();
|
|
1612
1694
|
}
|
|
1613
|
-
return started;
|
|
1614
1695
|
},
|
|
1615
1696
|
async stop(label) {
|
|
1616
1697
|
const state = await runningSupervisor(label);
|
|
@@ -2466,16 +2547,17 @@ function withUpdateClaim(label, work) {
|
|
|
2466
2547
|
/**
|
|
2467
2548
|
* Installs `source` beside the server the job runs, waits for offloaded builds and hosted sessions to finish,
|
|
2468
2549
|
* switches the job to it and restarts it, and switches back when it does not answer as the installed build. Never
|
|
2469
|
-
* touches pairings, approvals, the host app, `$STIM_HOME` or the serve route.
|
|
2550
|
+
* touches pairings, approvals, the host app, `$STIM_HOME` or the serve route. With `waitForWork` false,
|
|
2551
|
+
* restart interrupts active work instead of waiting for idle; shutdown and health checks still apply.
|
|
2470
2552
|
*/
|
|
2471
|
-
async function updateService(label, source, log) {
|
|
2553
|
+
async function updateService(label, source, log, waitForWork = true) {
|
|
2472
2554
|
if (!existsSync(plistPath(label))) throw new ServiceError(`${label} is not installed.`);
|
|
2473
|
-
return withUpdateClaim(label, (installed) => updateInstalledService(label, installed, source, log));
|
|
2555
|
+
return withUpdateClaim(label, (installed) => updateInstalledService(label, installed, source, log, waitForWork));
|
|
2474
2556
|
}
|
|
2475
|
-
async function updateInstalledService(label, installed, source, log) {
|
|
2557
|
+
async function updateInstalledService(label, installed, source, log, waitForWork = true) {
|
|
2476
2558
|
const at = (/* @__PURE__ */ new Date()).toISOString();
|
|
2477
2559
|
try {
|
|
2478
|
-
const notes = await switchToSource(label, installed, source, log);
|
|
2560
|
+
const notes = await switchToSource(label, installed, source, log, waitForWork);
|
|
2479
2561
|
recordOutcome(label, {
|
|
2480
2562
|
at,
|
|
2481
2563
|
target: describeSource(source),
|
|
@@ -2493,11 +2575,12 @@ async function updateInstalledService(label, installed, source, log) {
|
|
|
2493
2575
|
throw error;
|
|
2494
2576
|
}
|
|
2495
2577
|
}
|
|
2496
|
-
async function switchToSource(label, installed, source, log) {
|
|
2578
|
+
async function switchToSource(label, installed, source, log, waitForWork) {
|
|
2497
2579
|
const versions = join(serviceRoot(label), "versions");
|
|
2498
2580
|
const target = await installServer(versions, source, installed.node, log);
|
|
2499
2581
|
if (sameFile(installed.script, target.script)) return [`${label} already runs ${describeBuild(target.build)}.`];
|
|
2500
|
-
await waitForIdle(installed.port, log);
|
|
2582
|
+
if (waitForWork) await waitForIdle(installed.port, log);
|
|
2583
|
+
else log("Restarting without waiting for active builds or hosted sessions; they will be interrupted.");
|
|
2501
2584
|
log(`Switching ${label} to ${describeBuild(target.build)}.`);
|
|
2502
2585
|
const script = realpathSync(target.script);
|
|
2503
2586
|
await switchTo(installed, script, target.build);
|
|
@@ -2769,6 +2852,25 @@ function listCommandLines() {
|
|
|
2769
2852
|
}
|
|
2770
2853
|
//#endregion
|
|
2771
2854
|
//#region src/build-artifact-cache.ts
|
|
2855
|
+
const androidPackage = (value) => typeof value === "string" && /^[A-Za-z_][\w]*(?:\.[A-Za-z_][\w]*)+$/.test(value);
|
|
2856
|
+
function gradleQueryKey(query) {
|
|
2857
|
+
const { android, inputs } = query;
|
|
2858
|
+
return createHash("sha256").update(JSON.stringify([
|
|
2859
|
+
query.project,
|
|
2860
|
+
query.sourceDigest,
|
|
2861
|
+
inputs.ignored,
|
|
2862
|
+
inputs.outputs,
|
|
2863
|
+
android.variant,
|
|
2864
|
+
android.abi === "all" ? null : android.abi,
|
|
2865
|
+
android.compilerCache,
|
|
2866
|
+
android.gradleBuildCache,
|
|
2867
|
+
android.pch,
|
|
2868
|
+
android.gradleRoot ?? null,
|
|
2869
|
+
android.module ?? null
|
|
2870
|
+
])).digest("hex");
|
|
2871
|
+
}
|
|
2872
|
+
const completedKind = (query) => "build" in query ? "swiftpm" : "sourceDigest" in query ? "gradle" : "react-native";
|
|
2873
|
+
const completedKey = (query) => "sourceDigest" in query && !("build" in query) ? gradleQueryKey(query) : createHash("sha256").update(JSON.stringify(query)).digest("hex");
|
|
2772
2874
|
function validArtifactIdentity(value) {
|
|
2773
2875
|
return isJsonObject(value) && typeof value.fingerprint === "string" && value.fingerprint.length > 0 && value.fingerprint.length <= 256 && typeof value.cacheKey === "string" && value.cacheKey.length > 0 && value.cacheKey.length <= 1024;
|
|
2774
2876
|
}
|
|
@@ -2800,7 +2902,7 @@ var BuildArtifactCache = class {
|
|
|
2800
2902
|
encoding: "utf8",
|
|
2801
2903
|
signal
|
|
2802
2904
|
}));
|
|
2803
|
-
if (!isJsonObject(record) || record.fingerprint !== identity.fingerprint || record.cacheKey !== identity.cacheKey || typeof record.name !== "string" || basename(record.name) !== record.name || !record.name.endsWith(".app") || typeof record.size !== "number" || !Number.isSafeInteger(record.size) || record.size <= 0 || typeof record.sha256 !== "string" || !/^[a-f0-9]{64}$/.test(record.sha256)) return null;
|
|
2905
|
+
if (!isJsonObject(record) || record.fingerprint !== identity.fingerprint || record.cacheKey !== identity.cacheKey || typeof record.name !== "string" || basename(record.name) !== record.name || !(record.name.endsWith(".app") || record.name.endsWith(".apk") && androidPackage(record.androidPackage)) || typeof record.size !== "number" || !Number.isSafeInteger(record.size) || record.size <= 0 || typeof record.sha256 !== "string" || !/^[a-f0-9]{64}$/.test(record.sha256)) return null;
|
|
2804
2906
|
if ((await this.files.stat({
|
|
2805
2907
|
key: record.sha256,
|
|
2806
2908
|
signal
|
|
@@ -2809,7 +2911,8 @@ var BuildArtifactCache = class {
|
|
|
2809
2911
|
...identity,
|
|
2810
2912
|
name: record.name,
|
|
2811
2913
|
size: record.size,
|
|
2812
|
-
sha256: record.sha256
|
|
2914
|
+
sha256: record.sha256,
|
|
2915
|
+
...record.name.endsWith(".apk") && androidPackage(record.androidPackage) ? { androidPackage: record.androidPackage } : {}
|
|
2813
2916
|
};
|
|
2814
2917
|
} catch {
|
|
2815
2918
|
signal.throwIfAborted();
|
|
@@ -2817,7 +2920,7 @@ var BuildArtifactCache = class {
|
|
|
2817
2920
|
}
|
|
2818
2921
|
}
|
|
2819
2922
|
async store(identity, parameters, archive, artifact, signal) {
|
|
2820
|
-
if (!validArtifactIdentity(identity) || !await verified(archive, artifact, signal)) return;
|
|
2923
|
+
if (!validArtifactIdentity(identity) || !await verified(archive, artifact, signal)) return false;
|
|
2821
2924
|
await this.files.store({
|
|
2822
2925
|
key: artifact.sha256,
|
|
2823
2926
|
sourcePath: archive,
|
|
@@ -2840,10 +2943,57 @@ var BuildArtifactCache = class {
|
|
|
2840
2943
|
});
|
|
2841
2944
|
signal.throwIfAborted();
|
|
2842
2945
|
await rename(temporary, this.record(identity));
|
|
2946
|
+
return true;
|
|
2947
|
+
} finally {
|
|
2948
|
+
await rm(temporary, { force: true });
|
|
2949
|
+
}
|
|
2950
|
+
}
|
|
2951
|
+
async storeCompleted(query, identity, receipt, archive, artifact, signal) {
|
|
2952
|
+
if (!("build" in query) && ("sourceDigest" in query || query.platform === "android")) {
|
|
2953
|
+
if (!artifact.name.endsWith(".apk") || !androidPackage(artifact.androidPackage)) return;
|
|
2954
|
+
} else if (!artifact.name.endsWith(".app")) return;
|
|
2955
|
+
if (!await this.store(identity, { ...query }, archive, artifact, signal)) return;
|
|
2956
|
+
const key = completedKey(query);
|
|
2957
|
+
const temporary = join(this.directory, `${randomUUID()}.partial`);
|
|
2958
|
+
try {
|
|
2959
|
+
await writeFile(temporary, JSON.stringify({
|
|
2960
|
+
query: key,
|
|
2961
|
+
receipt
|
|
2962
|
+
}), {
|
|
2963
|
+
mode: 384,
|
|
2964
|
+
signal
|
|
2965
|
+
});
|
|
2966
|
+
signal.throwIfAborted();
|
|
2967
|
+
await rename(temporary, join(this.directory, `${key}.${completedKind(query)}.json`));
|
|
2843
2968
|
} finally {
|
|
2844
2969
|
await rm(temporary, { force: true });
|
|
2845
2970
|
}
|
|
2846
2971
|
}
|
|
2972
|
+
async completedRecord(query, signal) {
|
|
2973
|
+
signal.throwIfAborted();
|
|
2974
|
+
try {
|
|
2975
|
+
const key = completedKey(query);
|
|
2976
|
+
const record = JSON.parse(await readFile(join(this.directory, `${key}.${completedKind(query)}.json`), {
|
|
2977
|
+
encoding: "utf8",
|
|
2978
|
+
signal
|
|
2979
|
+
}));
|
|
2980
|
+
return isJsonObject(record) && record.query === key && isJsonObject(record.receipt) ? record : null;
|
|
2981
|
+
} catch {
|
|
2982
|
+
signal.throwIfAborted();
|
|
2983
|
+
return null;
|
|
2984
|
+
}
|
|
2985
|
+
}
|
|
2986
|
+
async receipt(query, signal) {
|
|
2987
|
+
return (await this.completedRecord(query, signal))?.receipt ?? null;
|
|
2988
|
+
}
|
|
2989
|
+
async lookupReactNative(query, signal) {
|
|
2990
|
+
const record = await this.completedRecord(query, signal);
|
|
2991
|
+
if (!record || !isJsonObject(record.receipt) || !isJsonObject(record.receipt.toolchain) || !isDeepStrictEqual(record.receipt.query, query)) return null;
|
|
2992
|
+
return this.lookup(reactNativeArtifactIdentity({
|
|
2993
|
+
query,
|
|
2994
|
+
toolchain: record.receipt.toolchain
|
|
2995
|
+
}), signal);
|
|
2996
|
+
}
|
|
2847
2997
|
async fetch(identity, destinationDir, signal) {
|
|
2848
2998
|
const artifact = await this.lookup(identity, signal);
|
|
2849
2999
|
if (!artifact) return null;
|
|
@@ -2866,6 +3016,175 @@ var BuildArtifactCache = class {
|
|
|
2866
3016
|
}
|
|
2867
3017
|
};
|
|
2868
3018
|
//#endregion
|
|
3019
|
+
//#region src/artifact-query.ts
|
|
3020
|
+
/** Revalidates only a completed build's private inputs, holding its area until the owned query has stopped. */
|
|
3021
|
+
function runArtifactQuery({ worker, area, env, query, receipt, signal, timeoutMs = 1e4, killGraceMs = 1e3 }) {
|
|
3022
|
+
signal.throwIfAborted();
|
|
3023
|
+
const attempt = tryAcquireClaim({
|
|
3024
|
+
root: `${area}.claims`,
|
|
3025
|
+
mode: "shared",
|
|
3026
|
+
label: "Completed artifact query"
|
|
3027
|
+
});
|
|
3028
|
+
if (!attempt.acquired) {
|
|
3029
|
+
if (attempt.pending) releaseClaim(attempt.pending);
|
|
3030
|
+
return {
|
|
3031
|
+
done: Promise.resolve({
|
|
3032
|
+
identity: null,
|
|
3033
|
+
settled: true
|
|
3034
|
+
}),
|
|
3035
|
+
cancel: () => {}
|
|
3036
|
+
};
|
|
3037
|
+
}
|
|
3038
|
+
const claim = attempt.acquired;
|
|
3039
|
+
markClaimChildPending(claim);
|
|
3040
|
+
let child;
|
|
3041
|
+
try {
|
|
3042
|
+
child = spawn(process.execPath, [worker, "build" in query ? "swiftpm-artifact" : "native-gradle-artifact"], {
|
|
3043
|
+
cwd: area,
|
|
3044
|
+
env,
|
|
3045
|
+
detached: true,
|
|
3046
|
+
stdio: [
|
|
3047
|
+
"pipe",
|
|
3048
|
+
"pipe",
|
|
3049
|
+
"pipe"
|
|
3050
|
+
]
|
|
3051
|
+
});
|
|
3052
|
+
} catch (error) {
|
|
3053
|
+
clearClaimChild(claim);
|
|
3054
|
+
releaseClaim(claim);
|
|
3055
|
+
throw error;
|
|
3056
|
+
}
|
|
3057
|
+
let identity = null;
|
|
3058
|
+
let closed = false;
|
|
3059
|
+
let ended = false;
|
|
3060
|
+
let stopped = false;
|
|
3061
|
+
let output = "";
|
|
3062
|
+
let treeStopped = null;
|
|
3063
|
+
let killTimer;
|
|
3064
|
+
let settleTimer;
|
|
3065
|
+
let groupTimer;
|
|
3066
|
+
let finish;
|
|
3067
|
+
const done = new Promise((resolve) => {
|
|
3068
|
+
finish = resolve;
|
|
3069
|
+
});
|
|
3070
|
+
const end = (settled) => {
|
|
3071
|
+
if (ended) return;
|
|
3072
|
+
ended = true;
|
|
3073
|
+
clearTimeout(deadline);
|
|
3074
|
+
clearTimeout(killTimer);
|
|
3075
|
+
clearTimeout(settleTimer);
|
|
3076
|
+
clearTimeout(groupTimer);
|
|
3077
|
+
signal.removeEventListener("abort", cancel);
|
|
3078
|
+
let result = null;
|
|
3079
|
+
if (settled) {
|
|
3080
|
+
try {
|
|
3081
|
+
clearClaimChild(claim);
|
|
3082
|
+
releaseClaim(claim);
|
|
3083
|
+
} catch (error) {
|
|
3084
|
+
settled = false;
|
|
3085
|
+
console.error(`stim-server: Completed artifact query could not release ${claim.path}: ${String(error)}`);
|
|
3086
|
+
}
|
|
3087
|
+
if (settled && !stopped) try {
|
|
3088
|
+
const value = JSON.parse(output);
|
|
3089
|
+
if (isJsonObject(value) && isJsonObject(value.identity) && typeof value.identity.hash === "string" && /^[a-f0-9]{64}$/.test(value.identity.hash) && typeof value.identity.key === "string" && value.identity.key.length > 0 && value.identity.key.length <= 1024) result = {
|
|
3090
|
+
fingerprint: value.identity.hash,
|
|
3091
|
+
cacheKey: value.identity.key
|
|
3092
|
+
};
|
|
3093
|
+
} catch {}
|
|
3094
|
+
}
|
|
3095
|
+
if (!settled) {
|
|
3096
|
+
child.stdin?.destroy();
|
|
3097
|
+
child.stdout?.destroy();
|
|
3098
|
+
child.stderr?.destroy();
|
|
3099
|
+
child.unref();
|
|
3100
|
+
console.error(`stim-server: Completed artifact query cleanup is unresolved at ${claim.path}; inspect before ${claimRemoveCommand(claim.root)}`);
|
|
3101
|
+
}
|
|
3102
|
+
finish({
|
|
3103
|
+
identity: result,
|
|
3104
|
+
settled
|
|
3105
|
+
});
|
|
3106
|
+
};
|
|
3107
|
+
const signalGroup = (name) => {
|
|
3108
|
+
if (!identity || !child.pid) return;
|
|
3109
|
+
const status = inspectProcessIdentity(identity);
|
|
3110
|
+
if (status !== "same" && status !== "gone") return;
|
|
3111
|
+
if (process.platform === "win32") treeStopped ??= new Promise((resolve) => {
|
|
3112
|
+
execFile(join(process.env.SystemRoot || "C:\\Windows", "System32", "taskkill.exe"), [
|
|
3113
|
+
"/PID",
|
|
3114
|
+
String(child.pid),
|
|
3115
|
+
"/T",
|
|
3116
|
+
"/F"
|
|
3117
|
+
], { timeout: 4e3 }, (error) => resolve(!error));
|
|
3118
|
+
});
|
|
3119
|
+
else try {
|
|
3120
|
+
process.kill(-child.pid, name);
|
|
3121
|
+
} catch {}
|
|
3122
|
+
};
|
|
3123
|
+
const cancel = () => {
|
|
3124
|
+
if (ended || stopped) return;
|
|
3125
|
+
stopped = true;
|
|
3126
|
+
signalGroup("SIGTERM");
|
|
3127
|
+
killTimer = setTimeout(() => signalGroup("SIGKILL"), killGraceMs);
|
|
3128
|
+
settleTimer = setTimeout(() => end(false), killGraceMs + 5e3);
|
|
3129
|
+
};
|
|
3130
|
+
const drained = async () => {
|
|
3131
|
+
if (ended || !closed) return;
|
|
3132
|
+
if (treeStopped && !await treeStopped) return end(false);
|
|
3133
|
+
if (!child.pid || !processGroupAlive(child.pid)) return end(true);
|
|
3134
|
+
cancel();
|
|
3135
|
+
groupTimer = setTimeout(() => void drained(), 25);
|
|
3136
|
+
};
|
|
3137
|
+
const deadline = setTimeout(cancel, timeoutMs);
|
|
3138
|
+
child.stdout?.setEncoding("utf8");
|
|
3139
|
+
child.stdout?.on("data", (chunk) => {
|
|
3140
|
+
if (Buffer.byteLength(output) + Buffer.byteLength(chunk) > 16384) cancel();
|
|
3141
|
+
else output += chunk;
|
|
3142
|
+
});
|
|
3143
|
+
child.stderr?.resume();
|
|
3144
|
+
child.stdin?.on("error", cancel);
|
|
3145
|
+
child.once("error", () => {
|
|
3146
|
+
cancel();
|
|
3147
|
+
closed = true;
|
|
3148
|
+
drained();
|
|
3149
|
+
});
|
|
3150
|
+
child.once("close", (code) => {
|
|
3151
|
+
if (code !== 0) cancel();
|
|
3152
|
+
closed = true;
|
|
3153
|
+
drained();
|
|
3154
|
+
});
|
|
3155
|
+
signal.addEventListener("abort", cancel, { once: true });
|
|
3156
|
+
const captured = child.pid === void 0 ? null : captureProcessIdentity(child.pid);
|
|
3157
|
+
if (!captured?.ok || child.pid === void 0) {
|
|
3158
|
+
child.kill("SIGKILL");
|
|
3159
|
+
cancel();
|
|
3160
|
+
} else {
|
|
3161
|
+
identity = {
|
|
3162
|
+
pid: child.pid,
|
|
3163
|
+
processToken: captured.token
|
|
3164
|
+
};
|
|
3165
|
+
try {
|
|
3166
|
+
setClaimChild(claim, identity);
|
|
3167
|
+
if (signal.aborted) cancel();
|
|
3168
|
+
else child.stdin?.end(JSON.stringify({
|
|
3169
|
+
root: join(area, "src", query.project),
|
|
3170
|
+
receipt,
|
|
3171
|
+
..."build" in query ? { query } : {
|
|
3172
|
+
sourceDigest: query.sourceDigest,
|
|
3173
|
+
inputs: query.inputs,
|
|
3174
|
+
android: query.android
|
|
3175
|
+
}
|
|
3176
|
+
}));
|
|
3177
|
+
} catch (error) {
|
|
3178
|
+
console.error(`stim-server: Completed artifact query could not record its child: ${String(error)}`);
|
|
3179
|
+
cancel();
|
|
3180
|
+
}
|
|
3181
|
+
}
|
|
3182
|
+
return {
|
|
3183
|
+
done,
|
|
3184
|
+
cancel
|
|
3185
|
+
};
|
|
3186
|
+
}
|
|
3187
|
+
//#endregion
|
|
2869
3188
|
//#region src/build.ts
|
|
2870
3189
|
const DEFAULT_BUILD_LIMITS = {
|
|
2871
3190
|
maxJobs: 1,
|
|
@@ -2931,6 +3250,61 @@ function androidOptions(value) {
|
|
|
2931
3250
|
module
|
|
2932
3251
|
};
|
|
2933
3252
|
}
|
|
3253
|
+
function macosBuildOptions(params) {
|
|
3254
|
+
const options = params.macos;
|
|
3255
|
+
if (!isJsonObject(options) || typeof options.product !== "string" || !/^[A-Za-z0-9_.-]{1,100}$/.test(options.product) || !validBuildPath(options.infoPlist) || typeof options.bundleId !== "string" || !/^[A-Za-z0-9][A-Za-z0-9.-]{0,199}$/.test(options.bundleId)) return refusal("bad-request", "A macos build needs a valid product, relative infoPlist path and bundleId in params.macos.");
|
|
3256
|
+
if (options.resources !== void 0 && (!isJsonObject(options.resources) || Object.keys(options.resources).length > 256 || Object.entries(options.resources).some(([destination, source]) => !validMacosResourceDestination(destination) || !validBuildPath(source))) || options.assetCatalog !== void 0 && options.assetCatalog !== null && !validBuildPath(options.assetCatalog)) return refusal("bad-request", "params.macos needs contained resource destinations and repository-relative sources.");
|
|
3257
|
+
const artifactQuery = options.artifactInputs === void 0 ? null : swiftpmArtifactQuery({
|
|
3258
|
+
project: params.project,
|
|
3259
|
+
sourceDigest: params.fingerprint,
|
|
3260
|
+
stimBuild: params.stimBuild,
|
|
3261
|
+
inputs: options.artifactInputs,
|
|
3262
|
+
build: {
|
|
3263
|
+
product: options.product,
|
|
3264
|
+
infoPlist: options.infoPlist,
|
|
3265
|
+
resources: options.resources ?? {},
|
|
3266
|
+
assetCatalog: options.assetCatalog ?? null
|
|
3267
|
+
}
|
|
3268
|
+
});
|
|
3269
|
+
if (options.artifactInputs !== void 0 && !artifactQuery) return refusal("bad-request", "SwiftPM artifact retention needs a complete repeatable input declaration.");
|
|
3270
|
+
return { options: {
|
|
3271
|
+
product: options.product,
|
|
3272
|
+
infoPlist: options.infoPlist,
|
|
3273
|
+
bundleId: options.bundleId,
|
|
3274
|
+
...artifactQuery ? { artifactInputs: artifactQuery.inputs } : {},
|
|
3275
|
+
...options.resources !== void 0 ? { resources: options.resources } : {},
|
|
3276
|
+
...options.assetCatalog !== void 0 ? { assetCatalog: options.assetCatalog } : {}
|
|
3277
|
+
} };
|
|
3278
|
+
}
|
|
3279
|
+
function gradleArtifactQuery(value) {
|
|
3280
|
+
if (!isJsonObject(value) || Object.keys(value).some((key) => ![
|
|
3281
|
+
"project",
|
|
3282
|
+
"sourceDigest",
|
|
3283
|
+
"inputs",
|
|
3284
|
+
"android"
|
|
3285
|
+
].includes(key))) return null;
|
|
3286
|
+
const { project, sourceDigest, inputs } = value;
|
|
3287
|
+
const path = (candidate) => validBuildPath(candidate) && !/[\\:]/.test(candidate);
|
|
3288
|
+
if (typeof project !== "string" || project !== "" && !path(project) || typeof sourceDigest !== "string" || !/^[a-f0-9]{64}$/.test(sourceDigest) || !isJsonObject(inputs) || inputs.complete !== true || Object.keys(inputs).some((key) => ![
|
|
3289
|
+
"complete",
|
|
3290
|
+
"ignored",
|
|
3291
|
+
"outputs"
|
|
3292
|
+
].includes(key)) || !Array.isArray(inputs.ignored) || !inputs.ignored.every(path) || !Array.isArray(inputs.outputs) || !inputs.outputs.every(path)) return null;
|
|
3293
|
+
const android = androidOptions(value.android);
|
|
3294
|
+
return android && android.variant !== null ? {
|
|
3295
|
+
project,
|
|
3296
|
+
sourceDigest,
|
|
3297
|
+
inputs: {
|
|
3298
|
+
complete: true,
|
|
3299
|
+
ignored: inputs.ignored,
|
|
3300
|
+
outputs: inputs.outputs
|
|
3301
|
+
},
|
|
3302
|
+
android: {
|
|
3303
|
+
...android,
|
|
3304
|
+
variant: android.variant
|
|
3305
|
+
}
|
|
3306
|
+
} : null;
|
|
3307
|
+
}
|
|
2934
3308
|
function freeBytes(path) {
|
|
2935
3309
|
try {
|
|
2936
3310
|
const stats = statfsSync(path);
|
|
@@ -2944,13 +3318,6 @@ function workerFreeBytes(root) {
|
|
|
2944
3318
|
const free = [root, ...process.platform === "win32" ? [dirname(dirname(windowsShortDir(root)))] : []].map(freeBytes).filter((bytes) => bytes !== null);
|
|
2945
3319
|
return free.length ? Math.min(...free) : null;
|
|
2946
3320
|
}
|
|
2947
|
-
function nonEmptyDir(path) {
|
|
2948
|
-
try {
|
|
2949
|
-
return readdirSync(path).length > 0;
|
|
2950
|
-
} catch {
|
|
2951
|
-
return false;
|
|
2952
|
-
}
|
|
2953
|
-
}
|
|
2954
3321
|
function gradleDaemonIdleMs() {
|
|
2955
3322
|
const definition = settingDefinition("server.gradleDaemonIdleMinutes");
|
|
2956
3323
|
const value = loadConfig()?.server?.gradleDaemonIdleMinutes;
|
|
@@ -3007,6 +3374,8 @@ var BuildHost = class {
|
|
|
3007
3374
|
owned = /* @__PURE__ */ new Map();
|
|
3008
3375
|
sessions = /* @__PURE__ */ new Map();
|
|
3009
3376
|
served = /* @__PURE__ */ new Set();
|
|
3377
|
+
removals = /* @__PURE__ */ new Map();
|
|
3378
|
+
queries = /* @__PURE__ */ new Set();
|
|
3010
3379
|
retained = /* @__PURE__ */ new Map();
|
|
3011
3380
|
closed = false;
|
|
3012
3381
|
draining = null;
|
|
@@ -3108,13 +3477,30 @@ var BuildHost = class {
|
|
|
3108
3477
|
});
|
|
3109
3478
|
return value;
|
|
3110
3479
|
}
|
|
3111
|
-
async offer(client, params) {
|
|
3480
|
+
async offer(client, params, signal = new AbortController().signal) {
|
|
3112
3481
|
if (!isJsonObject(params) || !validRepo(params.repo)) return refusal("bad-request", "build.offer needs params.repo.");
|
|
3482
|
+
let dependencies;
|
|
3483
|
+
if (params.dependencies !== void 0) {
|
|
3484
|
+
const value = params.dependencies;
|
|
3485
|
+
if (!isJsonObject(value) || typeof value.project !== "string" || value.project !== "" && (!validBuildPath(value.project) || /[\\:]/.test(value.project)) || value.packageName !== null && typeof value.packageName !== "string") return refusal("bad-request", "Dependency warmth needs a repository project and package name or null.");
|
|
3486
|
+
dependencies = {
|
|
3487
|
+
project: value.project,
|
|
3488
|
+
packageName: value.packageName
|
|
3489
|
+
};
|
|
3490
|
+
}
|
|
3113
3491
|
if (params.rubyVersion !== void 0 && (typeof params.rubyVersion !== "string" || !new RegExp(protocol_exports.BUILD_RUBY_VERSION_PATTERN).test(params.rubyVersion))) return refusal("bad-request", "build.offer needs a Ruby installation name without path separators.");
|
|
3114
3492
|
if (params.native !== void 0 && params.native !== "xcode" && params.native !== "gradle") return refusal("bad-request", "build.offer supports native Xcode or Gradle toolchains.");
|
|
3115
3493
|
if (params.artifact !== void 0 && (params.native !== "xcode" || !validArtifactIdentity(params.artifact))) return refusal("bad-request", "An artifact lookup needs a complete native Xcode identity.");
|
|
3116
|
-
const
|
|
3494
|
+
const query = params.gradleArtifact === void 0 ? void 0 : gradleArtifactQuery(params.gradleArtifact);
|
|
3495
|
+
if (query === null || query !== void 0 && (params.native !== "gradle" || params.artifact !== void 0)) return refusal("bad-request", "A native Gradle artifact lookup needs its completed source and build choices.");
|
|
3496
|
+
const reactNative = params.reactNativeArtifact === void 0 ? void 0 : reactNativeArtifactQuery(params.reactNativeArtifact);
|
|
3497
|
+
if (reactNative === null || reactNative !== void 0 && (params.native !== void 0 || params.artifact !== void 0 || query !== void 0)) return refusal("bad-request", "A React Native artifact lookup needs its development fingerprint and build choices.");
|
|
3498
|
+
const swiftpm = params.swiftpmArtifact === void 0 ? void 0 : swiftpmArtifactQuery(params.swiftpmArtifact);
|
|
3499
|
+
if (swiftpm === null || swiftpm !== void 0 && (params.native !== void 0 || params.artifact !== void 0 || query !== void 0 || reactNative !== void 0)) return refusal("bad-request", "A SwiftPM artifact lookup needs its completed source and declared build choices.");
|
|
3500
|
+
const artifact = swiftpm !== void 0 ? await this.privateArtifact(client, params.repo, swiftpm, signal) : reactNative !== void 0 ? await this.cache(client, params.repo).lookupReactNative(reactNative, signal) : query !== void 0 ? await this.privateArtifact(client, params.repo, query, signal) : params.artifact === void 0 ? void 0 : await this.cache(client, params.repo).lookup(params.artifact, signal);
|
|
3501
|
+
if (this.closed || this.removals.has(client) || signal.aborted || !(this.options.allowed?.(client) ?? true)) return refusal("build-refused", "This build connection closed or lost access during the offer.");
|
|
3117
3502
|
const toolchain = await this.toolchain(params.rubyVersion ?? null, params.native);
|
|
3503
|
+
if (this.closed || this.removals.has(client) || signal.aborted || !(this.options.allowed?.(client) ?? true)) return refusal("build-refused", "This build connection closed or lost access during the offer.");
|
|
3118
3504
|
if (!toolchain) return refusal("build-refused", "This machine could not read its build toolchain.");
|
|
3119
3505
|
const root = this.root();
|
|
3120
3506
|
mkdirSync(root, {
|
|
@@ -3122,10 +3508,6 @@ var BuildHost = class {
|
|
|
3122
3508
|
mode: 448
|
|
3123
3509
|
});
|
|
3124
3510
|
const area = join(this.clientDir(client), "repos", params.repo);
|
|
3125
|
-
let lockfile = null;
|
|
3126
|
-
try {
|
|
3127
|
-
lockfile = readFileSync(join(area, "lockfile"), "utf8");
|
|
3128
|
-
} catch {}
|
|
3129
3511
|
const available = await readAvailableMemory();
|
|
3130
3512
|
const total = totalmem();
|
|
3131
3513
|
const memory = available === null ? {} : {
|
|
@@ -3140,8 +3522,8 @@ var BuildHost = class {
|
|
|
3140
3522
|
},
|
|
3141
3523
|
warm: {
|
|
3142
3524
|
checkout: existsSync(join(area, "src")),
|
|
3143
|
-
dependencies: typeof params.lockfile === "string" &&
|
|
3144
|
-
build:
|
|
3525
|
+
dependencies: typeof params.lockfile === "string" && dependencies !== void 0 && reusableDependencies(area, dependencies.project, dependencies.packageName, params.lockfile),
|
|
3526
|
+
build: false
|
|
3145
3527
|
},
|
|
3146
3528
|
...artifact !== void 0 ? { artifact } : {}
|
|
3147
3529
|
} };
|
|
@@ -3149,6 +3531,101 @@ var BuildHost = class {
|
|
|
3149
3531
|
cache(client, repo) {
|
|
3150
3532
|
return new BuildArtifactCache(join(this.clientDir(client), "repos", repo, "artifact-cache"));
|
|
3151
3533
|
}
|
|
3534
|
+
workerEnvironment(clientDir, area) {
|
|
3535
|
+
return {
|
|
3536
|
+
...this.options.env,
|
|
3537
|
+
STIM_HOME: join(area, "home"),
|
|
3538
|
+
CP_HOME_DIR: join(clientDir, "cache", "cocoapods-home"),
|
|
3539
|
+
CP_CACHE_DIR: join(clientDir, "cache", "cocoapods"),
|
|
3540
|
+
npm_config_store_dir: join(clientDir, "cache", "pnpm-store"),
|
|
3541
|
+
GRADLE_USER_HOME: gradleUserHome(clientDir)
|
|
3542
|
+
};
|
|
3543
|
+
}
|
|
3544
|
+
async privateArtifact(client, repo, query, signal) {
|
|
3545
|
+
try {
|
|
3546
|
+
if (this.closed || this.removals.has(client) || !(this.options.allowed?.(client) ?? true) || this.queries.size >= 4) return null;
|
|
3547
|
+
const cache = this.cache(client, repo);
|
|
3548
|
+
const receipt = await cache.receipt(query, signal);
|
|
3549
|
+
if (!receipt || this.closed || this.removals.has(client) || signal.aborted || !(this.options.allowed?.(client) ?? true) || this.queries.size >= 4) return null;
|
|
3550
|
+
const clientDir = this.clientDir(client), area = join(clientDir, "repos", repo);
|
|
3551
|
+
const run = runArtifactQuery({
|
|
3552
|
+
worker: this.options.worker,
|
|
3553
|
+
area,
|
|
3554
|
+
env: this.workerEnvironment(clientDir, area),
|
|
3555
|
+
query,
|
|
3556
|
+
receipt,
|
|
3557
|
+
signal,
|
|
3558
|
+
killGraceMs: this.limits.killGraceMs
|
|
3559
|
+
});
|
|
3560
|
+
const entry = {
|
|
3561
|
+
client,
|
|
3562
|
+
run,
|
|
3563
|
+
settled: false
|
|
3564
|
+
};
|
|
3565
|
+
this.queries.add(entry);
|
|
3566
|
+
this.served.add(client);
|
|
3567
|
+
try {
|
|
3568
|
+
const result = await run.done;
|
|
3569
|
+
entry.settled = result.settled;
|
|
3570
|
+
if (!result.settled) throw new Error("Native artifact lookup cleanup is unresolved; inspect this machine's stim-server logs before retrying.");
|
|
3571
|
+
if (!result.identity || signal.aborted || this.closed || !(this.options.allowed?.(client) ?? true)) return null;
|
|
3572
|
+
const artifact = await cache.lookup(result.identity, signal);
|
|
3573
|
+
return "build" in query ? artifact?.name.endsWith(".app") ? artifact : null : artifact?.androidPackage ? artifact : null;
|
|
3574
|
+
} finally {
|
|
3575
|
+
if (entry.settled) this.queries.delete(entry);
|
|
3576
|
+
}
|
|
3577
|
+
} catch (error) {
|
|
3578
|
+
console.error(`stim-server: Completed artifact lookup failed for ${client}/${repo}: ${String(error)}`);
|
|
3579
|
+
throw new Error("This machine could not verify the completed artifact inputs or their cleanup; inspect its stim-server logs before retrying.", { cause: error });
|
|
3580
|
+
}
|
|
3581
|
+
}
|
|
3582
|
+
async stopQueries(client) {
|
|
3583
|
+
const entries = [...this.queries].filter((entry) => client === void 0 || entry.client === client);
|
|
3584
|
+
for (const entry of entries) entry.run.cancel();
|
|
3585
|
+
return (await Promise.all(entries.map((entry) => entry.run.done))).every((result) => result.settled);
|
|
3586
|
+
}
|
|
3587
|
+
async storeCompletedGradle(client, repo, job, entry, signal) {
|
|
3588
|
+
const outcome = entry.outcome, metadata = entry.cacheArtifact, native = job.native;
|
|
3589
|
+
if (!outcome?.ok || !isJsonObject(native) || native.provider !== "gradle" || !isJsonObject(native.cache) || native.cache.write !== true || native.cache.remote !== true || outcome.fingerprint !== null || outcome.sourceDigest !== native.sourceDigest || typeof outcome.androidPackage !== "string" || !metadata || !isJsonObject(metadata.identity) || !isJsonObject(metadata.receipt)) return;
|
|
3590
|
+
const identity = {
|
|
3591
|
+
fingerprint: metadata.identity.hash,
|
|
3592
|
+
cacheKey: metadata.identity.key
|
|
3593
|
+
};
|
|
3594
|
+
const query = gradleArtifactQuery({
|
|
3595
|
+
project: job.project,
|
|
3596
|
+
sourceDigest: native.sourceDigest,
|
|
3597
|
+
inputs: native.inputs,
|
|
3598
|
+
android: job.android
|
|
3599
|
+
});
|
|
3600
|
+
if (!query || !validArtifactIdentity(identity)) return;
|
|
3601
|
+
await this.cache(client, repo).storeCompleted(query, identity, metadata.receipt, entry.archive, {
|
|
3602
|
+
...outcome.artifact,
|
|
3603
|
+
androidPackage: outcome.androidPackage
|
|
3604
|
+
}, signal);
|
|
3605
|
+
}
|
|
3606
|
+
async storeCompletedSwiftpm(client, repo, job, entry, signal) {
|
|
3607
|
+
const metadata = entry.swiftpmArtifact;
|
|
3608
|
+
if (!entry.outcome?.ok || job.platform !== "macos" || job.cacheArtifact !== true || !isJsonObject(job.macos) || !metadata || !isJsonObject(metadata.identity) || !isJsonObject(metadata.receipt)) return;
|
|
3609
|
+
const options = job.macos;
|
|
3610
|
+
const query = swiftpmArtifactQuery({
|
|
3611
|
+
project: job.project,
|
|
3612
|
+
sourceDigest: job.expectedFingerprint,
|
|
3613
|
+
stimBuild: job.stimBuild,
|
|
3614
|
+
inputs: options.artifactInputs,
|
|
3615
|
+
build: {
|
|
3616
|
+
product: options.product,
|
|
3617
|
+
infoPlist: options.infoPlist,
|
|
3618
|
+
resources: options.resources ?? {},
|
|
3619
|
+
assetCatalog: options.assetCatalog ?? null
|
|
3620
|
+
}
|
|
3621
|
+
});
|
|
3622
|
+
const identity = {
|
|
3623
|
+
fingerprint: metadata.identity.hash,
|
|
3624
|
+
cacheKey: metadata.identity.key
|
|
3625
|
+
};
|
|
3626
|
+
if (!query || !validArtifactIdentity(identity) || identity.cacheKey !== `swiftpm-peer-${identity.fingerprint}` || entry.outcome.fingerprint !== query.sourceDigest || !isDeepStrictEqual(query, metadata.receipt.query) || !isDeepStrictEqual(metadata.identity, metadata.receipt.identity)) return;
|
|
3627
|
+
await this.cache(client, repo).storeCompleted(query, identity, metadata.receipt, entry.archive, entry.outcome.artifact, signal);
|
|
3628
|
+
}
|
|
3152
3629
|
running() {
|
|
3153
3630
|
return this.jobs.size;
|
|
3154
3631
|
}
|
|
@@ -3219,12 +3696,45 @@ var BuildHost = class {
|
|
|
3219
3696
|
} catch {}
|
|
3220
3697
|
}
|
|
3221
3698
|
/** Cancels the builds of `client`, a client that lost `build`, and deletes its area once they end. */
|
|
3222
|
-
|
|
3223
|
-
if (!client || client === "." || client === ".." || basename(client) !== client) return;
|
|
3699
|
+
remove(client) {
|
|
3700
|
+
if (!client || client === "." || client === ".." || basename(client) !== client) return Promise.resolve();
|
|
3701
|
+
const pending = this.removals.get(client);
|
|
3702
|
+
if (pending) return pending;
|
|
3703
|
+
const removal = this.removeClient(client).finally(() => this.removals.delete(client));
|
|
3704
|
+
this.removals.set(client, removal);
|
|
3705
|
+
return removal;
|
|
3706
|
+
}
|
|
3707
|
+
async removeClient(client) {
|
|
3224
3708
|
for (const [token, entry] of this.retained) if (entry.client === client) this.drop(token);
|
|
3225
3709
|
const jobs = [...this.jobs].filter((job) => job.client === client);
|
|
3226
3710
|
for (const job of jobs) job.cancel();
|
|
3711
|
+
const queriesSettled = await this.stopQueries(client);
|
|
3227
3712
|
await Promise.all(jobs.map((job) => job.done));
|
|
3713
|
+
if (!queriesSettled) {
|
|
3714
|
+
console.error(`stim-server: retaining the build area of ${client} because artifact query cleanup is unresolved.`);
|
|
3715
|
+
return;
|
|
3716
|
+
}
|
|
3717
|
+
let areas;
|
|
3718
|
+
try {
|
|
3719
|
+
areas = await readdir(join(this.clientDir(client), "repos"));
|
|
3720
|
+
} catch (error) {
|
|
3721
|
+
if (!(error instanceof Error && "code" in error && error.code === "ENOENT")) {
|
|
3722
|
+
console.error(`stim-server: retaining unreadable build areas of ${client}: ${String(error)}`);
|
|
3723
|
+
return;
|
|
3724
|
+
}
|
|
3725
|
+
areas = [];
|
|
3726
|
+
}
|
|
3727
|
+
for (const area of areas.filter((name) => name.endsWith(".claims"))) {
|
|
3728
|
+
const root = join(this.clientDir(client), "repos", area);
|
|
3729
|
+
const clearance = clearFreeClaimSet({
|
|
3730
|
+
root,
|
|
3731
|
+
label: "build worker"
|
|
3732
|
+
});
|
|
3733
|
+
if (clearance.status !== "cleared") {
|
|
3734
|
+
console.error(`stim-server: retaining the build area of ${client}; ownership at ${root} is ${clearance.status}.`);
|
|
3735
|
+
return;
|
|
3736
|
+
}
|
|
3737
|
+
}
|
|
3228
3738
|
if (process.platform === "win32") await this.sweepDaemons(client);
|
|
3229
3739
|
const removeDir = async (dir) => {
|
|
3230
3740
|
try {
|
|
@@ -3263,7 +3773,7 @@ var BuildHost = class {
|
|
|
3263
3773
|
}
|
|
3264
3774
|
/** Starts the build of one synced manifest; the job belongs to the caller's connection. */
|
|
3265
3775
|
launch({ client, repo, job, session, send }) {
|
|
3266
|
-
if (this.closed) return refusal("build-refused", "This machine is shutting down
|
|
3776
|
+
if (this.closed || this.removals.has(client)) return refusal("build-refused", "This machine is shutting down or removing this build client.");
|
|
3267
3777
|
const { declined } = this.capacity();
|
|
3268
3778
|
if (declined) return refusal("build-busy", `This machine declines the build: ${declined}.`);
|
|
3269
3779
|
const clientDir = this.clientDir(client);
|
|
@@ -3335,14 +3845,7 @@ var BuildHost = class {
|
|
|
3335
3845
|
"pipe",
|
|
3336
3846
|
"ignore"
|
|
3337
3847
|
],
|
|
3338
|
-
env:
|
|
3339
|
-
...this.options.env,
|
|
3340
|
-
STIM_HOME: join(area, "home"),
|
|
3341
|
-
CP_HOME_DIR: join(clientDir, "cache", "cocoapods-home"),
|
|
3342
|
-
CP_CACHE_DIR: join(clientDir, "cache", "cocoapods"),
|
|
3343
|
-
npm_config_store_dir: join(clientDir, "cache", "pnpm-store"),
|
|
3344
|
-
GRADLE_USER_HOME: gradleUserHome(clientDir)
|
|
3345
|
-
}
|
|
3848
|
+
env: this.workerEnvironment(clientDir, area)
|
|
3346
3849
|
});
|
|
3347
3850
|
} catch (error) {
|
|
3348
3851
|
releaseClaims();
|
|
@@ -3432,7 +3935,12 @@ var BuildHost = class {
|
|
|
3432
3935
|
job: id,
|
|
3433
3936
|
record: value.record
|
|
3434
3937
|
});
|
|
3435
|
-
else if (value.type === "result")
|
|
3938
|
+
else if (value.type === "result") {
|
|
3939
|
+
if (isJsonObject(value.cacheArtifact)) entry.cacheArtifact = value.cacheArtifact;
|
|
3940
|
+
if (isJsonObject(value.reactNativeArtifact)) entry.reactNativeArtifact = value.reactNativeArtifact;
|
|
3941
|
+
if (isJsonObject(value.swiftpmArtifact)) entry.swiftpmArtifact = value.swiftpmArtifact;
|
|
3942
|
+
entry.outcome = workerOutcome(value, isJsonObject(job.native) && job.native.provider === "gradle" ? "gradle" : void 0);
|
|
3943
|
+
}
|
|
3436
3944
|
});
|
|
3437
3945
|
this.jobs.add(entry);
|
|
3438
3946
|
this.owned.set(id, entry);
|
|
@@ -3477,6 +3985,24 @@ var BuildHost = class {
|
|
|
3477
3985
|
}
|
|
3478
3986
|
});
|
|
3479
3987
|
}
|
|
3988
|
+
try {
|
|
3989
|
+
await this.storeCompletedGradle(client, repo, job, entry, caching.signal);
|
|
3990
|
+
await this.storeCompletedSwiftpm(client, repo, job, entry, caching.signal);
|
|
3991
|
+
const metadata = entry.reactNativeArtifact;
|
|
3992
|
+
const query = metadata && isJsonObject(metadata.receipt) ? reactNativeArtifactQuery(metadata.receipt.query) : null;
|
|
3993
|
+
if (entry.outcome?.ok && job.cacheArtifact === true && !job.native && query && metadata && isJsonObject(metadata.identity) && query.fingerprint === job.expectedFingerprint && entry.outcome.fingerprint === job.expectedFingerprint && query.project === job.project && query.platform === job.platform) {
|
|
3994
|
+
const identity = {
|
|
3995
|
+
fingerprint: metadata.identity.hash,
|
|
3996
|
+
cacheKey: metadata.identity.key
|
|
3997
|
+
};
|
|
3998
|
+
if (validArtifactIdentity(identity)) await this.cache(client, repo).storeCompleted(query, identity, metadata.receipt, entry.archive, {
|
|
3999
|
+
...entry.outcome.artifact,
|
|
4000
|
+
...entry.outcome.androidPackage ? { androidPackage: entry.outcome.androidPackage } : {}
|
|
4001
|
+
}, caching.signal);
|
|
4002
|
+
}
|
|
4003
|
+
} catch (error) {
|
|
4004
|
+
console.error(`stim-server: could not retain the completed artifact for ${client}/${repo}: ${String(error)}`);
|
|
4005
|
+
}
|
|
3480
4006
|
entry.settled = true;
|
|
3481
4007
|
this.jobs.delete(entry);
|
|
3482
4008
|
if (cancelled) this.sweepDaemons(client);
|
|
@@ -3600,6 +4126,7 @@ var BuildHost = class {
|
|
|
3600
4126
|
*/
|
|
3601
4127
|
abandonDetached(allowed, deleteAreas = false) {
|
|
3602
4128
|
for (const job of this.owned.values()) if (!job.session && !allowed(job.client)) this.abandon(job);
|
|
4129
|
+
for (const entry of this.queries) if (!allowed(entry.client)) entry.run.cancel();
|
|
3603
4130
|
for (const [token, entry] of this.retained) if (!allowed(entry.client)) this.drop(token);
|
|
3604
4131
|
for (const client of this.served) {
|
|
3605
4132
|
if (!deleteAreas || allowed(client)) continue;
|
|
@@ -3612,15 +4139,17 @@ var BuildHost = class {
|
|
|
3612
4139
|
clearInterval(this.sweeper);
|
|
3613
4140
|
for (const job of this.owned.values()) this.abandon(job);
|
|
3614
4141
|
for (const token of this.retained.keys()) this.drop(token);
|
|
4142
|
+
await this.stopQueries();
|
|
3615
4143
|
await Promise.all([...this.jobs].map((job) => job.done));
|
|
3616
4144
|
await this.sweeping;
|
|
3617
4145
|
}
|
|
3618
4146
|
};
|
|
3619
4147
|
function workerOutcome(value, native) {
|
|
3620
4148
|
const timings = isJsonObject(value.timings) ? value.timings : {};
|
|
4149
|
+
const androidPackage = typeof value.androidPackage === "string" && /^[A-Za-z_][\w]*(?:\.[A-Za-z_][\w]*)+$/.test(value.androidPackage) ? value.androidPackage : void 0;
|
|
3621
4150
|
if (value.ok === true && isJsonObject(value.artifact)) {
|
|
3622
4151
|
const { name, size, sha256 } = value.artifact;
|
|
3623
|
-
if (typeof name === "string" && typeof size === "number" && typeof sha256 === "string" && (native !== "gradle" || value.fingerprint === null &&
|
|
4152
|
+
if (typeof name === "string" && typeof size === "number" && typeof sha256 === "string" && (native !== "gradle" || value.fingerprint === null && androidPackage !== void 0)) return {
|
|
3624
4153
|
ok: true,
|
|
3625
4154
|
artifact: {
|
|
3626
4155
|
name,
|
|
@@ -3628,7 +4157,7 @@ function workerOutcome(value, native) {
|
|
|
3628
4157
|
sha256
|
|
3629
4158
|
},
|
|
3630
4159
|
fingerprint: native === "gradle" ? null : String(value.fingerprint),
|
|
3631
|
-
...
|
|
4160
|
+
...androidPackage ? { androidPackage } : {},
|
|
3632
4161
|
...typeof value.sourceDigest === "string" ? { sourceDigest: value.sourceDigest } : {},
|
|
3633
4162
|
compilationCache: isJsonObject(value.compilationCache) ? value.compilationCache : {},
|
|
3634
4163
|
timings
|
|
@@ -3655,6 +4184,7 @@ var BuildSession = class {
|
|
|
3655
4184
|
jobs = /* @__PURE__ */ new Map();
|
|
3656
4185
|
closed = false;
|
|
3657
4186
|
transfers = new AbortController();
|
|
4187
|
+
signal = this.transfers.signal;
|
|
3658
4188
|
host;
|
|
3659
4189
|
client;
|
|
3660
4190
|
socket;
|
|
@@ -3753,6 +4283,12 @@ var BuildSession = class {
|
|
|
3753
4283
|
if (!isJsonObject(input) || typeof input.sourceDigest !== "string" || !/^[0-9a-f]{64}$/.test(input.sourceDigest)) return refusal("bad-request", "A native build needs its source transfer digest.");
|
|
3754
4284
|
if (input.provider === "gradle") {
|
|
3755
4285
|
const inputs = input.inputs;
|
|
4286
|
+
const cache = input.cache;
|
|
4287
|
+
if (cache !== void 0 && (!isJsonObject(cache) || [
|
|
4288
|
+
"read",
|
|
4289
|
+
"write",
|
|
4290
|
+
"remote"
|
|
4291
|
+
].some((key) => typeof cache[key] !== "boolean"))) return refusal("bad-request", "A native Gradle artifact cache policy needs boolean read, write and remote values.");
|
|
3756
4292
|
if (platform !== "android" || params.fingerprint !== null || !isJsonObject(inputs) || inputs.complete !== true || Object.keys(inputs).some((key) => ![
|
|
3757
4293
|
"complete",
|
|
3758
4294
|
"ignored",
|
|
@@ -3774,16 +4310,9 @@ var BuildSession = class {
|
|
|
3774
4310
|
if (platform === "android" && !android) return refusal("bad-request", "An android build needs params.android: variant and abi (string or null), gradleBuildCache, pch and compilerCache.");
|
|
3775
4311
|
let macos = null;
|
|
3776
4312
|
if (platform === "macos") {
|
|
3777
|
-
const
|
|
3778
|
-
if (
|
|
3779
|
-
|
|
3780
|
-
macos = {
|
|
3781
|
-
product: options.product,
|
|
3782
|
-
infoPlist: options.infoPlist,
|
|
3783
|
-
bundleId: options.bundleId,
|
|
3784
|
-
...options.resources !== void 0 ? { resources: options.resources } : {},
|
|
3785
|
-
...options.assetCatalog !== void 0 ? { assetCatalog: options.assetCatalog } : {}
|
|
3786
|
-
};
|
|
4313
|
+
const parsed = macosBuildOptions(params);
|
|
4314
|
+
if ("error" in parsed) return parsed;
|
|
4315
|
+
macos = parsed.options;
|
|
3787
4316
|
}
|
|
3788
4317
|
if (params.project !== "" && !validBuildPath(params.project)) return refusal("bad-request", "params.project must be a relative path inside the repository.");
|
|
3789
4318
|
if (!this.done || this.repo !== params.repo) return refusal("bad-request", "Send the whole manifest of this repo with build.sync first.");
|
|
@@ -3806,6 +4335,8 @@ var BuildSession = class {
|
|
|
3806
4335
|
project: params.project,
|
|
3807
4336
|
packageName: optional(params.packageName),
|
|
3808
4337
|
isExpo: params.isExpo === true,
|
|
4338
|
+
cacheArtifact: params.cacheArtifact === true,
|
|
4339
|
+
stimBuild: params.stimBuild,
|
|
3809
4340
|
configuration: optional(params.configuration),
|
|
3810
4341
|
scheme: optional(params.scheme),
|
|
3811
4342
|
iosProjectPath: optional(params.iosProjectPath),
|
|
@@ -3862,6 +4393,17 @@ var BuildSession = class {
|
|
|
3862
4393
|
async cachedArtifact(params) {
|
|
3863
4394
|
if (!isJsonObject(params) || !validRepo(params.repo) || !validArtifactIdentity(params.artifact)) return refusal("bad-request", "A cached artifact needs its repository and complete identity.");
|
|
3864
4395
|
if (this.closed) return refusal("build-refused", "The connection closed.");
|
|
4396
|
+
if (params.gradleArtifact !== void 0 || params.reactNativeArtifact !== void 0 || params.swiftpmArtifact !== void 0) {
|
|
4397
|
+
if ([
|
|
4398
|
+
params.gradleArtifact,
|
|
4399
|
+
params.reactNativeArtifact,
|
|
4400
|
+
params.swiftpmArtifact
|
|
4401
|
+
].filter((value) => value !== void 0).length > 1) return refusal("bad-request", "Choose one completed artifact query.");
|
|
4402
|
+
const query = params.gradleArtifact !== void 0 ? gradleArtifactQuery(params.gradleArtifact) : params.swiftpmArtifact !== void 0 ? swiftpmArtifactQuery(params.swiftpmArtifact) : reactNativeArtifactQuery(params.reactNativeArtifact);
|
|
4403
|
+
if (!query) return refusal("bad-request", "Choose the completed source and build options.");
|
|
4404
|
+
const current = "sourceDigest" in query ? await this.host.privateArtifact(this.client, params.repo, query, this.transfers.signal) : await this.host.cache(this.client, params.repo).lookupReactNative(query, this.transfers.signal);
|
|
4405
|
+
if (!current || current.fingerprint !== params.artifact.fingerprint || current.cacheKey !== params.artifact.cacheKey) return { result: null };
|
|
4406
|
+
} else if (params.artifact.cacheKey.startsWith("react-native-") || params.artifact.cacheKey.startsWith("swiftpm-peer-") || (await this.host.cache(this.client, params.repo).lookup(params.artifact, this.transfers.signal))?.androidPackage) return refusal("bad-request", "This cached artifact needs a current build query.");
|
|
3865
4407
|
const area = join(this.host.clientDir(this.client), "repos", params.repo);
|
|
3866
4408
|
mkdirSync(area, {
|
|
3867
4409
|
recursive: true,
|
|
@@ -4079,7 +4621,8 @@ function parseSetupArgs(args, now, ticket) {
|
|
|
4079
4621
|
},
|
|
4080
4622
|
yes: { type: "boolean" },
|
|
4081
4623
|
json: { type: "boolean" },
|
|
4082
|
-
verbose: { type: "boolean" }
|
|
4624
|
+
verbose: { type: "boolean" },
|
|
4625
|
+
"no-wait": { type: "boolean" }
|
|
4083
4626
|
}
|
|
4084
4627
|
});
|
|
4085
4628
|
if (values.ticket !== void 0) throw new SetupRefusal("setup no longer takes --ticket, because other users on this machine can read command-line arguments. Set STIM_SETUP_TICKET=<ticket> before stim-server setup, or copy a new command from Stim Desktop.");
|
|
@@ -4111,7 +4654,8 @@ function parseSetupArgs(args, now, ticket) {
|
|
|
4111
4654
|
pathPrepend,
|
|
4112
4655
|
yes: values.yes === true,
|
|
4113
4656
|
json: values.json === true,
|
|
4114
|
-
verbose: values.verbose === true
|
|
4657
|
+
verbose: values.verbose === true,
|
|
4658
|
+
noWait: values["no-wait"] === true
|
|
4115
4659
|
};
|
|
4116
4660
|
}
|
|
4117
4661
|
/** The record a client made for each capability by spending this run's ticket. */
|
|
@@ -4160,7 +4704,16 @@ function setupClaim() {
|
|
|
4160
4704
|
releaseClaim(attempt.acquired);
|
|
4161
4705
|
};
|
|
4162
4706
|
}
|
|
4163
|
-
const sleep = (ms) => new Promise((resolve) =>
|
|
4707
|
+
const sleep = (ms, signal) => new Promise((resolve) => {
|
|
4708
|
+
const finish = () => {
|
|
4709
|
+
clearTimeout(timer);
|
|
4710
|
+
signal?.removeEventListener("abort", finish);
|
|
4711
|
+
resolve();
|
|
4712
|
+
};
|
|
4713
|
+
const timer = setTimeout(finish, ms);
|
|
4714
|
+
signal?.addEventListener("abort", finish, { once: true });
|
|
4715
|
+
if (signal?.aborted) finish();
|
|
4716
|
+
});
|
|
4164
4717
|
const binary = () => findTailscale(process.env);
|
|
4165
4718
|
async function interruptible(work, signal) {
|
|
4166
4719
|
if (!signal) return work;
|
|
@@ -4276,6 +4829,7 @@ function defaultSetupDeps() {
|
|
|
4276
4829
|
if (!result.ok) throw new ServiceError(result.stderr.trim());
|
|
4277
4830
|
},
|
|
4278
4831
|
records: () => [...readBuildClients(), ...readDeviceHostClients()],
|
|
4832
|
+
watchRecords: watchClientRegistries,
|
|
4279
4833
|
storeSecret: storeSetupSecret,
|
|
4280
4834
|
dropSecret: dropSetupSecret,
|
|
4281
4835
|
panes: hostPermissionPanes,
|
|
@@ -4580,7 +5134,7 @@ async function runSetup(args, version, deps) {
|
|
|
4580
5134
|
protocol: health?.protocol,
|
|
4581
5135
|
running: health !== null
|
|
4582
5136
|
}, version, !desktop);
|
|
4583
|
-
const fix = desktop ? `If Stim Desktop runs this server, update Stim Desktop with Check for Updates in its app menu. Otherwise run npm install --global @stim-cli/server@${version} and restart the server.` : `stim-server service update --label ${opts.label} --release ${version}`;
|
|
5137
|
+
const fix = desktop ? `If Stim Desktop runs this server, update Stim Desktop with Check for Updates in its app menu. Otherwise run npm install --global @stim-cli/server@${version} and restart the server.` : `stim-server service update --label ${opts.label} --release ${version}${opts.noWait ? " --no-wait" : ""}`;
|
|
4584
5138
|
if (decision === "too-old") throw new SetupRefusal(`This server predates setup support (${current?.version} speaks protocol ${health?.protocol ?? 1}; setup needs protocol ${SETUP_PROTOCOL}). ${desktop ? "Update and restart the app's server, then rerun setup." : "Update the service, then rerun setup."}`, false, fix);
|
|
4585
5139
|
if (decision === "update") {
|
|
4586
5140
|
if (!installed?.script || !installed.node || installed.port === null) throw new SetupRefusal("The managed service has no usable server invocation.");
|
|
@@ -4589,7 +5143,7 @@ async function runSetup(args, version, deps) {
|
|
|
4589
5143
|
script: installed.script,
|
|
4590
5144
|
node: installed.node,
|
|
4591
5145
|
port: installed.port
|
|
4592
|
-
}, { release: version }, chatter))) chatter(note);
|
|
5146
|
+
}, { release: version }, chatter, !opts.noWait))) chatter(note);
|
|
4593
5147
|
installed = await wait(deps.installed(opts.label));
|
|
4594
5148
|
const updated = await wait(deps.health(opts.port));
|
|
4595
5149
|
if (!updated) throw new SetupRefusal("The updated server is not answering.");
|
|
@@ -4712,12 +5266,28 @@ async function runSetup(args, version, deps) {
|
|
|
4712
5266
|
approved.push(capability);
|
|
4713
5267
|
}
|
|
4714
5268
|
if (approved.length && deps.now() < Date.parse(opts.expiresAt)) {
|
|
4715
|
-
|
|
4716
|
-
|
|
4717
|
-
|
|
4718
|
-
|
|
4719
|
-
|
|
4720
|
-
|
|
5269
|
+
let changed = false;
|
|
5270
|
+
let sleeping;
|
|
5271
|
+
const stopWatching = deps.watchRecords(() => {
|
|
5272
|
+
changed = true;
|
|
5273
|
+
sleeping?.abort();
|
|
5274
|
+
});
|
|
5275
|
+
try {
|
|
5276
|
+
deps.storeSecret(opts.ticket, approved, opts.expiresAt, deps.now());
|
|
5277
|
+
secretStored = true;
|
|
5278
|
+
if (deps.fromDoctor) printer.line("Waiting for the requesting machine to reconnect. Run `stim doctor` on that machine to finish.");
|
|
5279
|
+
while (deps.now() < Date.parse(opts.expiresAt)) {
|
|
5280
|
+
changed = false;
|
|
5281
|
+
for (const grant of recorded(approved)) note(grant, false);
|
|
5282
|
+
if (approved.every((c) => journal.granted.some((g) => g.capability === c))) break;
|
|
5283
|
+
if (changed) continue;
|
|
5284
|
+
sleeping = new AbortController();
|
|
5285
|
+
await wait(deps.sleep(Math.min(1e3, Math.max(0, Date.parse(opts.expiresAt) - deps.now())), sleeping.signal));
|
|
5286
|
+
sleeping = void 0;
|
|
5287
|
+
}
|
|
5288
|
+
} finally {
|
|
5289
|
+
sleeping?.abort();
|
|
5290
|
+
stopWatching();
|
|
4721
5291
|
}
|
|
4722
5292
|
}
|
|
4723
5293
|
step("approve", journal.granted.length === opts.capabilities.length ? "ok" : "pending", "Access approval", journal.granted.length === opts.capabilities.length ? "Chosen capabilities approved." : "Missing approval: builds or hosted simulators will not work. Generate a new command after expiry.", void 0, approvalView(journal.granted.length === opts.capabilities.length));
|
|
@@ -4798,7 +5368,7 @@ const ERROR_REPEAT_MS = 6e4;
|
|
|
4798
5368
|
const MAX_REMEMBERED = 256;
|
|
4799
5369
|
/** One token of a service-log line: no whitespace or control characters, so a client cannot forge another field or line. */
|
|
4800
5370
|
const printable = (value) => String(value).replace(/[^\x21-\x7e]+/g, "_").slice(0, 80);
|
|
4801
|
-
const text = (fields) => Object.entries(fields).filter(([, value]) => value !== void 0 && value !== null).map(([key, value]) => `${key}=${printable(value)}`).join(" ");
|
|
5371
|
+
const text$1 = (fields) => Object.entries(fields).filter(([, value]) => value !== void 0 && value !== null).map(([key, value]) => `${key}=${printable(value)}`).join(" ");
|
|
4802
5372
|
/**
|
|
4803
5373
|
* Request timing for stim-server. An error reply always goes to the service log (stderr), once a minute per
|
|
4804
5374
|
* client, method and code. With debug logging on, every request goes to the service log and to
|
|
@@ -4859,21 +5429,165 @@ function createRequestLog({ service = (line) => console.error(line), debug = cre
|
|
|
4859
5429
|
error: code
|
|
4860
5430
|
};
|
|
4861
5431
|
if (debug.enabled()) {
|
|
4862
|
-
service(`stim-server: debug request ${text(fields)}`);
|
|
5432
|
+
service(`stim-server: debug request ${text$1(fields)}`);
|
|
4863
5433
|
const { run, ...rest } = fields;
|
|
4864
5434
|
debug.log("request", {
|
|
4865
5435
|
...rest,
|
|
4866
5436
|
runId: run
|
|
4867
5437
|
});
|
|
4868
|
-
} else if (code !== void 0 && errorDue(`${client.id}|${method}|${code}`)) service(`stim-server: request failed ${text(fields)}`);
|
|
5438
|
+
} else if (code !== void 0 && errorDue(`${client.id}|${method}|${code}`)) service(`stim-server: request failed ${text$1(fields)}`);
|
|
4869
5439
|
}
|
|
4870
5440
|
};
|
|
4871
5441
|
},
|
|
4872
5442
|
host(event, fields, failed = false) {
|
|
4873
5443
|
if (debug.enabled()) {
|
|
4874
|
-
service(`stim-server: debug ${event} ${text(fields)}`);
|
|
5444
|
+
service(`stim-server: debug ${event} ${text$1(fields)}`);
|
|
4875
5445
|
debug.log(event, fields);
|
|
4876
|
-
} else if (failed && errorDue(`host|${event}|${String(fields.host)}`)) service(`stim-server: ${event} ${text(fields)}`);
|
|
5446
|
+
} else if (failed && errorDue(`host|${event}|${String(fields.host)}`)) service(`stim-server: ${event} ${text$1(fields)}`);
|
|
5447
|
+
}
|
|
5448
|
+
};
|
|
5449
|
+
}
|
|
5450
|
+
//#endregion
|
|
5451
|
+
//#region src/platform-run.ts
|
|
5452
|
+
const nativeText = [
|
|
5453
|
+
"slot",
|
|
5454
|
+
"remoteBuild",
|
|
5455
|
+
"remote",
|
|
5456
|
+
"easProfile"
|
|
5457
|
+
];
|
|
5458
|
+
const nativeBoolean = ["metroCheck", "buildCache"];
|
|
5459
|
+
const iosText = [
|
|
5460
|
+
"configuration",
|
|
5461
|
+
"scheme",
|
|
5462
|
+
"deviceType",
|
|
5463
|
+
"runtime",
|
|
5464
|
+
"simulatorApp"
|
|
5465
|
+
];
|
|
5466
|
+
const androidText = [
|
|
5467
|
+
"variant",
|
|
5468
|
+
"systemImage",
|
|
5469
|
+
"deviceProfile"
|
|
5470
|
+
];
|
|
5471
|
+
function text(value) {
|
|
5472
|
+
return typeof value === "string" && value.length > 0 && value.length <= 1024 && !value.includes("\0");
|
|
5473
|
+
}
|
|
5474
|
+
function optionalText(value) {
|
|
5475
|
+
return typeof value === "string" ? value : void 0;
|
|
5476
|
+
}
|
|
5477
|
+
function optionalBoolean(value) {
|
|
5478
|
+
return typeof value === "boolean" ? value : void 0;
|
|
5479
|
+
}
|
|
5480
|
+
function selector(value) {
|
|
5481
|
+
return typeof value === "string" || typeof value === "boolean" ? value : void 0;
|
|
5482
|
+
}
|
|
5483
|
+
function parsePlatformRun(params) {
|
|
5484
|
+
if (!isJsonObject(params) || Object.keys(params).some((key) => key !== "cwd" && key !== "options") || typeof params.cwd !== "string" || params.cwd.length > 4096 || params.cwd.includes("\0") || !isAbsolute(params.cwd) || !isJsonObject(params.options)) return null;
|
|
5485
|
+
const { cwd, options } = params;
|
|
5486
|
+
const { platform } = options;
|
|
5487
|
+
if (platform !== "ios" && platform !== "android" && platform !== "macos" && platform !== "web") return null;
|
|
5488
|
+
const texts = platform === "ios" ? [...nativeText, ...iosText] : platform === "android" ? [...nativeText, ...androidText] : platform === "macos" ? ["remote", "remoteBuild"] : [];
|
|
5489
|
+
const booleans = platform === "ios" || platform === "android" ? nativeBoolean : platform === "web" ? ["headed"] : [];
|
|
5490
|
+
for (const [key, value] of Object.entries(options)) {
|
|
5491
|
+
if (key === "platform") continue;
|
|
5492
|
+
if (texts.includes(key) ? !text(value) : booleans.includes(key) ? typeof value !== "boolean" : (platform === "ios" || platform === "android") && (key === "device" || key === "wait") ? typeof value !== "boolean" && !text(value) : true) return null;
|
|
5493
|
+
}
|
|
5494
|
+
if (platform === "web") return {
|
|
5495
|
+
cwd,
|
|
5496
|
+
options: {
|
|
5497
|
+
platform,
|
|
5498
|
+
headed: optionalBoolean(options.headed)
|
|
5499
|
+
}
|
|
5500
|
+
};
|
|
5501
|
+
if (platform === "macos") return {
|
|
5502
|
+
cwd,
|
|
5503
|
+
options: {
|
|
5504
|
+
platform,
|
|
5505
|
+
remote: optionalText(options.remote),
|
|
5506
|
+
remoteBuild: optionalText(options.remoteBuild)
|
|
5507
|
+
}
|
|
5508
|
+
};
|
|
5509
|
+
const native = {
|
|
5510
|
+
slot: optionalText(options.slot),
|
|
5511
|
+
metroCheck: optionalBoolean(options.metroCheck),
|
|
5512
|
+
buildCache: optionalBoolean(options.buildCache),
|
|
5513
|
+
remoteBuild: optionalText(options.remoteBuild),
|
|
5514
|
+
remote: optionalText(options.remote),
|
|
5515
|
+
easProfile: optionalText(options.easProfile),
|
|
5516
|
+
device: selector(options.device),
|
|
5517
|
+
wait: selector(options.wait)
|
|
5518
|
+
};
|
|
5519
|
+
return platform === "ios" ? {
|
|
5520
|
+
cwd,
|
|
5521
|
+
options: {
|
|
5522
|
+
...native,
|
|
5523
|
+
platform,
|
|
5524
|
+
configuration: optionalText(options.configuration),
|
|
5525
|
+
scheme: optionalText(options.scheme),
|
|
5526
|
+
deviceType: optionalText(options.deviceType),
|
|
5527
|
+
runtime: optionalText(options.runtime),
|
|
5528
|
+
simulatorApp: optionalText(options.simulatorApp)
|
|
5529
|
+
}
|
|
5530
|
+
} : {
|
|
5531
|
+
cwd,
|
|
5532
|
+
options: {
|
|
5533
|
+
...native,
|
|
5534
|
+
platform,
|
|
5535
|
+
variant: optionalText(options.variant),
|
|
5536
|
+
systemImage: optionalText(options.systemImage),
|
|
5537
|
+
deviceProfile: optionalText(options.deviceProfile)
|
|
5538
|
+
}
|
|
5539
|
+
};
|
|
5540
|
+
}
|
|
5541
|
+
//#endregion
|
|
5542
|
+
//#region src/platform-build.ts
|
|
5543
|
+
function parsePlatformBuild(params) {
|
|
5544
|
+
if (!isJsonObject(params) || Object.keys(params).some((key) => key !== "cwd" && key !== "options") || typeof params.cwd !== "string" || params.cwd.length > 4096 || params.cwd.includes("\0") || !isAbsolute(params.cwd) || !isJsonObject(params.options)) return null;
|
|
5545
|
+
const { cwd, options } = params;
|
|
5546
|
+
const { platform } = options;
|
|
5547
|
+
if (platform !== "ios" && platform !== "android" && platform !== "macos") return null;
|
|
5548
|
+
const texts = platform === "ios" ? [
|
|
5549
|
+
"configuration",
|
|
5550
|
+
"scheme",
|
|
5551
|
+
"remoteBuild"
|
|
5552
|
+
] : platform === "android" ? ["variant", "remoteBuild"] : ["remoteBuild"];
|
|
5553
|
+
for (const [key, value] of Object.entries(options)) {
|
|
5554
|
+
if (key === "platform" || platform === "ios" && key === "arch" || platform === "android" && key === "abi") continue;
|
|
5555
|
+
if (texts.includes(key) ? typeof value !== "string" || value.length === 0 || value.length > 1024 || value.includes("\0") : platform !== "macos" && key === "buildCache" ? typeof value !== "boolean" : true) return null;
|
|
5556
|
+
}
|
|
5557
|
+
const remoteBuild = typeof options.remoteBuild === "string" ? options.remoteBuild : void 0;
|
|
5558
|
+
if (platform === "macos") return {
|
|
5559
|
+
cwd,
|
|
5560
|
+
options: {
|
|
5561
|
+
platform,
|
|
5562
|
+
remoteBuild
|
|
5563
|
+
}
|
|
5564
|
+
};
|
|
5565
|
+
const buildCache = typeof options.buildCache === "boolean" ? options.buildCache : void 0;
|
|
5566
|
+
if (platform === "ios") {
|
|
5567
|
+
const { arch } = options;
|
|
5568
|
+
if (arch !== void 0 && arch !== "arm64" && arch !== "x86_64" && arch !== "all") return null;
|
|
5569
|
+
return {
|
|
5570
|
+
cwd,
|
|
5571
|
+
options: {
|
|
5572
|
+
platform,
|
|
5573
|
+
remoteBuild,
|
|
5574
|
+
buildCache,
|
|
5575
|
+
arch,
|
|
5576
|
+
configuration: typeof options.configuration === "string" ? options.configuration : void 0,
|
|
5577
|
+
scheme: typeof options.scheme === "string" ? options.scheme : void 0
|
|
5578
|
+
}
|
|
5579
|
+
};
|
|
5580
|
+
}
|
|
5581
|
+
const { abi } = options;
|
|
5582
|
+
if (abi !== void 0 && abi !== "arm64-v8a" && abi !== "armeabi-v7a" && abi !== "x86" && abi !== "x86_64" && abi !== "all") return null;
|
|
5583
|
+
return {
|
|
5584
|
+
cwd,
|
|
5585
|
+
options: {
|
|
5586
|
+
platform,
|
|
5587
|
+
remoteBuild,
|
|
5588
|
+
buildCache,
|
|
5589
|
+
abi,
|
|
5590
|
+
variant: typeof options.variant === "string" ? options.variant : void 0
|
|
4877
5591
|
}
|
|
4878
5592
|
};
|
|
4879
5593
|
}
|
|
@@ -13037,7 +13751,7 @@ function readFailure(cause) {
|
|
|
13037
13751
|
return `${cause.code}: ${cause.message}${cause.remedy ? ` ${cause.remedy}` : ""}`;
|
|
13038
13752
|
}
|
|
13039
13753
|
function stopAction(cwd, env, limits, options = {}) {
|
|
13040
|
-
|
|
13754
|
+
let root = realpathSync(cwd);
|
|
13041
13755
|
if (!lstatSync(root).isDirectory()) throw new Error("The workspace is not a directory.");
|
|
13042
13756
|
let stderr = "";
|
|
13043
13757
|
const client = createStim({
|
|
@@ -13064,7 +13778,11 @@ function stopAction(cwd, env, limits, options = {}) {
|
|
|
13064
13778
|
});
|
|
13065
13779
|
const drained = Promise.resolve().then(() => client.stop({
|
|
13066
13780
|
slot: options.slot,
|
|
13067
|
-
signal: abort.signal
|
|
13781
|
+
signal: abort.signal,
|
|
13782
|
+
beforeStop: (workspace) => {
|
|
13783
|
+
options.beforeStop?.(workspace);
|
|
13784
|
+
root = workspace;
|
|
13785
|
+
}
|
|
13068
13786
|
})).then((result) => {
|
|
13069
13787
|
const stdout = JSON.stringify({
|
|
13070
13788
|
root,
|
|
@@ -13086,7 +13804,8 @@ function stopAction(cwd, env, limits, options = {}) {
|
|
|
13086
13804
|
};
|
|
13087
13805
|
}, (cause) => ({
|
|
13088
13806
|
ok: false,
|
|
13089
|
-
message: readFailure(cause)
|
|
13807
|
+
message: readFailure(cause),
|
|
13808
|
+
...cause instanceof StimError ? { code: cause.code } : {}
|
|
13090
13809
|
})).finally(() => clearTimeout(timer));
|
|
13091
13810
|
return {
|
|
13092
13811
|
outcome: Promise.race([drained, deadline]),
|
|
@@ -13160,34 +13879,199 @@ function reloadAction(cwd, env, limits, options = {}) {
|
|
|
13160
13879
|
}
|
|
13161
13880
|
};
|
|
13162
13881
|
}
|
|
13163
|
-
function
|
|
13164
|
-
|
|
13165
|
-
|
|
13166
|
-
|
|
13167
|
-
|
|
13168
|
-
|
|
13169
|
-
|
|
13170
|
-
|
|
13171
|
-
const
|
|
13172
|
-
|
|
13173
|
-
|
|
13174
|
-
|
|
13175
|
-
|
|
13176
|
-
|
|
13177
|
-
|
|
13178
|
-
}
|
|
13179
|
-
|
|
13180
|
-
|
|
13181
|
-
|
|
13182
|
-
}
|
|
13183
|
-
|
|
13184
|
-
|
|
13185
|
-
|
|
13186
|
-
|
|
13187
|
-
|
|
13188
|
-
|
|
13189
|
-
|
|
13190
|
-
|
|
13882
|
+
function lifecycleAction(cwd, env, limits, label, timeoutCode, execute, onProgress) {
|
|
13883
|
+
const abort = new AbortController();
|
|
13884
|
+
let bytes = 0;
|
|
13885
|
+
let timedOut = false;
|
|
13886
|
+
const timer = setTimeout(() => {
|
|
13887
|
+
timedOut = true;
|
|
13888
|
+
abort.abort();
|
|
13889
|
+
}, limits.timeoutMs);
|
|
13890
|
+
const drained = Promise.resolve().then(() => execute(createStim({
|
|
13891
|
+
projectRoot: cwd,
|
|
13892
|
+
env,
|
|
13893
|
+
onProgress: (progress) => {
|
|
13894
|
+
bytes += Buffer.byteLength(progress.message);
|
|
13895
|
+
if (bytes > limits.maxOutputBytes) throw new StimError({
|
|
13896
|
+
code: "STIM_OUTPUT_LIMIT",
|
|
13897
|
+
message: `${label} printed more than ${limits.maxOutputBytes} bytes.`
|
|
13898
|
+
});
|
|
13899
|
+
onProgress?.(progress);
|
|
13900
|
+
}
|
|
13901
|
+
}), abort.signal)).then((result) => result, (cause) => {
|
|
13902
|
+
const details = cause instanceof StimError && isJsonObject(cause.details) ? cause.details.cause : void 0;
|
|
13903
|
+
return {
|
|
13904
|
+
ok: false,
|
|
13905
|
+
message: readFailure(cause),
|
|
13906
|
+
...cause instanceof StimError ? {
|
|
13907
|
+
code: cause.code,
|
|
13908
|
+
stdout: JSON.stringify({
|
|
13909
|
+
code: cause.code,
|
|
13910
|
+
message: cause.message,
|
|
13911
|
+
remedy: cause.remedy
|
|
13912
|
+
}),
|
|
13913
|
+
unresolved: cause.code === "STIM_CLAIM_UNAVAILABLE",
|
|
13914
|
+
...isJsonObject(details) && Array.isArray(details.retainedClaimRoots) && details.retainedClaimRoots.every((root) => typeof root === "string" && isAbsolute(root)) ? { retainedClaimRoots: details.retainedClaimRoots } : {}
|
|
13915
|
+
} : {}
|
|
13916
|
+
};
|
|
13917
|
+
}).then((outcome) => !outcome.unresolved && bytes + Buffer.byteLength(outcome.stdout ?? "") > limits.maxOutputBytes ? {
|
|
13918
|
+
ok: false,
|
|
13919
|
+
code: "STIM_OUTPUT_LIMIT",
|
|
13920
|
+
unresolved: outcome.unresolved,
|
|
13921
|
+
retainedClaimRoots: outcome.retainedClaimRoots,
|
|
13922
|
+
message: `${label} printed more than ${limits.maxOutputBytes} bytes.`
|
|
13923
|
+
} : outcome).then((outcome) => timedOut && !outcome.unresolved ? {
|
|
13924
|
+
ok: false,
|
|
13925
|
+
code: timeoutCode,
|
|
13926
|
+
message: `${label} did not finish within ${limits.timeoutMs / 1e3} s.`
|
|
13927
|
+
} : outcome).finally(() => clearTimeout(timer));
|
|
13928
|
+
return {
|
|
13929
|
+
outcome: drained,
|
|
13930
|
+
drained,
|
|
13931
|
+
cancel: async () => {
|
|
13932
|
+
abort.abort();
|
|
13933
|
+
return await drained;
|
|
13934
|
+
}
|
|
13935
|
+
};
|
|
13936
|
+
}
|
|
13937
|
+
function startAction(cwd, env, limits, options = {}) {
|
|
13938
|
+
return lifecycleAction(cwd, env, limits, "stim start", "STIM_METRO_TIMEOUT", async (client, signal) => {
|
|
13939
|
+
const result = await client.start({
|
|
13940
|
+
remote: options.remote,
|
|
13941
|
+
resetCache: options.resetCache,
|
|
13942
|
+
waitSeconds: options.waitSeconds,
|
|
13943
|
+
beforeStart: options.beforeStart,
|
|
13944
|
+
signal
|
|
13945
|
+
});
|
|
13946
|
+
return {
|
|
13947
|
+
ok: true,
|
|
13948
|
+
stdout: JSON.stringify(result),
|
|
13949
|
+
summary: `Dev server ${result.alreadyRunning ? "already running" : "ready"} on port ${result.port}.`
|
|
13950
|
+
};
|
|
13951
|
+
}, options.onProgress);
|
|
13952
|
+
}
|
|
13953
|
+
function platformRunAction(cwd, env, limits, options, onProgress) {
|
|
13954
|
+
return lifecycleAction(cwd, env, limits, `stim ${options.platform}`, "STIM_RUN_FAILED", async (client, signal) => {
|
|
13955
|
+
const result = await client.run({
|
|
13956
|
+
...options,
|
|
13957
|
+
signal
|
|
13958
|
+
});
|
|
13959
|
+
const summary = result.platform === "web" ? `${result.facts.url} ${result.facts.launched === true ? "loaded" : result.facts.launched}. ${result.facts.running ? "Logs: stim logs --errors." : "The owned Chrome is no longer running. Run stim logs --errors, then stim web again."}` : `${options.platform} run completed.`;
|
|
13960
|
+
return {
|
|
13961
|
+
ok: true,
|
|
13962
|
+
stdout: JSON.stringify(result),
|
|
13963
|
+
summary
|
|
13964
|
+
};
|
|
13965
|
+
}, onProgress);
|
|
13966
|
+
}
|
|
13967
|
+
function platformBuildAction(cwd, env, limits, options, onProgress) {
|
|
13968
|
+
return lifecycleAction(cwd, env, limits, `${options.platform} build`, "STIM_BUILD_FAILED", async (client, signal) => {
|
|
13969
|
+
const result = await client.build({
|
|
13970
|
+
...options,
|
|
13971
|
+
signal
|
|
13972
|
+
});
|
|
13973
|
+
return {
|
|
13974
|
+
ok: true,
|
|
13975
|
+
stdout: JSON.stringify(result),
|
|
13976
|
+
summary: `${options.platform} build completed.`
|
|
13977
|
+
};
|
|
13978
|
+
}, onProgress);
|
|
13979
|
+
}
|
|
13980
|
+
function gcAction(cwd, env, limits, options, onProgress) {
|
|
13981
|
+
return lifecycleAction(cwd, env, limits, "stim gc", "STIM_GC_TIMEOUT", async (client, signal) => {
|
|
13982
|
+
const { report, exitCode } = await client.collectGarbage({
|
|
13983
|
+
...options,
|
|
13984
|
+
signal
|
|
13985
|
+
});
|
|
13986
|
+
const stdout = JSON.stringify(report);
|
|
13987
|
+
return exitCode === 0 ? {
|
|
13988
|
+
ok: true,
|
|
13989
|
+
stdout,
|
|
13990
|
+
summary: "Garbage collection completed."
|
|
13991
|
+
} : {
|
|
13992
|
+
ok: false,
|
|
13993
|
+
stdout,
|
|
13994
|
+
code: "STIM_GC_FAILED",
|
|
13995
|
+
message: "Garbage collection reported failures."
|
|
13996
|
+
};
|
|
13997
|
+
}, onProgress);
|
|
13998
|
+
}
|
|
13999
|
+
function machineAccessAction(cwd, env, limits, ticket, onProgress) {
|
|
14000
|
+
return lifecycleAction(cwd, env, limits, "Machine access request", "STIM_REPORT_TIMEOUT", async (client, signal) => ({
|
|
14001
|
+
ok: true,
|
|
14002
|
+
stdout: JSON.stringify(await client.requestMachineAccess({
|
|
14003
|
+
ticket,
|
|
14004
|
+
signal
|
|
14005
|
+
})),
|
|
14006
|
+
summary: "Machine access report completed."
|
|
14007
|
+
}), onProgress);
|
|
14008
|
+
}
|
|
14009
|
+
function worktreeWarmAction(cwd, env, limits, options, onProgress) {
|
|
14010
|
+
return lifecycleAction(cwd, env, limits, "stim worktree warm", "STIM_WORKTREE_WARM_TIMEOUT", async (client, signal) => {
|
|
14011
|
+
const result = await client.warmWorktree({
|
|
14012
|
+
...options,
|
|
14013
|
+
signal
|
|
14014
|
+
});
|
|
14015
|
+
const stdout = JSON.stringify(result);
|
|
14016
|
+
return result.exitCode === 0 ? {
|
|
14017
|
+
ok: true,
|
|
14018
|
+
stdout,
|
|
14019
|
+
summary: "Worktree warming completed."
|
|
14020
|
+
} : {
|
|
14021
|
+
ok: false,
|
|
14022
|
+
stdout,
|
|
14023
|
+
code: "STIM_WORKTREE_WARM_FAILED",
|
|
14024
|
+
message: "Worktree warming reported failures."
|
|
14025
|
+
};
|
|
14026
|
+
}, onProgress);
|
|
14027
|
+
}
|
|
14028
|
+
function worktreeRemoveAction(cwd, env, limits, options, onProgress) {
|
|
14029
|
+
return lifecycleAction(cwd, env, limits, "stim worktree remove", "STIM_WORKTREE_REMOVE_TIMEOUT", async (client, signal) => {
|
|
14030
|
+
const result = await client.removeWorktree({
|
|
14031
|
+
...options,
|
|
14032
|
+
signal
|
|
14033
|
+
});
|
|
14034
|
+
const stdout = JSON.stringify(result);
|
|
14035
|
+
return result.exitCode === 0 ? {
|
|
14036
|
+
ok: true,
|
|
14037
|
+
stdout,
|
|
14038
|
+
summary: "Worktree cleanup completed."
|
|
14039
|
+
} : {
|
|
14040
|
+
ok: false,
|
|
14041
|
+
stdout,
|
|
14042
|
+
code: "STIM_WORKTREE_REMOVE_FAILED",
|
|
14043
|
+
message: "Worktree cleanup reported failures."
|
|
14044
|
+
};
|
|
14045
|
+
}, onProgress);
|
|
14046
|
+
}
|
|
14047
|
+
function isLoopback(address) {
|
|
14048
|
+
return address === "127.0.0.1" || address === "::1" || address === "::ffff:127.0.0.1";
|
|
14049
|
+
}
|
|
14050
|
+
/**
|
|
14051
|
+
* The tailnet address of the peer. `tailscale serve` proxies from loopback and names the peer in
|
|
14052
|
+
* X-Forwarded-For; a loopback connection without that header comes from this machine.
|
|
14053
|
+
*/
|
|
14054
|
+
function peerAddress(request) {
|
|
14055
|
+
const remote = request.socket.remoteAddress;
|
|
14056
|
+
if (!isLoopback(remote)) return remote ?? null;
|
|
14057
|
+
const forwarded = request.headers["x-forwarded-for"];
|
|
14058
|
+
return (Array.isArray(forwarded) ? forwarded[0] : forwarded)?.split(",")[0]?.trim() || null;
|
|
14059
|
+
}
|
|
14060
|
+
function peerLabel(identity) {
|
|
14061
|
+
return identity ? `${identity.name || identity.id}${identity.user ? ` (${identity.user})` : ""}` : null;
|
|
14062
|
+
}
|
|
14063
|
+
const NO_TAILSCALE = {
|
|
14064
|
+
state: "unavailable",
|
|
14065
|
+
reason: "the tailscale command was not found"
|
|
14066
|
+
};
|
|
14067
|
+
function serverTransports(options) {
|
|
14068
|
+
const tailscale = options.transports?.find(isTailscaleTransport) ?? tailscaleTransport({
|
|
14069
|
+
env: options.env,
|
|
14070
|
+
snapshot: {
|
|
14071
|
+
binary: options.tailscale ?? null,
|
|
14072
|
+
state: options.tailscaleState ?? NO_TAILSCALE
|
|
14073
|
+
},
|
|
14074
|
+
monitor: options.tailscaleMonitor ?? null
|
|
13191
14075
|
});
|
|
13192
14076
|
return {
|
|
13193
14077
|
transports: options.transports ?? [tailscale],
|
|
@@ -13263,6 +14147,9 @@ function buildAllowed(session) {
|
|
|
13263
14147
|
function requestId(value) {
|
|
13264
14148
|
return typeof value === "number" || typeof value === "string" ? value : null;
|
|
13265
14149
|
}
|
|
14150
|
+
function validPlanRequest(value) {
|
|
14151
|
+
return typeof value === "string" && /^[A-Za-z0-9_-]{1,128}$/.test(value);
|
|
14152
|
+
}
|
|
13266
14153
|
function registeredWorkspace(workspace) {
|
|
13267
14154
|
if (typeof workspace !== "string") return {
|
|
13268
14155
|
code: "bad-request",
|
|
@@ -13481,6 +14368,7 @@ async function startServer(options) {
|
|
|
13481
14368
|
...options.actionLimits
|
|
13482
14369
|
};
|
|
13483
14370
|
const busyWorkspaces = /* @__PURE__ */ new Set();
|
|
14371
|
+
const retainedLifecycles = /* @__PURE__ */ new Set();
|
|
13484
14372
|
const planQueues = /* @__PURE__ */ new Map();
|
|
13485
14373
|
const readDetailsReport = (method, cwd = homedir(), platform) => {
|
|
13486
14374
|
const abort = new AbortController();
|
|
@@ -13791,6 +14679,7 @@ async function startServer(options) {
|
|
|
13791
14679
|
const replays = /* @__PURE__ */ new Map();
|
|
13792
14680
|
let keyframeReads = 0;
|
|
13793
14681
|
const commands = /* @__PURE__ */ new Set();
|
|
14682
|
+
const planRequests = /* @__PURE__ */ new Map();
|
|
13794
14683
|
const persistentActions = /* @__PURE__ */ new Set();
|
|
13795
14684
|
let nextSubscription = 1;
|
|
13796
14685
|
let device = null;
|
|
@@ -13874,7 +14763,19 @@ async function startServer(options) {
|
|
|
13874
14763
|
},
|
|
13875
14764
|
...options.host ? { host } : {},
|
|
13876
14765
|
capabilities: device.capabilities,
|
|
13877
|
-
features: protocol_exports.FEATURES.filter((feature) => (![
|
|
14766
|
+
features: protocol_exports.FEATURES.filter((feature) => (![
|
|
14767
|
+
"desktop-settings-write",
|
|
14768
|
+
"desktop-reload",
|
|
14769
|
+
"desktop-start",
|
|
14770
|
+
"desktop-run",
|
|
14771
|
+
"desktop-build",
|
|
14772
|
+
"desktop-gc",
|
|
14773
|
+
"desktop-worktree-remove",
|
|
14774
|
+
"desktop-worktree-warm",
|
|
14775
|
+
"desktop-machine-access",
|
|
14776
|
+
"desktop-plan"
|
|
14777
|
+
].includes(feature) || options.desktopBootstrap !== void 0) && (local || ![
|
|
14778
|
+
"desktop-plan",
|
|
13878
14779
|
"desktop-reports",
|
|
13879
14780
|
"desktop-report-platform",
|
|
13880
14781
|
"desktop-settings",
|
|
@@ -13882,7 +14783,14 @@ async function startServer(options) {
|
|
|
13882
14783
|
"desktop-full-logs",
|
|
13883
14784
|
"desktop-doctor-run",
|
|
13884
14785
|
"desktop-stop",
|
|
13885
|
-
"desktop-reload"
|
|
14786
|
+
"desktop-reload",
|
|
14787
|
+
"desktop-start",
|
|
14788
|
+
"desktop-run",
|
|
14789
|
+
"desktop-build",
|
|
14790
|
+
"desktop-gc",
|
|
14791
|
+
"desktop-worktree-remove",
|
|
14792
|
+
"desktop-worktree-warm",
|
|
14793
|
+
"desktop-machine-access"
|
|
13886
14794
|
].includes(feature))),
|
|
13887
14795
|
actions: device.capabilities.includes("control") ? [...protocol_exports.ACTIONS] : [],
|
|
13888
14796
|
device: {
|
|
@@ -14561,54 +15469,6 @@ async function startServer(options) {
|
|
|
14561
15469
|
});
|
|
14562
15470
|
subscriptions.set(subscription, cleanup);
|
|
14563
15471
|
}
|
|
14564
|
-
function command(id, args, cwd, result, limits = commandLimits, turn = null) {
|
|
14565
|
-
if (commands.size >= MAX_COMMANDS) {
|
|
14566
|
-
error(id, "limit-exceeded", `A connection can run ${MAX_COMMANDS} requests at a time.`);
|
|
14567
|
-
return null;
|
|
14568
|
-
}
|
|
14569
|
-
let run = null;
|
|
14570
|
-
let dropped = false;
|
|
14571
|
-
let release;
|
|
14572
|
-
const released = new Promise((resolve) => {
|
|
14573
|
-
release = resolve;
|
|
14574
|
-
});
|
|
14575
|
-
const cancel = async () => {
|
|
14576
|
-
dropped = true;
|
|
14577
|
-
if (!run) return void (turn ?? Promise.resolve()).then(release);
|
|
14578
|
-
await run.cancel();
|
|
14579
|
-
release();
|
|
14580
|
-
};
|
|
14581
|
-
const start = async () => {
|
|
14582
|
-
if (dropped) return;
|
|
14583
|
-
run = runStim(options.stimCli, options.env, args, cwd, limits);
|
|
14584
|
-
const outcome = await run.outcome;
|
|
14585
|
-
commands.delete(cancel);
|
|
14586
|
-
running.delete(cancel);
|
|
14587
|
-
try {
|
|
14588
|
-
if (!outcome.ok) {
|
|
14589
|
-
const printed = actionOutcome(outcome);
|
|
14590
|
-
return error(id, "stim-failed", printed.ok ? outcome.message : printed.error.message);
|
|
14591
|
-
}
|
|
14592
|
-
let value;
|
|
14593
|
-
try {
|
|
14594
|
-
value = result(outcome.stdout);
|
|
14595
|
-
} catch {
|
|
14596
|
-
return error(id, "stim-failed", `stim ${args[0]} printed output that is not JSON.`);
|
|
14597
|
-
}
|
|
14598
|
-
send(socket, {
|
|
14599
|
-
id,
|
|
14600
|
-
result: value
|
|
14601
|
-
});
|
|
14602
|
-
} finally {
|
|
14603
|
-
release();
|
|
14604
|
-
}
|
|
14605
|
-
};
|
|
14606
|
-
commands.add(cancel);
|
|
14607
|
-
running.add(cancel);
|
|
14608
|
-
if (turn) turn.then(start);
|
|
14609
|
-
else start();
|
|
14610
|
-
return released;
|
|
14611
|
-
}
|
|
14612
15472
|
function archiveRead(id, read) {
|
|
14613
15473
|
if (commands.size >= MAX_COMMANDS) return error(id, "limit-exceeded", `A connection can run ${MAX_COMMANDS} requests at a time.`);
|
|
14614
15474
|
let dropped = false;
|
|
@@ -14674,15 +15534,22 @@ async function startServer(options) {
|
|
|
14674
15534
|
running.delete(cancel);
|
|
14675
15535
|
});
|
|
14676
15536
|
}
|
|
14677
|
-
function inProcessRead(id, label, read, timeoutMs = commandLimits.timeoutMs) {
|
|
14678
|
-
if (commands.size >= MAX_COMMANDS)
|
|
15537
|
+
function inProcessRead(id, label, read, timeoutMs = commandLimits.timeoutMs, turn = null) {
|
|
15538
|
+
if (commands.size >= MAX_COMMANDS) {
|
|
15539
|
+
error(id, "limit-exceeded", `A connection can run ${MAX_COMMANDS} requests at a time.`);
|
|
15540
|
+
return null;
|
|
15541
|
+
}
|
|
14679
15542
|
const abort = new AbortController();
|
|
14680
|
-
|
|
14681
|
-
|
|
14682
|
-
abort.
|
|
14683
|
-
|
|
14684
|
-
|
|
14685
|
-
|
|
15543
|
+
let deadline;
|
|
15544
|
+
const task = Promise.resolve(turn).then(() => {
|
|
15545
|
+
abort.signal.throwIfAborted();
|
|
15546
|
+
deadline = setTimeout(() => {
|
|
15547
|
+
if (abort.signal.aborted) return;
|
|
15548
|
+
abort.abort();
|
|
15549
|
+
error(id, "stim-failed", `${label} did not finish within ${timeoutMs / 1e3} s.`);
|
|
15550
|
+
}, timeoutMs);
|
|
15551
|
+
return read(abort.signal);
|
|
15552
|
+
}).then((result) => {
|
|
14686
15553
|
if (abort.signal.aborted) return void 0;
|
|
14687
15554
|
if (Buffer.byteLength(JSON.stringify(result)) > commandLimits.maxOutputBytes) error(id, "stim-failed", `${label} printed more than ${commandLimits.maxOutputBytes} bytes.`);
|
|
14688
15555
|
else send(socket, {
|
|
@@ -14702,16 +15569,23 @@ async function startServer(options) {
|
|
|
14702
15569
|
};
|
|
14703
15570
|
commands.add(cancel);
|
|
14704
15571
|
running.add(cancel);
|
|
15572
|
+
return {
|
|
15573
|
+
done: task,
|
|
15574
|
+
cancel
|
|
15575
|
+
};
|
|
14705
15576
|
}
|
|
14706
15577
|
function workspaceCommand(id, method, params) {
|
|
14707
15578
|
if (params !== void 0 && !isJsonObject(params)) return error(id, "bad-request", "params must be an object.");
|
|
14708
15579
|
const cwd = workspaceDir(id, params?.workspace, false);
|
|
14709
15580
|
if (!cwd) return;
|
|
14710
|
-
if (method === "settings.get")
|
|
14711
|
-
|
|
14712
|
-
|
|
14713
|
-
|
|
14714
|
-
|
|
15581
|
+
if (method === "settings.get") {
|
|
15582
|
+
inProcessRead(id, "stim settings", (signal) => createStim({
|
|
15583
|
+
projectRoot: cwd,
|
|
15584
|
+
env: options.env
|
|
15585
|
+
}).settings({ signal }));
|
|
15586
|
+
return;
|
|
15587
|
+
}
|
|
15588
|
+
inProcessRead(id, "stim stats", (signal) => createStim({
|
|
14715
15589
|
projectRoot: cwd,
|
|
14716
15590
|
env: options.env
|
|
14717
15591
|
}).stats({ signal }));
|
|
@@ -14767,7 +15641,16 @@ async function startServer(options) {
|
|
|
14767
15641
|
let admittedCwd = resolved.dir;
|
|
14768
15642
|
let run;
|
|
14769
15643
|
try {
|
|
14770
|
-
run = action.action === "stop" ? stopAction(resolved.dir, options.env, actionLimits
|
|
15644
|
+
run = action.action === "stop" ? stopAction(resolved.dir, options.env, actionLimits, { beforeStop: (workspace) => {
|
|
15645
|
+
if (workspace === resolved.dir) return;
|
|
15646
|
+
if (busyWorkspaces.has(workspace)) throw new StimError({
|
|
15647
|
+
code: "STIM_ACTION_BUSY",
|
|
15648
|
+
message: `An action is already running in ${workspace}.`
|
|
15649
|
+
});
|
|
15650
|
+
busyWorkspaces.add(workspace);
|
|
15651
|
+
admittedCwd = workspace;
|
|
15652
|
+
record.workspace = clip(workspace);
|
|
15653
|
+
} }) : reloadAction(resolved.dir, options.env, actionLimits, {
|
|
14771
15654
|
platform: action.platform,
|
|
14772
15655
|
beforeReload: (workspace) => {
|
|
14773
15656
|
if (workspace === resolved.dir) return;
|
|
@@ -14854,6 +15737,7 @@ async function startServer(options) {
|
|
|
14854
15737
|
const subscription = openSubscription(id);
|
|
14855
15738
|
if (!subscription) return;
|
|
14856
15739
|
const startedAt = Date.now();
|
|
15740
|
+
let admittedCwd = cwd;
|
|
14857
15741
|
const slot = typeof params.slot === "string" ? params.slot : void 0;
|
|
14858
15742
|
let observing = true;
|
|
14859
15743
|
let terminal;
|
|
@@ -14875,6 +15759,15 @@ async function startServer(options) {
|
|
|
14875
15759
|
try {
|
|
14876
15760
|
run = stopAction(cwd, options.env, actionLimits, {
|
|
14877
15761
|
slot,
|
|
15762
|
+
beforeStop: (workspace) => {
|
|
15763
|
+
if (workspace === cwd) return;
|
|
15764
|
+
if (busyWorkspaces.has(workspace)) throw new StimError({
|
|
15765
|
+
code: "STIM_ACTION_BUSY",
|
|
15766
|
+
message: `An action is already running in ${workspace}.`
|
|
15767
|
+
});
|
|
15768
|
+
busyWorkspaces.add(workspace);
|
|
15769
|
+
admittedCwd = workspace;
|
|
15770
|
+
},
|
|
14878
15771
|
onProgress: (progress) => {
|
|
14879
15772
|
if (!observing) return;
|
|
14880
15773
|
outputBytes += Buffer.byteLength(progress.message);
|
|
@@ -14911,6 +15804,7 @@ async function startServer(options) {
|
|
|
14911
15804
|
run.drained.then((outcome) => {
|
|
14912
15805
|
running.delete(cancel);
|
|
14913
15806
|
busyWorkspaces.delete(cwd);
|
|
15807
|
+
busyWorkspaces.delete(admittedCwd);
|
|
14914
15808
|
const recorded = terminal ?? outcome;
|
|
14915
15809
|
auditSafely({
|
|
14916
15810
|
at: new Date(startedAt).toISOString(),
|
|
@@ -14919,7 +15813,7 @@ async function startServer(options) {
|
|
|
14919
15813
|
name: owner.name
|
|
14920
15814
|
},
|
|
14921
15815
|
action: "stop",
|
|
14922
|
-
workspace:
|
|
15816
|
+
workspace: admittedCwd,
|
|
14923
15817
|
...slot === void 0 ? {} : { slot },
|
|
14924
15818
|
ok: recorded.ok,
|
|
14925
15819
|
durationMs: Date.now() - startedAt,
|
|
@@ -15016,6 +15910,193 @@ async function startServer(options) {
|
|
|
15016
15910
|
});
|
|
15017
15911
|
});
|
|
15018
15912
|
}
|
|
15913
|
+
function subscribeStart(id, params, owner) {
|
|
15914
|
+
if (!isJsonObject(params) || Object.keys(params).some((key) => ![
|
|
15915
|
+
"cwd",
|
|
15916
|
+
"remote",
|
|
15917
|
+
"resetCache",
|
|
15918
|
+
"waitSeconds"
|
|
15919
|
+
].includes(key)) || typeof params.cwd !== "string" || params.cwd.length > 4096 || params.cwd.includes("\0") || !isAbsolute(params.cwd) || params.remote !== void 0 && typeof params.remote !== "boolean" || params.resetCache !== void 0 && typeof params.resetCache !== "boolean" || params.waitSeconds !== void 0 && (typeof params.waitSeconds !== "number" || !Number.isSafeInteger(params.waitSeconds) || params.waitSeconds <= 0)) return error(id, "bad-request", "Start requires an absolute cwd, optional remote/resetCache booleans, and a positive whole waitSeconds.");
|
|
15920
|
+
const remote = typeof params.remote === "boolean" ? params.remote : void 0;
|
|
15921
|
+
const resetCache = typeof params.resetCache === "boolean" ? params.resetCache : void 0;
|
|
15922
|
+
const waitSeconds = typeof params.waitSeconds === "number" ? params.waitSeconds : void 0;
|
|
15923
|
+
subscribeLifecycle(id, params.cwd, owner, "start", (cwd, beforeStart, onProgress) => startAction(cwd, options.env, actionLimits, {
|
|
15924
|
+
remote,
|
|
15925
|
+
resetCache,
|
|
15926
|
+
waitSeconds,
|
|
15927
|
+
beforeStart,
|
|
15928
|
+
onProgress
|
|
15929
|
+
}));
|
|
15930
|
+
}
|
|
15931
|
+
function subscribeRun(id, params, owner) {
|
|
15932
|
+
const parsed = parsePlatformRun(params);
|
|
15933
|
+
if (!parsed) return error(id, "bad-request", "Run requires an absolute cwd and valid platform options.");
|
|
15934
|
+
const limits = {
|
|
15935
|
+
...actionLimits,
|
|
15936
|
+
timeoutMs: options.actionLimits?.timeoutMs ?? 36e5
|
|
15937
|
+
};
|
|
15938
|
+
subscribeLifecycle(id, parsed.cwd, owner, "run", (cwd, _beforeRun, onProgress) => platformRunAction(cwd, options.env, limits, parsed.options, onProgress), parsed.options.platform);
|
|
15939
|
+
}
|
|
15940
|
+
function subscribeBuild(id, params, owner) {
|
|
15941
|
+
const parsed = parsePlatformBuild(params);
|
|
15942
|
+
if (!parsed) return error(id, "bad-request", "Build requires an absolute cwd and valid artifact-only options.");
|
|
15943
|
+
const limits = {
|
|
15944
|
+
...actionLimits,
|
|
15945
|
+
timeoutMs: options.actionLimits?.timeoutMs ?? 36e5
|
|
15946
|
+
};
|
|
15947
|
+
subscribeLifecycle(id, parsed.cwd, owner, "build", (cwd, _beforeBuild, onProgress) => platformBuildAction(cwd, options.env, limits, parsed.options, onProgress), parsed.options.platform);
|
|
15948
|
+
}
|
|
15949
|
+
function subscribeGc(id, params, owner) {
|
|
15950
|
+
if (!desktopSessions.has(socket)) return error(id, "forbidden", "Garbage collection requires the connection of the Stim Desktop that started this server.");
|
|
15951
|
+
if (!isJsonObject(params) || Object.keys(params).some((key) => ![
|
|
15952
|
+
"cwd",
|
|
15953
|
+
"cache",
|
|
15954
|
+
"olderThan",
|
|
15955
|
+
"worktrees",
|
|
15956
|
+
"delete",
|
|
15957
|
+
"idleMs"
|
|
15958
|
+
].includes(key)) || typeof params.cwd !== "string" || params.cwd.length > 4096 || params.cwd.includes("\0") || !isAbsolute(params.cwd) || params.cache !== void 0 && (typeof params.cache !== "string" || !params.cache.trim() || params.cache.length > 1024) || params.olderThan !== void 0 && (typeof params.olderThan !== "number" || !Number.isSafeInteger(params.olderThan)) || params.worktrees !== void 0 && typeof params.worktrees !== "boolean" || params.delete !== void 0 && typeof params.delete !== "boolean" || params.idleMs !== void 0 && (typeof params.idleMs !== "number" || !Number.isSafeInteger(params.idleMs) || params.idleMs <= 0)) return error(id, "bad-request", "GC requires an absolute cwd and valid cache, age, worktree, delete and idle selectors.");
|
|
15959
|
+
const request = {
|
|
15960
|
+
cache: typeof params.cache === "string" ? params.cache : void 0,
|
|
15961
|
+
olderThan: typeof params.olderThan === "number" ? params.olderThan : void 0,
|
|
15962
|
+
worktrees: typeof params.worktrees === "boolean" ? params.worktrees : void 0,
|
|
15963
|
+
delete: typeof params.delete === "boolean" ? params.delete : void 0,
|
|
15964
|
+
idleMs: typeof params.idleMs === "number" ? params.idleMs : void 0
|
|
15965
|
+
};
|
|
15966
|
+
subscribeLifecycle(id, params.cwd, owner, "gc", (cwd, _admit, onProgress) => gcAction(cwd, options.env, actionLimits, request, onProgress));
|
|
15967
|
+
}
|
|
15968
|
+
function subscribeMachineAccess(id, params, owner) {
|
|
15969
|
+
if (!desktopSessions.has(socket)) return error(id, "forbidden", "Requesting machine access requires the connection of the Stim Desktop that started this server.");
|
|
15970
|
+
if (!isJsonObject(params) || Object.keys(params).some((key) => key !== "cwd" && key !== "ticket") || typeof params.cwd !== "string" || params.cwd.length > 4096 || params.cwd.includes("\0") || !isAbsolute(params.cwd) || params.ticket !== void 0 && (typeof params.ticket !== "string" || !decodeSecret(params.ticket))) return error(id, "bad-request", "Machine access requires an absolute cwd and an optional valid setup ticket.");
|
|
15971
|
+
const ticket = typeof params.ticket === "string" ? params.ticket : void 0;
|
|
15972
|
+
subscribeLifecycle(id, params.cwd, owner, "machine-access", (cwd, _admit, onProgress) => machineAccessAction(cwd, options.env, actionLimits, ticket, onProgress));
|
|
15973
|
+
}
|
|
15974
|
+
function subscribeWorktreeWarm(id, params, owner) {
|
|
15975
|
+
if (!desktopSessions.has(socket)) return error(id, "forbidden", "Warming worktrees requires the connection of the Stim Desktop that started this server.");
|
|
15976
|
+
if (!isJsonObject(params) || Object.keys(params).some((key) => key !== "cwd") || typeof params.cwd !== "string" || params.cwd.length > 4096 || params.cwd.includes("\0") || !isAbsolute(params.cwd)) return error(id, "bad-request", "Worktree warming requires only an absolute cwd.");
|
|
15977
|
+
const limits = {
|
|
15978
|
+
...actionLimits,
|
|
15979
|
+
timeoutMs: options.actionLimits?.timeoutMs ?? 54e5
|
|
15980
|
+
};
|
|
15981
|
+
subscribeLifecycle(id, params.cwd, owner, "worktree-warm", (cwd, beforeWarm, onProgress) => worktreeWarmAction(cwd, options.env, limits, { beforeWarm }, onProgress));
|
|
15982
|
+
}
|
|
15983
|
+
function subscribeWorktreeRemove(id, params, owner) {
|
|
15984
|
+
if (!desktopSessions.has(socket)) return error(id, "forbidden", "Removing worktrees requires the connection of the Stim Desktop that started this server.");
|
|
15985
|
+
if (!isJsonObject(params) || Object.keys(params).some((key) => ![
|
|
15986
|
+
"cwd",
|
|
15987
|
+
"target",
|
|
15988
|
+
"force"
|
|
15989
|
+
].includes(key)) || typeof params.cwd !== "string" || params.cwd.length > 4096 || params.cwd.includes("\0") || !isAbsolute(params.cwd) || params.target !== void 0 && (typeof params.target !== "string" || params.target.length > 4096 || params.target.includes("\0") || !isAbsolute(params.target)) || params.force !== void 0 && typeof params.force !== "boolean") return error(id, "bad-request", "Worktree removal requires an absolute cwd, optional absolute target and boolean force.");
|
|
15990
|
+
const request = {
|
|
15991
|
+
target: typeof params.target === "string" ? params.target : void 0,
|
|
15992
|
+
force: typeof params.force === "boolean" ? params.force : void 0
|
|
15993
|
+
};
|
|
15994
|
+
subscribeLifecycle(id, params.cwd, owner, "worktree-remove", (cwd, beforeRemove, onProgress) => worktreeRemoveAction(cwd, options.env, actionLimits, {
|
|
15995
|
+
...request,
|
|
15996
|
+
beforeRemove
|
|
15997
|
+
}, onProgress));
|
|
15998
|
+
}
|
|
15999
|
+
function subscribeLifecycle(id, requestedCwd, owner, kind, begin, platform) {
|
|
16000
|
+
let cwd;
|
|
16001
|
+
try {
|
|
16002
|
+
cwd = realpathSync(requestedCwd);
|
|
16003
|
+
if (!lstatSync(cwd).isDirectory()) throw new Error("The workspace is not a directory.");
|
|
16004
|
+
} catch (cause) {
|
|
16005
|
+
return error(id, "bad-request", `Cannot ${kind} this workspace: ${readFailure(cause)}`);
|
|
16006
|
+
}
|
|
16007
|
+
if (commands.size >= MAX_COMMANDS) return error(id, "limit-exceeded", `A connection can run ${MAX_COMMANDS} requests at a time.`);
|
|
16008
|
+
if (busyWorkspaces.has(cwd)) return error(id, "action-busy", `An action is already running in ${cwd}.`);
|
|
16009
|
+
const subscription = openSubscription(id);
|
|
16010
|
+
if (!subscription) return;
|
|
16011
|
+
const startedAt = Date.now();
|
|
16012
|
+
let admittedCwd = cwd;
|
|
16013
|
+
const admittedWorkspaces = /* @__PURE__ */ new Set();
|
|
16014
|
+
let observing = true;
|
|
16015
|
+
let terminal;
|
|
16016
|
+
const finish = (outcome) => {
|
|
16017
|
+
terminal ??= outcome;
|
|
16018
|
+
if (!observing) return;
|
|
16019
|
+
observing = false;
|
|
16020
|
+
subscriptions.delete(subscription);
|
|
16021
|
+
send(socket, {
|
|
16022
|
+
event: `desktop-${kind}-ended`,
|
|
16023
|
+
subscription,
|
|
16024
|
+
exitCode: outcome.ok ? 0 : 1,
|
|
16025
|
+
stdout: outcome.stdout ?? "",
|
|
16026
|
+
message: outcome.ok ? outcome.summary : outcome.message
|
|
16027
|
+
});
|
|
16028
|
+
};
|
|
16029
|
+
const run = begin(cwd, (workspace) => {
|
|
16030
|
+
if (admittedWorkspaces.has(workspace)) return;
|
|
16031
|
+
if (workspace !== cwd && busyWorkspaces.has(workspace)) throw new StimError({
|
|
16032
|
+
code: "STIM_ACTION_BUSY",
|
|
16033
|
+
message: `An action is already running in ${workspace}.`
|
|
16034
|
+
});
|
|
16035
|
+
if (admittedWorkspaces.size === 0) admittedCwd = workspace;
|
|
16036
|
+
admittedWorkspaces.add(workspace);
|
|
16037
|
+
busyWorkspaces.add(workspace);
|
|
16038
|
+
}, (progress) => {
|
|
16039
|
+
if (observing) send(socket, {
|
|
16040
|
+
event: `desktop-${kind}-progress`,
|
|
16041
|
+
subscription,
|
|
16042
|
+
...progress
|
|
16043
|
+
});
|
|
16044
|
+
});
|
|
16045
|
+
const cancel = async () => {
|
|
16046
|
+
await run.cancel();
|
|
16047
|
+
};
|
|
16048
|
+
commands.add(cancel);
|
|
16049
|
+
running.add(cancel);
|
|
16050
|
+
busyWorkspaces.add(cwd);
|
|
16051
|
+
subscriptions.set(subscription, async () => {
|
|
16052
|
+
observing = false;
|
|
16053
|
+
const outcome = await run.cancel();
|
|
16054
|
+
if (!outcome.ok && outcome.unresolved) return {
|
|
16055
|
+
code: "action-failed",
|
|
16056
|
+
message: outcome.message
|
|
16057
|
+
};
|
|
16058
|
+
});
|
|
16059
|
+
run.outcome.then(finish);
|
|
16060
|
+
run.drained.then((outcome) => {
|
|
16061
|
+
if (kind === "gc" || kind === "worktree-remove" || kind === "worktree-warm" || kind === "machine-access") desktopReports.invalidate();
|
|
16062
|
+
const release = () => {
|
|
16063
|
+
commands.delete(cancel);
|
|
16064
|
+
running.delete(cancel);
|
|
16065
|
+
busyWorkspaces.delete(cwd);
|
|
16066
|
+
for (const workspace of admittedWorkspaces) busyWorkspaces.delete(workspace);
|
|
16067
|
+
};
|
|
16068
|
+
const roots = outcome.retainedClaimRoots;
|
|
16069
|
+
if (outcome.unresolved) {
|
|
16070
|
+
const reconcile = () => {
|
|
16071
|
+
if (!roots?.length) return;
|
|
16072
|
+
if (roots.some((root) => {
|
|
16073
|
+
const claims = readClaimSet(root);
|
|
16074
|
+
return claims.live.length > 0 || claims.unresolved.length > 0;
|
|
16075
|
+
})) return;
|
|
16076
|
+
release();
|
|
16077
|
+
retainedLifecycles.delete(reconcile);
|
|
16078
|
+
};
|
|
16079
|
+
retainedLifecycles.add(reconcile);
|
|
16080
|
+
} else release();
|
|
16081
|
+
const recorded = terminal ?? outcome;
|
|
16082
|
+
auditSafely({
|
|
16083
|
+
at: new Date(startedAt).toISOString(),
|
|
16084
|
+
device: {
|
|
16085
|
+
id: owner.id,
|
|
16086
|
+
name: owner.name
|
|
16087
|
+
},
|
|
16088
|
+
action: kind,
|
|
16089
|
+
workspace: admittedCwd,
|
|
16090
|
+
...platform ? { platform } : {},
|
|
16091
|
+
ok: recorded.ok,
|
|
16092
|
+
durationMs: Date.now() - startedAt,
|
|
16093
|
+
...recorded.ok ? {} : { error: {
|
|
16094
|
+
code: "action-failed",
|
|
16095
|
+
message: outcome.unresolved ? "STIM_CLAIM_UNAVAILABLE" : recorded.code || (kind === "start" ? "STIM_METRO_START_FAILED" : kind === "gc" ? "STIM_GC_FAILED" : kind === "build" ? "STIM_BUILD_FAILED" : "STIM_RUN_FAILED")
|
|
16096
|
+
} }
|
|
16097
|
+
});
|
|
16098
|
+
});
|
|
16099
|
+
}
|
|
15019
16100
|
function writeDesktopSetting(id, params, owner) {
|
|
15020
16101
|
if (!isJsonObject(params) || typeof params.cwd !== "string" || !isAbsolute(params.cwd) || params.cwd.length > 4096 || params.cwd.includes("\0") || typeof params.key !== "string" || params.key.length === 0 || params.key.length > 256 || params.action !== "set" && params.action !== "unset" || (params.action === "set" ? typeof params.value !== "string" : "value" in params) || Object.keys(params).some((key) => ![
|
|
15021
16102
|
"cwd",
|
|
@@ -15396,23 +16477,46 @@ async function startServer(options) {
|
|
|
15396
16477
|
if (slot !== void 0 && (typeof slot !== "string" || !SLOT_NAME.test(slot))) return error(id, "bad-request", "params.slot must be 1-64 letters, digits, underscores or hyphens.");
|
|
15397
16478
|
const cwd = workspaceDir(id, params.workspace, true);
|
|
15398
16479
|
if (!cwd) return;
|
|
15399
|
-
|
|
16480
|
+
queuePlan(id, cwd, {
|
|
15400
16481
|
platform,
|
|
15401
|
-
|
|
15402
|
-
|
|
15403
|
-
|
|
15404
|
-
|
|
15405
|
-
|
|
15406
|
-
|
|
15407
|
-
return
|
|
15408
|
-
}
|
|
15409
|
-
|
|
15410
|
-
|
|
15411
|
-
|
|
16482
|
+
...slot === void 0 ? {} : { slot }
|
|
16483
|
+
}, "build.plan");
|
|
16484
|
+
}
|
|
16485
|
+
function queuePlan(id, cwd, plan, method, request) {
|
|
16486
|
+
if (request !== void 0 && planRequests.size >= MAX_COMMANDS) {
|
|
16487
|
+
error(id, "limit-exceeded", "Acknowledge retained plan refusals before requesting more previews.");
|
|
16488
|
+
return;
|
|
16489
|
+
}
|
|
16490
|
+
let unresolved;
|
|
16491
|
+
const finished = inProcessRead(id, `stim ${plan.platform}`, async (signal) => {
|
|
16492
|
+
try {
|
|
16493
|
+
return await createStim({
|
|
16494
|
+
projectRoot: cwd,
|
|
16495
|
+
env: options.env
|
|
16496
|
+
}).plan({
|
|
16497
|
+
...plan,
|
|
16498
|
+
signal
|
|
16499
|
+
});
|
|
16500
|
+
} catch (cause) {
|
|
16501
|
+
if (method !== "desktop.plan.get" || !(cause instanceof StimError)) throw cause;
|
|
16502
|
+
const refusal = {
|
|
16503
|
+
code: cause.code,
|
|
16504
|
+
message: cause.message,
|
|
16505
|
+
remedy: cause.remedy
|
|
16506
|
+
};
|
|
16507
|
+
if (cause.code === "STIM_CLAIM_UNAVAILABLE") unresolved = refusal;
|
|
16508
|
+
return refusal;
|
|
16509
|
+
}
|
|
16510
|
+
}, options.commandLimits?.timeoutMs ?? PLAN_TIMEOUT_MS, planQueues.get(cwd) ?? null);
|
|
15412
16511
|
if (!finished) return;
|
|
15413
|
-
planQueues.set(cwd, finished);
|
|
15414
|
-
|
|
15415
|
-
|
|
16512
|
+
planQueues.set(cwd, finished.done);
|
|
16513
|
+
if (request !== void 0) planRequests.set(request, {
|
|
16514
|
+
cancel: finished.cancel,
|
|
16515
|
+
refusal: () => unresolved
|
|
16516
|
+
});
|
|
16517
|
+
finished.done.finally(() => {
|
|
16518
|
+
if (planQueues.get(cwd) === finished.done) planQueues.delete(cwd);
|
|
16519
|
+
if (request !== void 0 && planRequests.get(request)?.cancel === finished.cancel && !unresolved) planRequests.delete(request);
|
|
15416
16520
|
});
|
|
15417
16521
|
}
|
|
15418
16522
|
function registerPush(id, params, session) {
|
|
@@ -15441,7 +16545,7 @@ async function startServer(options) {
|
|
|
15441
16545
|
async function buildMethod(id, method, params, session) {
|
|
15442
16546
|
if (!buildAllowed(session)) return error(id, "forbidden", `${method} needs build access, which this machine has not granted this device.`);
|
|
15443
16547
|
buildSession ??= builds.session(session.id, socket, (event) => send(socket, event));
|
|
15444
|
-
const answer = method === "build.offer" ? await builds.offer(session.id, params) : method === "build.sync" ? buildSession.sync(params) : method === "build.start" ? await buildSession.start(params) : method === "build.cancel" ? buildSession.cancel(params) : method === "build.attach" ? buildSession.attach(params) : method === "build.cached-artifact" ? await buildSession.cachedArtifact(params) : await buildSession.artifact(params);
|
|
16548
|
+
const answer = method === "build.offer" ? await builds.offer(session.id, params, buildSession.signal) : method === "build.sync" ? buildSession.sync(params) : method === "build.start" ? await buildSession.start(params) : method === "build.cancel" ? buildSession.cancel(params) : method === "build.attach" ? buildSession.attach(params) : method === "build.cached-artifact" ? await buildSession.cachedArtifact(params) : await buildSession.artifact(params);
|
|
15445
16549
|
send(socket, "error" in answer ? {
|
|
15446
16550
|
id,
|
|
15447
16551
|
error: answer.error
|
|
@@ -15464,10 +16568,19 @@ async function startServer(options) {
|
|
|
15464
16568
|
"desktop.settings.write",
|
|
15465
16569
|
"desktop.logs.subscribe",
|
|
15466
16570
|
"desktop.doctor.run",
|
|
16571
|
+
"desktop.machine-access.subscribe",
|
|
15467
16572
|
"desktop.stop.subscribe",
|
|
15468
|
-
"desktop.reload.subscribe"
|
|
16573
|
+
"desktop.reload.subscribe",
|
|
16574
|
+
"desktop.start.subscribe",
|
|
16575
|
+
"desktop.run.subscribe",
|
|
16576
|
+
"desktop.build.subscribe",
|
|
16577
|
+
"desktop.gc.subscribe",
|
|
16578
|
+
"desktop.worktree.remove.subscribe",
|
|
16579
|
+
"desktop.worktree.warm.subscribe",
|
|
16580
|
+
"desktop.plan.get",
|
|
16581
|
+
"desktop.plan.cancel"
|
|
15469
16582
|
].includes(method)) return null;
|
|
15470
|
-
if (!local || options.desktopBootstrap !== void 0 && !desktopSessions.has(socket)) return Promise.resolve(error(id, "forbidden", method === "route.setup" ? "Phone connection setup requires the connection of the Stim Desktop that started this server." : method === "desktop.doctor.run" ? "Running project doctor requires the connection of the Stim Desktop that started this server." : method === "desktop.settings.write" ? "Changing settings requires the connection of the Stim Desktop that started this server." : method === "desktop.reload.subscribe" ? "Reloading apps requires the connection of the Stim Desktop that started this server." : method === "desktop.stop.subscribe" ? "Stopping workspaces requires the connection of the Stim Desktop that started this server." : method.startsWith("desktop.") ? "Desktop
|
|
16583
|
+
if (!local || options.desktopBootstrap !== void 0 && !desktopSessions.has(socket)) return Promise.resolve(error(id, "forbidden", method === "route.setup" ? "Phone connection setup requires the connection of the Stim Desktop that started this server." : method === "desktop.doctor.run" ? "Running project doctor requires the connection of the Stim Desktop that started this server." : method === "desktop.settings.write" ? "Changing settings requires the connection of the Stim Desktop that started this server." : method === "desktop.reload.subscribe" ? "Reloading apps requires the connection of the Stim Desktop that started this server." : method === "desktop.build.subscribe" ? "Building artifacts requires the connection of the Stim Desktop that started this server." : method === "desktop.run.subscribe" ? "Running apps requires the connection of the Stim Desktop that started this server." : method === "desktop.start.subscribe" ? "Starting dev servers requires the connection of the Stim Desktop that started this server." : method === "desktop.stop.subscribe" ? "Stopping workspaces requires the connection of the Stim Desktop that started this server." : method.startsWith("desktop.") ? "Desktop operations require the connection of the Stim Desktop that started this server." : method.startsWith("device-host.sessions") ? "Listing or stopping hosted sessions requires the connection of the Stim Desktop that started this server." : "Updating another machine requires the connection of the Stim Desktop that started this server."));
|
|
15471
16584
|
if (method === "desktop.settings.write") {
|
|
15472
16585
|
if (!desktopSessions.has(socket)) {
|
|
15473
16586
|
error(id, "forbidden", "Changing settings requires the connection of the Stim Desktop that started this server.");
|
|
@@ -15484,6 +16597,93 @@ async function startServer(options) {
|
|
|
15484
16597
|
subscribeReload(id, params, owner);
|
|
15485
16598
|
return Promise.resolve();
|
|
15486
16599
|
}
|
|
16600
|
+
if (method === "desktop.plan.cancel") {
|
|
16601
|
+
if (!desktopSessions.has(socket)) {
|
|
16602
|
+
error(id, "forbidden", "Cancelling a plan requires the connection of the Stim Desktop that started this server.");
|
|
16603
|
+
return Promise.resolve();
|
|
16604
|
+
}
|
|
16605
|
+
if (!isJsonObject(params) || Object.keys(params).length !== 1 || !validPlanRequest(params.request)) {
|
|
16606
|
+
error(id, "bad-request", "Choose the request of this connection to cancel.");
|
|
16607
|
+
return Promise.resolve();
|
|
16608
|
+
}
|
|
16609
|
+
const request = params.request;
|
|
16610
|
+
const pending = planRequests.get(request);
|
|
16611
|
+
(async () => {
|
|
16612
|
+
await pending?.cancel();
|
|
16613
|
+
const refusal = pending?.refusal();
|
|
16614
|
+
if (planRequests.get(request) === pending) planRequests.delete(request);
|
|
16615
|
+
send(socket, {
|
|
16616
|
+
id,
|
|
16617
|
+
result: {
|
|
16618
|
+
cancelled: pending !== void 0,
|
|
16619
|
+
...refusal ? { refusal } : {}
|
|
16620
|
+
}
|
|
16621
|
+
});
|
|
16622
|
+
})();
|
|
16623
|
+
return Promise.resolve();
|
|
16624
|
+
}
|
|
16625
|
+
if (method === "desktop.plan.get") {
|
|
16626
|
+
if (!desktopSessions.has(socket)) {
|
|
16627
|
+
error(id, "forbidden", "Planning builds requires the connection of the Stim Desktop that started this server.");
|
|
16628
|
+
return Promise.resolve();
|
|
16629
|
+
}
|
|
16630
|
+
if (!isJsonObject(params) || Object.keys(params).some((key) => key !== "cwd" && key !== "platform" && key !== "slot" && key !== "request") || typeof params.cwd !== "string" || params.cwd.length > 4096 || !isAbsolute(params.cwd) || params.request !== void 0 && !validPlanRequest(params.request) || params.platform !== "ios" && params.platform !== "android" && params.platform !== "macos" || params.slot !== void 0 && (params.platform === "macos" || typeof params.slot !== "string" || !SLOT_NAME.test(params.slot))) {
|
|
16631
|
+
error(id, "bad-request", "Choose an absolute cwd and ios/android/macos platform; slot applies only to ios/android.");
|
|
16632
|
+
return Promise.resolve();
|
|
16633
|
+
}
|
|
16634
|
+
if (typeof params.request === "string" && planRequests.has(params.request)) {
|
|
16635
|
+
error(id, "bad-request", "This connection already has a plan with that request.");
|
|
16636
|
+
return Promise.resolve();
|
|
16637
|
+
}
|
|
16638
|
+
let cwd;
|
|
16639
|
+
try {
|
|
16640
|
+
cwd = realpathSync(params.cwd);
|
|
16641
|
+
if (!lstatSync(cwd).isDirectory()) throw new Error("not a directory");
|
|
16642
|
+
} catch {
|
|
16643
|
+
error(id, "bad-request", "Choose an existing project directory.");
|
|
16644
|
+
return Promise.resolve();
|
|
16645
|
+
}
|
|
16646
|
+
const plan = params.platform === "macos" ? { platform: "macos" } : {
|
|
16647
|
+
platform: params.platform,
|
|
16648
|
+
...params.slot === void 0 ? {} : { slot: params.slot }
|
|
16649
|
+
};
|
|
16650
|
+
queuePlan(id, cwd, plan, "desktop.plan.get", typeof params.request === "string" ? params.request : void 0);
|
|
16651
|
+
return Promise.resolve();
|
|
16652
|
+
}
|
|
16653
|
+
const subscribeCleanup = {
|
|
16654
|
+
"desktop.machine-access.subscribe": subscribeMachineAccess,
|
|
16655
|
+
"desktop.gc.subscribe": subscribeGc,
|
|
16656
|
+
"desktop.worktree.remove.subscribe": subscribeWorktreeRemove,
|
|
16657
|
+
"desktop.worktree.warm.subscribe": subscribeWorktreeWarm
|
|
16658
|
+
}[method];
|
|
16659
|
+
if (subscribeCleanup) {
|
|
16660
|
+
subscribeCleanup(id, params, owner);
|
|
16661
|
+
return Promise.resolve();
|
|
16662
|
+
}
|
|
16663
|
+
if (method === "desktop.build.subscribe") {
|
|
16664
|
+
if (!desktopSessions.has(socket)) {
|
|
16665
|
+
error(id, "forbidden", "Building artifacts requires the connection of the Stim Desktop that started this server.");
|
|
16666
|
+
return Promise.resolve();
|
|
16667
|
+
}
|
|
16668
|
+
subscribeBuild(id, params, owner);
|
|
16669
|
+
return Promise.resolve();
|
|
16670
|
+
}
|
|
16671
|
+
if (method === "desktop.run.subscribe") {
|
|
16672
|
+
if (!desktopSessions.has(socket)) {
|
|
16673
|
+
error(id, "forbidden", "Running apps requires the connection of the Stim Desktop that started this server.");
|
|
16674
|
+
return Promise.resolve();
|
|
16675
|
+
}
|
|
16676
|
+
subscribeRun(id, params, owner);
|
|
16677
|
+
return Promise.resolve();
|
|
16678
|
+
}
|
|
16679
|
+
if (method === "desktop.start.subscribe") {
|
|
16680
|
+
if (!desktopSessions.has(socket)) {
|
|
16681
|
+
error(id, "forbidden", "Starting dev servers requires the connection of the Stim Desktop that started this server.");
|
|
16682
|
+
return Promise.resolve();
|
|
16683
|
+
}
|
|
16684
|
+
subscribeStart(id, params, owner);
|
|
16685
|
+
return Promise.resolve();
|
|
16686
|
+
}
|
|
15487
16687
|
if (method === "desktop.stop.subscribe") {
|
|
15488
16688
|
subscribeStop(id, params, owner);
|
|
15489
16689
|
return Promise.resolve();
|
|
@@ -15552,11 +16752,14 @@ async function startServer(options) {
|
|
|
15552
16752
|
cwd: resolve(String(params.cwd)),
|
|
15553
16753
|
...params.platform === "ios" || params.platform === "android" ? { platform: params.platform } : {}
|
|
15554
16754
|
};
|
|
15555
|
-
if (method === "desktop.reports.get")
|
|
15556
|
-
|
|
15557
|
-
|
|
15558
|
-
|
|
15559
|
-
|
|
16755
|
+
if (method === "desktop.reports.get") {
|
|
16756
|
+
inProcessRead(id, `stim ${target.report}`, async (signal) => {
|
|
16757
|
+
const snapshot = await desktopReports.get(target, signal);
|
|
16758
|
+
if (snapshot.error) throw new Error(snapshot.error);
|
|
16759
|
+
return snapshot;
|
|
16760
|
+
}, options.commandLimits?.timeoutMs ?? DETAILS_TIMEOUT_MS);
|
|
16761
|
+
return;
|
|
16762
|
+
}
|
|
15560
16763
|
const subscription = openSubscription(id);
|
|
15561
16764
|
if (!subscription) return;
|
|
15562
16765
|
subscriptions.set(subscription, desktopReports.subscribe(target, (snapshot) => send(socket, {
|
|
@@ -15687,17 +16890,7 @@ async function startServer(options) {
|
|
|
15687
16890
|
result: {}
|
|
15688
16891
|
});
|
|
15689
16892
|
}
|
|
15690
|
-
if (method === "device-host.unsubscribe")
|
|
15691
|
-
const name = params.subscription;
|
|
15692
|
-
const unsubscribe = typeof name === "string" ? subscriptions.get(name) : void 0;
|
|
15693
|
-
if (!unsubscribe) return error(id, "unknown-subscription", `No subscription ${String(name)}.`);
|
|
15694
|
-
unsubscribe();
|
|
15695
|
-
subscriptions.delete(name);
|
|
15696
|
-
return send(socket, {
|
|
15697
|
-
id,
|
|
15698
|
-
result: {}
|
|
15699
|
-
});
|
|
15700
|
-
}
|
|
16893
|
+
if (method === "device-host.unsubscribe") return unsubscribeRequest(id, params.subscription);
|
|
15701
16894
|
if (method === "device-host.control.end") {
|
|
15702
16895
|
const name = params.session;
|
|
15703
16896
|
if (typeof name !== "string" || !control.endById(controller(session), name)) return error(id, "unknown-session", `No control session ${String(name)} on this connection.`);
|
|
@@ -15730,6 +16923,20 @@ async function startServer(options) {
|
|
|
15730
16923
|
result: { authorized: true }
|
|
15731
16924
|
});
|
|
15732
16925
|
}
|
|
16926
|
+
async function unsubscribeRequest(id, name) {
|
|
16927
|
+
if (typeof name !== "string") return error(id, "unknown-subscription", `No subscription ${String(name)}.`);
|
|
16928
|
+
const unsubscribe = subscriptions.get(name);
|
|
16929
|
+
if (!unsubscribe) return error(id, "unknown-subscription", `No subscription ${name}.`);
|
|
16930
|
+
subscriptions.delete(name);
|
|
16931
|
+
const failure = await unsubscribe();
|
|
16932
|
+
send(socket, failure ? {
|
|
16933
|
+
id,
|
|
16934
|
+
error: failure
|
|
16935
|
+
} : {
|
|
16936
|
+
id,
|
|
16937
|
+
result: {}
|
|
16938
|
+
});
|
|
16939
|
+
}
|
|
15733
16940
|
async function handle(raw) {
|
|
15734
16941
|
if (socket.readyState !== socket.OPEN) return;
|
|
15735
16942
|
let message;
|
|
@@ -15753,6 +16960,7 @@ async function startServer(options) {
|
|
|
15753
16960
|
if (message.method === "hello") return hello(id, message.params);
|
|
15754
16961
|
if (!device) return refuse(id, "unauthorized", "Send hello first.", CLOSE_REFUSED);
|
|
15755
16962
|
if (message.method === "desktop.authenticate") return authenticateDesktop(id, message.params);
|
|
16963
|
+
for (const reconcile of retainedLifecycles) reconcile();
|
|
15756
16964
|
if (protocol_exports.DEVICE_HOST_METHODS.includes(message.method)) {
|
|
15757
16965
|
if (!device.capabilities.includes("device-host")) return error(id, "forbidden", "Explicit device-host approval is required.");
|
|
15758
16966
|
return hostedMethod(id, message.method, message.params, device);
|
|
@@ -15889,23 +17097,7 @@ async function startServer(options) {
|
|
|
15889
17097
|
result: {}
|
|
15890
17098
|
});
|
|
15891
17099
|
}
|
|
15892
|
-
if (message.method === "unsubscribe")
|
|
15893
|
-
const name = isJsonObject(message.params) ? message.params.subscription : void 0;
|
|
15894
|
-
const unsubscribe = typeof name === "string" ? subscriptions.get(name) : void 0;
|
|
15895
|
-
if (!unsubscribe) return send(socket, {
|
|
15896
|
-
id,
|
|
15897
|
-
error: {
|
|
15898
|
-
code: "unknown-subscription",
|
|
15899
|
-
message: `No subscription ${String(name)}.`
|
|
15900
|
-
}
|
|
15901
|
-
});
|
|
15902
|
-
subscriptions.delete(name);
|
|
15903
|
-
unsubscribe();
|
|
15904
|
-
return send(socket, {
|
|
15905
|
-
id,
|
|
15906
|
-
result: {}
|
|
15907
|
-
});
|
|
15908
|
-
}
|
|
17100
|
+
if (message.method === "unsubscribe") return unsubscribeRequest(id, isJsonObject(message.params) ? message.params.subscription : void 0);
|
|
15909
17101
|
send(socket, {
|
|
15910
17102
|
id,
|
|
15911
17103
|
error: {
|
|
@@ -16448,7 +17640,7 @@ const USAGE = `Usage:
|
|
|
16448
17640
|
--env and --path-prepend apply after the login shell's environment
|
|
16449
17641
|
STIM_SETUP_TICKET=<t> stim-server setup --expires <iso>
|
|
16450
17642
|
[--build] [--device-host] [--port <n>] [--label <name>]
|
|
16451
|
-
[--env KEY=VALUE]... [--path-prepend <dir>]... [--yes] [--json] [--verbose]
|
|
17643
|
+
[--env KEY=VALUE]... [--path-prepend <dir>]... [--yes] [--json] [--verbose] [--no-wait]
|
|
16452
17644
|
worker-side approval of the machine holding this ticket, until expiry
|
|
16453
17645
|
stim-server service install [--port <n>] [--label <name>] [--serve]
|
|
16454
17646
|
[--env KEY=VALUE]... [--path-prepend <dir>]...
|
|
@@ -16456,10 +17648,11 @@ const USAGE = `Usage:
|
|
|
16456
17648
|
a Linux systemd user service, or a Windows scheduled task;
|
|
16457
17649
|
--serve adds a tailnet-only \`tailscale serve\` route
|
|
16458
17650
|
stim-server service status [--label <name>] [--json]
|
|
16459
|
-
stim-server service update [--label <name>] --release <version>|--from <dir>
|
|
17651
|
+
stim-server service update [--label <name>] [--no-wait] --release <version>|--from <dir>
|
|
16460
17652
|
install that stim-server release from npm, or the .tgz
|
|
16461
17653
|
packages in <dir>, beside the current one, switch the
|
|
16462
17654
|
service to it, and switch back if it does not answer
|
|
17655
|
+
--no-wait interrupts active builds and hosted sessions at restart
|
|
16463
17656
|
stim-server service rollback [--label <name>]
|
|
16464
17657
|
switch the service back to the server it ran before
|
|
16465
17658
|
stim-server service uninstall [--label <name>]
|
|
@@ -16643,6 +17836,7 @@ async function runService(sub, extra, values, env, pathPrepend) {
|
|
|
16643
17836
|
"uninstall"
|
|
16644
17837
|
].includes(sub ?? "")) fail(`unknown command.\n${USAGE}`);
|
|
16645
17838
|
if (sub !== "update" && (values.release !== void 0 || values.from !== void 0)) fail(`--release and --from apply only to \`service update\`.\n${USAGE}`);
|
|
17839
|
+
if (sub !== "update" && values["no-wait"]) fail(`--no-wait applies only to \`service update\`.\n${USAGE}`);
|
|
16646
17840
|
if (sub !== "install" && (values.serve || values.port !== void 0 || values.env || values["path-prepend"])) fail(`--port, --serve, --env and --path-prepend apply only to \`service install\`.\n${USAGE}`);
|
|
16647
17841
|
if (sub !== "status" && values.json) fail(`--json applies only to \`service status\`.\n${USAGE}`);
|
|
16648
17842
|
try {
|
|
@@ -16670,7 +17864,7 @@ async function runService(sub, extra, values, env, pathPrepend) {
|
|
|
16670
17864
|
const from = values.from === void 0 ? null : resolve(values.from);
|
|
16671
17865
|
if (from !== null && !statSync(from, { throwIfNoEntry: false })?.isDirectory()) fail(`--from takes a directory of .tgz packages, got ${values.from}.`);
|
|
16672
17866
|
const source = from === null ? { release: values.release } : { from };
|
|
16673
|
-
for (const line of await updateService(label, source, (progress) => console.log(progress))) console.log(line);
|
|
17867
|
+
for (const line of await updateService(label, source, (progress) => console.log(progress), !values["no-wait"])) console.log(line);
|
|
16674
17868
|
} else if (sub === "rollback") for (const line of await rollbackService(label, (progress) => console.log(progress))) console.log(line);
|
|
16675
17869
|
else for (const line of await uninstallService(label)) console.log(line);
|
|
16676
17870
|
} catch (error) {
|
|
@@ -16685,11 +17879,14 @@ async function setup() {
|
|
|
16685
17879
|
process.on("SIGINT", interrupt);
|
|
16686
17880
|
process.on("SIGTERM", interrupt);
|
|
16687
17881
|
const ticket = process.env.STIM_SETUP_TICKET;
|
|
17882
|
+
const fromDoctor = process.env.STIM_SETUP_ORIGIN === "doctor";
|
|
16688
17883
|
delete process.env.STIM_SETUP_TICKET;
|
|
17884
|
+
delete process.env.STIM_SETUP_ORIGIN;
|
|
16689
17885
|
try {
|
|
16690
17886
|
process.exitCode = await runSetup(process.argv.slice(3), pkg.version, {
|
|
16691
17887
|
...defaultSetupDeps(),
|
|
16692
17888
|
ticket,
|
|
17889
|
+
fromDoctor,
|
|
16693
17890
|
signal: controller.signal
|
|
16694
17891
|
});
|
|
16695
17892
|
} finally {
|
|
@@ -16722,6 +17919,7 @@ async function main() {
|
|
|
16722
17919
|
},
|
|
16723
17920
|
release: { type: "string" },
|
|
16724
17921
|
from: { type: "string" },
|
|
17922
|
+
"no-wait": { type: "boolean" },
|
|
16725
17923
|
help: {
|
|
16726
17924
|
type: "boolean",
|
|
16727
17925
|
short: "h"
|
|
@@ -16747,6 +17945,7 @@ async function main() {
|
|
|
16747
17945
|
}
|
|
16748
17946
|
if ((values.label !== void 0 || values.serve) && !service) fail(`--label and --serve apply only to \`service\`.\n${USAGE}`);
|
|
16749
17947
|
if ((values.release !== void 0 || values.from !== void 0) && !service) fail(`--release and --from apply only to \`service update\`.\n${USAGE}`);
|
|
17948
|
+
if (values["no-wait"] && !service) fail(`--no-wait applies only to \`service update\` or \`setup\`.\n${USAGE}`);
|
|
16750
17949
|
if (values["loopback-only"] && command !== void 0) fail(`--loopback-only applies only to serving.\n${USAGE}`);
|
|
16751
17950
|
if (command === void 0) return serve(port, extraEnv, pathPrepend, values["loopback-only"] === true);
|
|
16752
17951
|
const grant = command === "devices" && sub === "grant";
|