stim 1.17.0 → 1.17.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{android--GorydsK.mjs → android-B9yaNKwa.mjs} +15 -15
- package/dist/{budget-DM984mnd.mjs → budget-DfcgFdQB.mjs} +33 -12
- package/dist/{build-plan-CmQCoyT-.mjs → build-plan-BDsSi09V.mjs} +14 -14
- package/dist/{idle-B2m-Ovod.mjs → build-progress-D0WkjCPd.mjs} +56 -183
- package/dist/{cli-Daxug5-K.mjs → cli-CYXb29oE.mjs} +13 -13
- package/dist/cli.mjs +1 -1
- package/dist/{machines-C2XiNwh9.mjs → client-SvBdgObD.mjs} +348 -309
- package/dist/{deps-DNWaQs4V.mjs → deps-iKw0eD4P.mjs} +1 -1
- package/dist/{device-capacity-D0GWHnTM.mjs → device-capacity-ZC4XSZ4R.mjs} +2 -2
- package/dist/device-host-worker.mjs +3 -3
- package/dist/{device-ios-Bc7vbGwl.mjs → device-ios-C2Um-XUR.mjs} +1 -1
- package/dist/{doctor-Cnk9d7Hm.mjs → doctor-CFv1fCrF.mjs} +14 -13
- package/dist/{gc-DxCL2A46.mjs → gc-DEmkg3Xh.mjs} +9 -8
- package/dist/{gradle-D7-L2Th3.mjs → gradle-dJpre3Yn.mjs} +1 -1
- package/dist/{guide-DjrTQGik.mjs → guide-CSmhMTf8.mjs} +156 -127
- package/dist/{hosted-android-DwLS4Mse.mjs → hosted-android-E1Heoqnv.mjs} +3 -3
- package/dist/{hosted-client-wk1W_CNW.mjs → hosted-client-Bp4knSwu.mjs} +4 -4
- package/dist/{hosted-ios-aMo7Cljf.mjs → hosted-ios-BnbeYOUD.mjs} +1 -1
- package/dist/{hosted-logs-DqYmWtXc.mjs → hosted-logs-OcgIBvCT.mjs} +2 -2
- package/dist/{hosted-macos-dlHXEeF5.mjs → hosted-macos-DcxKjtfH.mjs} +2 -2
- package/dist/{hosted-native-C9xJ9rpA.mjs → hosted-native-1Ebbig4w.mjs} +3 -3
- package/dist/idle-BXG__1BA.mjs +176 -0
- package/dist/{idle-shutdown-_3oOV1V_.mjs → idle-shutdown-IGyNV8Cr.mjs} +1 -1
- package/dist/{ios-DE6duQoz.mjs → ios-BJ4XUSe1.mjs} +18 -18
- package/dist/{logs-DUoKlDoS.mjs → logs-BAaS7gS2.mjs} +3 -3
- package/dist/{macos-32po3kUq.mjs → macos-BIRQS90f.mjs} +66 -25
- package/dist/maintenance-run.mjs +2 -2
- package/dist/{metro-gateway-DmslUM30.mjs → metro-gateway-C-ijGI2F.mjs} +1 -1
- package/dist/{native-runtime-DhJI3IVQ.mjs → native-runtime-BEK171xQ.mjs} +1 -1
- package/dist/offload-worker.mjs +5 -5
- package/dist/{preview-CxPRIEht.mjs → preview-B-g9d7kV.mjs} +1 -1
- package/dist/{reload-qZei6iWn.mjs → reload-DcRxj3qG.mjs} +2 -2
- package/dist/settings.schema.json +86 -68
- package/dist/{start-BopZcr4n.mjs → start-AlfCPRVU.mjs} +1 -1
- package/dist/{start-uee1wWjY.mjs → start-DUz3mYdb.mjs} +1 -1
- package/dist/{status-dXX2JX1C.mjs → status-BA2pehwf.mjs} +9 -9
- package/dist/{stop-Cr1d8DSE.mjs → stop-BIiM49EY.mjs} +1 -1
- package/dist/{stop-BjizrQwv.mjs → stop-DQIasgKp.mjs} +3 -3
- package/dist/{stop-BCwvvs7F.mjs → stop-zF1y04f2.mjs} +1 -1
- package/dist/supervisor-run.mjs +4 -4
- package/dist/{support-BJPgYCu-.mjs → support-CsJNVfOc.mjs} +4 -4
- package/dist/{support-F9-sr0Oi.mjs → support-WvD__tSV.mjs} +2 -2
- package/dist/{toolchain-CNIBTIOv.mjs → toolchain-DNyR3NPq.mjs} +10 -6
- package/dist/{web-exQtGkoc.mjs → web-Dl0KSRHf.mjs} +2 -2
- package/dist/{worktree-ZpZR3S1k.mjs → worktree-Be_7NLKy.mjs} +6 -6
- package/dist/{worktree-Dc98cE6_.mjs → worktree-j3IcFAI5.mjs} +1 -1
- package/package.json +5 -5
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { t as getExecutor } from "./exec-CaErVv7t.mjs";
|
|
2
2
|
import { _ as loadConfig$1, d as getConfigDir } from "./config-D0DbpZtM.mjs";
|
|
3
3
|
import { w as withDirLock } from "./workspace-state-DqcCbpE7.mjs";
|
|
4
|
-
import { T as CCACHE_UNAVAILABLE, a as toolchainMismatches, l as COMPILATION_CACHE_UNAVAILABLE, s as manifestDigest } from "./toolchain-
|
|
4
|
+
import { T as CCACHE_UNAVAILABLE, a as toolchainMismatches, l as COMPILATION_CACHE_UNAVAILABLE, s as manifestDigest } from "./toolchain-DNyR3NPq.mjs";
|
|
5
5
|
import { createHash } from "node:crypto";
|
|
6
6
|
import { closeSync, existsSync, lstatSync, mkdirSync, openSync, readFileSync, readlinkSync, realpathSync, renameSync, rmSync, writeFileSync, writeSync } from "node:fs";
|
|
7
7
|
import { basename, dirname, join, relative } from "node:path";
|
|
@@ -30,6 +30,28 @@ function readHostPermissions(value) {
|
|
|
30
30
|
//#region src/offload/tailnet.ts
|
|
31
31
|
const MAC_APP_TAILSCALE = "/Applications/Tailscale.app/Contents/MacOS/Tailscale";
|
|
32
32
|
const HELLO_TIMEOUT_MS = 1e4;
|
|
33
|
+
function isLapsed(credential) {
|
|
34
|
+
return credential.state === "pending" && !!credential.expiresAt && Date.parse(credential.expiresAt) <= Date.now();
|
|
35
|
+
}
|
|
36
|
+
function markApproved(credential) {
|
|
37
|
+
const { expiresAt: _expiresAt, ...rest } = credential;
|
|
38
|
+
return {
|
|
39
|
+
...rest,
|
|
40
|
+
state: "approved"
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
function pendingReport(credential) {
|
|
44
|
+
return {
|
|
45
|
+
state: "pending",
|
|
46
|
+
...credential.expiresAt ? { expiresAt: credential.expiresAt } : {}
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
function lapsedLine(machine, credential) {
|
|
50
|
+
return `${machine} did not approve this Mac before the request lapsed at ${credential.expiresAt}.`;
|
|
51
|
+
}
|
|
52
|
+
function requestedLine(credential) {
|
|
53
|
+
return `Requested at ${credential.requestedAt}${credential.expiresAt ? `; the request lapses at ${credential.expiresAt}` : ""}.`;
|
|
54
|
+
}
|
|
33
55
|
function tailscaleStatus() {
|
|
34
56
|
for (const binary of ["tailscale", MAC_APP_TAILSCALE]) {
|
|
35
57
|
const output = getExecutor().runFileQuiet(binary, ["status", "--json"], { timeoutMs: 5e3 });
|
|
@@ -94,16 +116,280 @@ function pinnedEndpoint$1(credential, status = tailscaleStatus) {
|
|
|
94
116
|
return pinnedEndpoint(credential, parseMachine(credential.machine) ? status() : null);
|
|
95
117
|
}
|
|
96
118
|
//#endregion
|
|
119
|
+
//#region src/device-host/machines.ts
|
|
120
|
+
function note$1(title, detail, fix = null) {
|
|
121
|
+
return {
|
|
122
|
+
code: "device-host-machine",
|
|
123
|
+
level: "note",
|
|
124
|
+
title,
|
|
125
|
+
detail,
|
|
126
|
+
fix
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
const approval$1 = (machine, id) => `A person on ${machine} approves it with \`stim-server devices grant ${id} --device-host\`.`;
|
|
130
|
+
function configuredMachines() {
|
|
131
|
+
const remote = loadConfig$1()?.remote;
|
|
132
|
+
if (remote !== void 0 && !isJsonObject(remote)) return null;
|
|
133
|
+
const machines = remote?.machines;
|
|
134
|
+
if (machines === void 0) return [];
|
|
135
|
+
return acceptsShape(settingDefinition("remote.machines"), machines) ? machines : null;
|
|
136
|
+
}
|
|
137
|
+
/** The `remote.machines` entries this Mac asked for device-host access, in setting order; null when the setting is invalid. */
|
|
138
|
+
function hostingMachines() {
|
|
139
|
+
const configured = configuredMachines();
|
|
140
|
+
if (configured === null) return null;
|
|
141
|
+
try {
|
|
142
|
+
const asked = readDeviceHostMachines();
|
|
143
|
+
return configured.filter((machine) => asked.some((each) => each.machine === machine));
|
|
144
|
+
} catch {
|
|
145
|
+
return configured;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
function store(machines) {
|
|
149
|
+
withDirLock(deviceHostMachinesLock(), () => {
|
|
150
|
+
const file = deviceHostMachinesFile();
|
|
151
|
+
const tmp = `${file}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
|
|
152
|
+
try {
|
|
153
|
+
writeFileSync(tmp, `${JSON.stringify({
|
|
154
|
+
version: 1,
|
|
155
|
+
machines
|
|
156
|
+
}, null, 2)}\n`, { mode: 384 });
|
|
157
|
+
renameSync(tmp, file);
|
|
158
|
+
} finally {
|
|
159
|
+
rmSync(tmp, { force: true });
|
|
160
|
+
}
|
|
161
|
+
}, { ensureParent: () => mkdirSync(getConfigDir(), { recursive: true }) });
|
|
162
|
+
}
|
|
163
|
+
async function request(machine, peer, port, deviceName, io) {
|
|
164
|
+
const ticket = readAccessTicket();
|
|
165
|
+
const reply = await io.hello(endpoint(peer, port), {
|
|
166
|
+
request: "device-host",
|
|
167
|
+
deviceName,
|
|
168
|
+
...ticket ? { setupTicket: ticket.ticket } : {}
|
|
169
|
+
});
|
|
170
|
+
if (!("result" in reply) || reply.result?.approval?.state !== "pending" || typeof reply.result.deviceToken !== "string" || !reply.result.deviceToken || typeof reply.result.device?.id !== "string" || !reply.result.device.id || typeof reply.result.approval.expiresAt !== "string" || !reply.result.approval.expiresAt) return {
|
|
171
|
+
report: {
|
|
172
|
+
machine,
|
|
173
|
+
dnsName: peer.dnsName,
|
|
174
|
+
state: "unreachable"
|
|
175
|
+
},
|
|
176
|
+
finding: note$1(`Could not ask ${machine} for hosting access`, "The worker did not return a pending request.")
|
|
177
|
+
};
|
|
178
|
+
const credential = {
|
|
179
|
+
machine,
|
|
180
|
+
nodeId: peer.nodeId,
|
|
181
|
+
dnsName: peer.dnsName,
|
|
182
|
+
deviceId: reply.result.device.id,
|
|
183
|
+
deviceToken: reply.result.deviceToken,
|
|
184
|
+
state: "pending",
|
|
185
|
+
requestedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
186
|
+
expiresAt: reply.result.approval.expiresAt,
|
|
187
|
+
...ticket ? { ticketHash: ticket.ticketHash } : {}
|
|
188
|
+
};
|
|
189
|
+
return {
|
|
190
|
+
report: {
|
|
191
|
+
machine,
|
|
192
|
+
dnsName: peer.dnsName,
|
|
193
|
+
state: "pending",
|
|
194
|
+
deviceId: credential.deviceId,
|
|
195
|
+
requestedAt: credential.requestedAt,
|
|
196
|
+
expiresAt: credential.expiresAt
|
|
197
|
+
},
|
|
198
|
+
finding: note$1(`Asked ${machine} for hosting access`, `The request lapses at ${reply.result.approval.expiresAt}.`, approval$1(machine, credential.deviceId)),
|
|
199
|
+
credential
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
/** Reports the device-host approval of each remote Mac, separate from its build approval. Only --fix asks or forgets credentials; every token stays pinned. */
|
|
203
|
+
async function inspectDeviceHostMachines({ fix }, io = realIo, entries = configuredMachines()) {
|
|
204
|
+
if (entries === null) return {
|
|
205
|
+
findings: [note$1("Invalid remote.machines setting", "Use a remote object with machines as an array of tailnet names. Existing hosting credentials and pinned nodes are preserved.", "Run `stim guide settings` and correct the setting before running doctor again.")],
|
|
206
|
+
machines: []
|
|
207
|
+
};
|
|
208
|
+
const unavailable = (state, finding) => ({
|
|
209
|
+
findings: [finding],
|
|
210
|
+
machines: entries.map((machine) => ({
|
|
211
|
+
machine,
|
|
212
|
+
state
|
|
213
|
+
}))
|
|
214
|
+
});
|
|
215
|
+
const unreadable = () => unavailable("credentials-unavailable", note$1("Hosting credentials are unreadable", `Stim preserves ${deviceHostMachinesFile()} and its pinned nodes.`, "Restore the existing file from a trusted backup or restore read access before running doctor again."));
|
|
216
|
+
if (!entries.length) {
|
|
217
|
+
if (!fix) return {
|
|
218
|
+
findings: [],
|
|
219
|
+
machines: []
|
|
220
|
+
};
|
|
221
|
+
try {
|
|
222
|
+
if (!readDeviceHostMachines().length) return {
|
|
223
|
+
findings: [],
|
|
224
|
+
machines: []
|
|
225
|
+
};
|
|
226
|
+
} catch {
|
|
227
|
+
return unreadable();
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
const claim = tryAcquireClaim({
|
|
231
|
+
root: deviceHostMachinesClaims(),
|
|
232
|
+
mode: "exclusive",
|
|
233
|
+
label: "device-host approval"
|
|
234
|
+
});
|
|
235
|
+
if (!claim.acquired) {
|
|
236
|
+
releaseClaim(claim.pending);
|
|
237
|
+
return unavailable("busy", note$1("Device-host approval is in use", "Another doctor run holds the hosting credential claim. Run doctor again after it finishes."));
|
|
238
|
+
}
|
|
239
|
+
try {
|
|
240
|
+
let credentials;
|
|
241
|
+
try {
|
|
242
|
+
credentials = readDeviceHostMachines();
|
|
243
|
+
} catch {
|
|
244
|
+
return unreadable();
|
|
245
|
+
}
|
|
246
|
+
if (fix) {
|
|
247
|
+
const kept = credentials.filter((each) => entries.includes(each.machine));
|
|
248
|
+
if (kept.length !== credentials.length) {
|
|
249
|
+
store(kept);
|
|
250
|
+
credentials = kept;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
if (!entries.length) return {
|
|
254
|
+
findings: [],
|
|
255
|
+
machines: []
|
|
256
|
+
};
|
|
257
|
+
const status = io.status();
|
|
258
|
+
if (!isJsonObject(status)) return unavailable("tailscale-off", note$1("Remote Macs are unreachable", "remote.machines names remote Macs, but Tailscale is not running."));
|
|
259
|
+
const self = isJsonObject(status.Self) ? status.Self : {};
|
|
260
|
+
const deviceName = (typeof self.HostName === "string" && self.HostName ? self.HostName : hostname()).replace(/[\p{Cc}\p{Cf}]/gu, "").trim().slice(0, 64) || "Mac";
|
|
261
|
+
const inspected = {
|
|
262
|
+
findings: [],
|
|
263
|
+
machines: []
|
|
264
|
+
};
|
|
265
|
+
const namedNodes = /* @__PURE__ */ new Set();
|
|
266
|
+
for (const machine of new Set(entries)) {
|
|
267
|
+
const parsed = parseMachine$1(machine);
|
|
268
|
+
if (!parsed) {
|
|
269
|
+
inspected.machines.push({
|
|
270
|
+
machine,
|
|
271
|
+
state: "invalid"
|
|
272
|
+
});
|
|
273
|
+
inspected.findings.push(note$1(`Remote Mac ${machine} is not a tailnet name`, "Expected `name` or `name:port`."));
|
|
274
|
+
continue;
|
|
275
|
+
}
|
|
276
|
+
const current = io.status();
|
|
277
|
+
if (!isJsonObject(current)) {
|
|
278
|
+
inspected.machines.push({
|
|
279
|
+
machine,
|
|
280
|
+
state: "tailscale-off"
|
|
281
|
+
});
|
|
282
|
+
inspected.findings.push(note$1(`Remote Mac ${machine} is unreachable`, "Tailscale stopped before this connection."));
|
|
283
|
+
continue;
|
|
284
|
+
}
|
|
285
|
+
const peer = findPeer$1(current, parsed.name);
|
|
286
|
+
if (peer === "missing" || peer === "ambiguous") {
|
|
287
|
+
inspected.machines.push({
|
|
288
|
+
machine,
|
|
289
|
+
state: "not-on-tailnet"
|
|
290
|
+
});
|
|
291
|
+
inspected.findings.push(note$1(`Remote Mac ${machine} is not on this tailnet`, peer === "missing" ? "No peer has that name." : "Several peers match that name."));
|
|
292
|
+
continue;
|
|
293
|
+
}
|
|
294
|
+
const nodeRoute = `${peer.nodeId}:${parsed.port}`;
|
|
295
|
+
if (namedNodes.has(nodeRoute)) {
|
|
296
|
+
inspected.machines.push({
|
|
297
|
+
machine,
|
|
298
|
+
dnsName: peer.dnsName,
|
|
299
|
+
state: "invalid"
|
|
300
|
+
});
|
|
301
|
+
inspected.findings.push(note$1(`Remote Mac ${machine} repeats a named node`, "Name each tailnet node and serve port only once in remote.machines."));
|
|
302
|
+
continue;
|
|
303
|
+
}
|
|
304
|
+
namedNodes.add(nodeRoute);
|
|
305
|
+
const credential = credentials.find((each) => each.machine === machine);
|
|
306
|
+
const known = {
|
|
307
|
+
machine,
|
|
308
|
+
dnsName: peer.dnsName,
|
|
309
|
+
...credential ? {
|
|
310
|
+
deviceId: credential.deviceId,
|
|
311
|
+
requestedAt: credential.requestedAt
|
|
312
|
+
} : {}
|
|
313
|
+
};
|
|
314
|
+
if (credential && credential.nodeId !== peer.nodeId) {
|
|
315
|
+
inspected.machines.push({
|
|
316
|
+
...known,
|
|
317
|
+
state: "node-changed"
|
|
318
|
+
});
|
|
319
|
+
inspected.findings.push(note$1(`Remote Mac ${machine} is a different tailnet node`, "Stim refuses to connect or send the saved token.", `If that Mac was replaced, remove ${machine} from remote.machines, run \`stim doctor --fix\`, then add it back and run \`stim doctor --fix\` again to request approval on the new node.`));
|
|
320
|
+
continue;
|
|
321
|
+
}
|
|
322
|
+
let ask = !credential && fix;
|
|
323
|
+
if (credential) {
|
|
324
|
+
const reply = await io.hello(endpoint(peer, parsed.port), { deviceToken: credential.deviceToken });
|
|
325
|
+
if ("result" in reply && reply.result?.device?.id === credential.deviceId && Array.isArray(reply.result.capabilities) && reply.result.capabilities.includes("device-host")) {
|
|
326
|
+
if (credential.state !== "approved") {
|
|
327
|
+
credentials = credentials.map((each) => each.machine === machine ? markApproved(each) : each);
|
|
328
|
+
store(credentials);
|
|
329
|
+
}
|
|
330
|
+
const host = readHostPermissions(reply.result.host);
|
|
331
|
+
if (host && (host.screenRecording === false || host.accessibility === false)) {
|
|
332
|
+
const panes = [...host.screenRecording === false ? ["Screen & System Audio Recording (Screen Recording on macOS 14)"] : [], ...host.accessibility === false ? ["Device Control and Data Access (Accessibility on macOS 26 and earlier)"] : []];
|
|
333
|
+
const detail = [...host.screenRecording === false ? ["Viewing hosted macOS apps needs Screen Recording."] : [], ...host.accessibility === false ? ["Controlling them needs the control permission."] : []];
|
|
334
|
+
inspected.findings.push(note$1(`Remote Mac ${machine} needs ${panes.join(" and ")} for ${host.name}`, detail.join(" "), `On ${machine}, approve ${host.name} in System Settings > Privacy & Security, or run \`stim-server service install\` there again to show the requests.`));
|
|
335
|
+
}
|
|
336
|
+
inspected.machines.push({
|
|
337
|
+
...known,
|
|
338
|
+
state: "approved",
|
|
339
|
+
...host ? { host } : {}
|
|
340
|
+
});
|
|
341
|
+
continue;
|
|
342
|
+
}
|
|
343
|
+
const pending = "error" in reply && reply.error?.code === "approval-pending";
|
|
344
|
+
const revoked = "error" in reply && reply.error?.code === "unauthorized";
|
|
345
|
+
const ticket = fix && pending && credential.state !== "approved" ? readAccessTicket() : void 0;
|
|
346
|
+
ask = fix && (revoked || !!ticket && credential.ticketHash !== ticket.ticketHash);
|
|
347
|
+
if (pending && !ask) {
|
|
348
|
+
inspected.machines.push({
|
|
349
|
+
...known,
|
|
350
|
+
...pendingReport(credential)
|
|
351
|
+
});
|
|
352
|
+
inspected.findings.push(note$1(`Remote Mac ${machine} has not approved this Mac for device hosting yet`, requestedLine(credential), approval$1(machine, credential.deviceId)));
|
|
353
|
+
continue;
|
|
354
|
+
}
|
|
355
|
+
if (!ask) {
|
|
356
|
+
const lapsed = revoked && isLapsed(credential);
|
|
357
|
+
inspected.machines.push({
|
|
358
|
+
...known,
|
|
359
|
+
state: lapsed ? "lapsed" : revoked ? "revoked" : "unreachable"
|
|
360
|
+
});
|
|
361
|
+
inspected.findings.push(note$1(lapsed ? `Hosting request to ${machine} lapsed` : `Remote Mac ${machine} ${revoked ? "no longer accepts this Mac for device hosting" : "did not confirm hosting access"}`, lapsed ? `${lapsedLine(machine, credential)} The saved token and pinned node are preserved.` : "The saved token and pinned node are preserved.", revoked ? "Run `stim doctor --fix` to ask again." : `Check stim-server and its tailnet serve route on ${machine}, then run doctor again.`));
|
|
362
|
+
continue;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
if (ask) {
|
|
366
|
+
const result = await request(machine, peer, parsed.port, deviceName, io);
|
|
367
|
+
if (result.credential) {
|
|
368
|
+
credentials = [...credentials.filter((each) => each.machine !== machine), result.credential];
|
|
369
|
+
store(credentials);
|
|
370
|
+
}
|
|
371
|
+
inspected.machines.push(result.report);
|
|
372
|
+
inspected.findings.push(result.finding);
|
|
373
|
+
} else {
|
|
374
|
+
inspected.machines.push({
|
|
375
|
+
...known,
|
|
376
|
+
state: "not-asked"
|
|
377
|
+
});
|
|
378
|
+
inspected.findings.push(note$1(`Remote Mac ${machine} has not approved this Mac for device hosting`, "This Mac has not asked for hosting access.", "Run `stim doctor --fix` to ask."));
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
return inspected;
|
|
382
|
+
} finally {
|
|
383
|
+
releaseClaim(claim.acquired);
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
//#endregion
|
|
97
387
|
//#region src/offload/build-machines.ts
|
|
98
|
-
/** The
|
|
99
|
-
function pairedMachines(entries = configuredMachines
|
|
388
|
+
/** The `remote.machines` entries that this Mac asked for build access, in that order. */
|
|
389
|
+
function pairedMachines(entries = configuredMachines() ?? []) {
|
|
100
390
|
const credentials = readBuildMachines();
|
|
101
391
|
return entries.flatMap((entry) => credentials.filter((each) => each.machine === entry));
|
|
102
392
|
}
|
|
103
|
-
function configuredMachines$1() {
|
|
104
|
-
const machines = loadConfig$1()?.offload?.machines;
|
|
105
|
-
return Array.isArray(machines) ? machines.filter((entry) => typeof entry === "string") : [];
|
|
106
|
-
}
|
|
107
393
|
function updateCredentials(change) {
|
|
108
394
|
const dir = getConfigDir();
|
|
109
395
|
withDirLock(buildMachinesLock(), () => {
|
|
@@ -121,7 +407,7 @@ function updateCredentials(change) {
|
|
|
121
407
|
}
|
|
122
408
|
}, { ensureParent: () => mkdirSync(dir, { recursive: true }) });
|
|
123
409
|
}
|
|
124
|
-
function note
|
|
410
|
+
function note(title, detail, fix = null) {
|
|
125
411
|
return {
|
|
126
412
|
code: "build-machine",
|
|
127
413
|
level: "note",
|
|
@@ -130,7 +416,7 @@ function note$1(title, detail, fix = null) {
|
|
|
130
416
|
fix
|
|
131
417
|
};
|
|
132
418
|
}
|
|
133
|
-
const approval
|
|
419
|
+
const approval = (entry, deviceId) => `A person on ${entry} approves it with \`stim-server devices grant ${deviceId} --build\`.`;
|
|
134
420
|
const PROBLEM_TITLES = {
|
|
135
421
|
unreachable: "does not answer a build offer",
|
|
136
422
|
checkout: "cannot build an app outside a git checkout",
|
|
@@ -155,7 +441,7 @@ function problemFix(code, entry) {
|
|
|
155
441
|
case "unreachable": return `Check that stim-server runs on ${entry} and that its tailscale serve route answers, then run \`stim doctor\` again. To keep it running there, run \`stim-server service install --serve\` on ${entry}.`;
|
|
156
442
|
case "checkout": return "Offload syncs the files git lists; run Stim from a git checkout of the app.";
|
|
157
443
|
case "stim-build": return `Update stim-server on ${entry} to the same Stim build as this Mac: there, run \`stim-server service update --release <version>\` for this Mac's release, or \`--from <dir>\` with the packed packages of this Mac's checkout.`;
|
|
158
|
-
case "arch": return "Use a
|
|
444
|
+
case "arch": return "Use a remote Mac with the same CPU architecture as this Mac.";
|
|
159
445
|
case "xcode":
|
|
160
446
|
case "simulator-sdk":
|
|
161
447
|
case "macos-sdk": return `Install and select the same Xcode on ${entry} and this Mac (\`xcode-select -p\` on each).`;
|
|
@@ -167,8 +453,8 @@ function problemFix(code, entry) {
|
|
|
167
453
|
case "ndk":
|
|
168
454
|
case "build-tools":
|
|
169
455
|
case "compile-sdk": return `Install the missing package with sdkmanager in the Android SDK stim-server on ${entry} uses (its ANDROID_HOME, else ~/Library/Android/sdk).`;
|
|
170
|
-
case "disk": return `Free space on ${entry}'s worker root (
|
|
171
|
-
case "busy": return `Builds stay on this Mac until ${entry} has capacity. On ${entry},
|
|
456
|
+
case "disk": return `Free space on ${entry}'s worker root (server.workerRoot there).`;
|
|
457
|
+
case "busy": return `Builds stay on this Mac until ${entry} has capacity. On ${entry}, server.maxLoadPerCore sets the load it accepts and concurrency.maxBuilds how many of its own builds it runs; it takes one offloaded build at a time.`;
|
|
172
458
|
}
|
|
173
459
|
}
|
|
174
460
|
const TRANSIENT = /* @__PURE__ */ new Set(["unreachable", "busy"]);
|
|
@@ -176,7 +462,7 @@ function offloadFindings(entry, problems) {
|
|
|
176
462
|
return problems.map((problem) => ({
|
|
177
463
|
code: `build-machine-${problem.code}`,
|
|
178
464
|
level: TRANSIENT.has(problem.code) ? "note" : "cost",
|
|
179
|
-
title: `
|
|
465
|
+
title: `Remote Mac ${entry} ${PROBLEM_TITLES[problem.code]}`,
|
|
180
466
|
detail: `Builds fall back to this Mac: ${problem.reason}.`,
|
|
181
467
|
fix: problemFix(problem.code, entry)
|
|
182
468
|
}));
|
|
@@ -196,7 +482,7 @@ async function requestAccess(entry, peer, port, deviceName, io) {
|
|
|
196
482
|
state: "unreachable",
|
|
197
483
|
dnsName: peer.dnsName
|
|
198
484
|
},
|
|
199
|
-
finding: note
|
|
485
|
+
finding: note(`Could not ask ${entry} for build access`, reason)
|
|
200
486
|
};
|
|
201
487
|
}
|
|
202
488
|
const credential = {
|
|
@@ -207,6 +493,7 @@ async function requestAccess(entry, peer, port, deviceName, io) {
|
|
|
207
493
|
deviceToken: reply.result.deviceToken,
|
|
208
494
|
state: "pending",
|
|
209
495
|
requestedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
496
|
+
expiresAt: reply.result.approval.expiresAt,
|
|
210
497
|
...ticket ? { ticketHash: ticket.ticketHash } : {}
|
|
211
498
|
};
|
|
212
499
|
updateCredentials((credentials) => [...credentials.filter((each) => each.machine !== entry), credential]);
|
|
@@ -216,9 +503,10 @@ async function requestAccess(entry, peer, port, deviceName, io) {
|
|
|
216
503
|
state: "pending",
|
|
217
504
|
dnsName: peer.dnsName,
|
|
218
505
|
deviceId: credential.deviceId,
|
|
219
|
-
requestedAt: credential.requestedAt
|
|
506
|
+
requestedAt: credential.requestedAt,
|
|
507
|
+
expiresAt: credential.expiresAt
|
|
220
508
|
},
|
|
221
|
-
finding: note
|
|
509
|
+
finding: note(`Asked ${entry} for build access`, `The request lapses at ${reply.result.approval.expiresAt}.`, approval(entry, credential.deviceId))
|
|
222
510
|
};
|
|
223
511
|
}
|
|
224
512
|
async function inspectMachine(entry, status, deviceName, fix, io, check) {
|
|
@@ -228,7 +516,7 @@ async function inspectMachine(entry, status, deviceName, fix, io, check) {
|
|
|
228
516
|
machine: entry,
|
|
229
517
|
state: "invalid"
|
|
230
518
|
},
|
|
231
|
-
finding: note
|
|
519
|
+
finding: note(`Remote Mac ${entry} is not a tailnet name`, "Expected `name` or `name:port`.")
|
|
232
520
|
};
|
|
233
521
|
const peer = findPeer$1(status, parsed.name);
|
|
234
522
|
if (peer === "missing" || peer === "ambiguous") {
|
|
@@ -238,7 +526,7 @@ async function inspectMachine(entry, status, deviceName, fix, io, check) {
|
|
|
238
526
|
machine: entry,
|
|
239
527
|
state: "not-on-tailnet"
|
|
240
528
|
},
|
|
241
|
-
finding: note
|
|
529
|
+
finding: note(`Remote Mac ${entry} is not on this tailnet`, detail)
|
|
242
530
|
};
|
|
243
531
|
}
|
|
244
532
|
const credential = readBuildMachines().find((each) => each.machine === entry);
|
|
@@ -253,7 +541,7 @@ async function inspectMachine(entry, status, deviceName, fix, io, check) {
|
|
|
253
541
|
deviceId: credential.deviceId,
|
|
254
542
|
requestedAt: credential.requestedAt
|
|
255
543
|
},
|
|
256
|
-
finding: note
|
|
544
|
+
finding: note(`Remote Mac ${entry} is a different tailnet node`, `This Mac paired with node ${credential.nodeId}, but ${peer.dnsName} is now node ${peer.nodeId}. Stim does not connect to it.`, `If that Mac was replaced, remove ${entry} from remote.machines, run \`stim doctor --fix\` to forget the old pairing, then add it back and run \`stim doctor --fix\` again.`)
|
|
257
545
|
};
|
|
258
546
|
if (!credential) {
|
|
259
547
|
if (fix) return requestAccess(entry, peer, parsed.port, deviceName, io);
|
|
@@ -262,7 +550,7 @@ async function inspectMachine(entry, status, deviceName, fix, io, check) {
|
|
|
262
550
|
...known,
|
|
263
551
|
state: "not-asked"
|
|
264
552
|
},
|
|
265
|
-
finding: note
|
|
553
|
+
finding: note(`Remote Mac ${entry} has not approved this Mac for builds`, "This Mac has not asked it for build access.", "Run `stim doctor --fix` to ask.")
|
|
266
554
|
};
|
|
267
555
|
}
|
|
268
556
|
const paired = {
|
|
@@ -274,10 +562,7 @@ async function inspectMachine(entry, status, deviceName, fix, io, check) {
|
|
|
274
562
|
if ("result" in reply && reply.result.capabilities.includes("build")) {
|
|
275
563
|
const permissions = readHostPermissions(reply.result.host);
|
|
276
564
|
const host = permissions ? { host: permissions } : {};
|
|
277
|
-
if (credential.state !== "approved") updateCredentials((credentials) => credentials.map((each) => each.machine === entry ?
|
|
278
|
-
...each,
|
|
279
|
-
state: "approved"
|
|
280
|
-
} : each));
|
|
565
|
+
if (credential.state !== "approved") updateCredentials((credentials) => credentials.map((each) => each.machine === entry ? markApproved(each) : each));
|
|
281
566
|
if (!check) return {
|
|
282
567
|
report: {
|
|
283
568
|
...paired,
|
|
@@ -310,19 +595,26 @@ async function inspectMachine(entry, status, deviceName, fix, io, check) {
|
|
|
310
595
|
return {
|
|
311
596
|
report: {
|
|
312
597
|
...paired,
|
|
313
|
-
|
|
598
|
+
...pendingReport(credential)
|
|
314
599
|
},
|
|
315
|
-
finding: note
|
|
600
|
+
finding: note(`Remote Mac ${entry} has not approved this Mac for builds yet`, requestedLine(credential), approval(entry, credential.deviceId))
|
|
316
601
|
};
|
|
317
602
|
}
|
|
318
603
|
if ("error" in reply && reply.error.code === "unauthorized") {
|
|
319
604
|
if (fix) return requestAccess(entry, peer, parsed.port, deviceName, io);
|
|
605
|
+
if (isLapsed(credential)) return {
|
|
606
|
+
report: {
|
|
607
|
+
...paired,
|
|
608
|
+
state: "lapsed"
|
|
609
|
+
},
|
|
610
|
+
finding: note(`Build request to ${entry} lapsed`, lapsedLine(entry, credential), "Run `stim doctor --fix` to ask again.")
|
|
611
|
+
};
|
|
320
612
|
return {
|
|
321
613
|
report: {
|
|
322
614
|
...paired,
|
|
323
615
|
state: "revoked"
|
|
324
616
|
},
|
|
325
|
-
finding: note
|
|
617
|
+
finding: note(`Remote Mac ${entry} no longer accepts this Mac for builds`, `${reply.error.message} It was revoked or denied.`, "Run `stim doctor --fix` to ask again.")
|
|
326
618
|
};
|
|
327
619
|
}
|
|
328
620
|
const reason = "failed" in reply ? reply.failed : "error" in reply ? reply.error.message : "unexpected reply";
|
|
@@ -331,17 +623,21 @@ async function inspectMachine(entry, status, deviceName, fix, io, check) {
|
|
|
331
623
|
...paired,
|
|
332
624
|
state: "unreachable"
|
|
333
625
|
},
|
|
334
|
-
finding: note
|
|
626
|
+
finding: note(`Could not reach remote Mac ${entry}`, reason, `Check that stim-server runs on ${entry}. To keep it running there, run \`stim-server service install --serve\` on ${entry}.`)
|
|
335
627
|
};
|
|
336
628
|
}
|
|
337
629
|
/**
|
|
338
|
-
* Doctor findings and a report for each `
|
|
630
|
+
* Doctor findings and a report for each `remote.machines` entry. The worker's node is pinned when access is
|
|
339
631
|
* requested; a later connection goes only to the current MagicDNS name of that same node, checked before the token
|
|
340
632
|
* is sent. With `fix`, requests access from a named machine this Mac holds no pairing for, requests again when the
|
|
341
633
|
* pinned node forgot this Mac or, with `STIM_ACCESS_TICKET` set, when a pending request carries another ticket, and forgets pairings of machines no longer named. With `check`, each approved
|
|
342
634
|
* machine is asked for one build offer and reports every reason it would not take this project's build.
|
|
343
635
|
*/
|
|
344
|
-
async function inspectBuildMachines({ fix, check = null }, io = realIo, entries = configuredMachines
|
|
636
|
+
async function inspectBuildMachines({ fix, check = null }, io = realIo, entries = configuredMachines()) {
|
|
637
|
+
if (entries === null) return {
|
|
638
|
+
findings: [note("Invalid remote.machines setting", "Use a remote object with machines as an array of tailnet names. Existing credentials and pinned nodes are preserved.", "Run `stim guide settings` and correct the setting before running doctor again.")],
|
|
639
|
+
machines: []
|
|
640
|
+
};
|
|
345
641
|
const unnamed = (each) => !entries.includes(each.machine);
|
|
346
642
|
if (fix && readBuildMachines().some(unnamed)) updateCredentials((credentials) => credentials.filter((each) => !unnamed(each)));
|
|
347
643
|
if (entries.length === 0) return {
|
|
@@ -350,7 +646,7 @@ async function inspectBuildMachines({ fix, check = null }, io = realIo, entries
|
|
|
350
646
|
};
|
|
351
647
|
const status = io.status();
|
|
352
648
|
if (!isJsonObject(status)) return {
|
|
353
|
-
findings: [note
|
|
649
|
+
findings: [note("Remote Macs are unreachable", "remote.machines names remote Macs, but Tailscale is not running.")],
|
|
354
650
|
machines: entries.map((machine) => ({
|
|
355
651
|
machine,
|
|
356
652
|
state: "tailscale-off"
|
|
@@ -374,14 +670,14 @@ async function inspectBuildMachines({ fix, check = null }, io = realIo, entries
|
|
|
374
670
|
//#region src/offload/selection.ts
|
|
375
671
|
var OffloadRefusal = class extends Error {
|
|
376
672
|
code = "STIM_OFFLOAD_REFUSED";
|
|
377
|
-
remedy = "Run stim doctor --fix to ask for build access if not paired. On the worker, a person finds the id with stim-server devices and approves it with stim-server devices grant <id> --build. Check stim settings get
|
|
673
|
+
remedy = "Run stim doctor --fix to ask for build access if not paired. On the worker, a person finds the id with stim-server devices and approves it with stim-server devices grant <id> --build. Check stim settings get remote.machines, or rerun with --remote-build auto or --remote-build local.";
|
|
378
674
|
constructor(machine, reason) {
|
|
379
675
|
super(reason.startsWith(`${machine}: `) ? reason : `${machine}: ${reason}`);
|
|
380
676
|
}
|
|
381
677
|
};
|
|
382
678
|
function resolveBuildMachine(flag, env, setting) {
|
|
383
679
|
const selected = flag ?? (env?.trim() || void 0) ?? setting ?? "auto";
|
|
384
|
-
if (typeof selected !== "string" || !selected.trim() || !parseMachine(selected)) throw Object.assign(/* @__PURE__ */ new Error("--build
|
|
680
|
+
if (typeof selected !== "string" || !selected.trim() || !parseMachine(selected)) throw Object.assign(/* @__PURE__ */ new Error("--remote-build / remote.build must be auto, local, or a tailnet machine name."), { code: "STIM_BAD_ARG" });
|
|
385
681
|
const trimmed = selected.trim();
|
|
386
682
|
const reserved = trimmed.toLowerCase();
|
|
387
683
|
return reserved === "auto" || reserved === "local" ? reserved : trimmed;
|
|
@@ -404,16 +700,16 @@ function requireConfiguredMachine(selected, machines) {
|
|
|
404
700
|
const candidate = parseMachine(entry);
|
|
405
701
|
return candidate?.name === parsed.name && candidate.port === parsed.port;
|
|
406
702
|
});
|
|
407
|
-
if (!match) throw new OffloadRefusal(selected, `not listed in
|
|
703
|
+
if (!match) throw new OffloadRefusal(selected, `not listed in remote.machines (configured: ${entries.join(", ") || "none"})`);
|
|
408
704
|
return match;
|
|
409
705
|
}
|
|
410
706
|
function resolveBuildPlacement(flag) {
|
|
411
|
-
const
|
|
707
|
+
const remote = loadConfig$1()?.remote;
|
|
412
708
|
let selected = "auto";
|
|
413
709
|
try {
|
|
414
|
-
selected = resolveBuildMachine(flag, process.env.
|
|
415
|
-
selected = requireConfiguredMachine(selected,
|
|
416
|
-
if (namedBuildMachine(selected) && pairedMachines([selected]).length === 0) throw new OffloadRefusal(selected, "no
|
|
710
|
+
selected = resolveBuildMachine(flag, process.env.STIM_REMOTE_BUILD, remote?.build);
|
|
711
|
+
selected = requireConfiguredMachine(selected, remote?.machines);
|
|
712
|
+
if (namedBuildMachine(selected) && pairedMachines([selected]).length === 0) throw new OffloadRefusal(selected, "no remote Mac is paired");
|
|
417
713
|
return { selected };
|
|
418
714
|
} catch (error) {
|
|
419
715
|
return {
|
|
@@ -421,7 +717,7 @@ function resolveBuildPlacement(flag) {
|
|
|
421
717
|
failure: {
|
|
422
718
|
code: error.code,
|
|
423
719
|
message: error.message,
|
|
424
|
-
remedy: error instanceof OffloadRefusal ? error.remedy : "Pass --build
|
|
720
|
+
remedy: error instanceof OffloadRefusal ? error.remedy : "Pass --remote-build auto, local, or an entry from stim settings get remote.machines."
|
|
425
721
|
}
|
|
426
722
|
};
|
|
427
723
|
}
|
|
@@ -450,7 +746,7 @@ const CHUNK_BYTES = 61440;
|
|
|
450
746
|
const MAX_BUFFERED = 8388608;
|
|
451
747
|
const DIGEST_BYTES = 32;
|
|
452
748
|
function offloadMode(env = process.env) {
|
|
453
|
-
const raw = env.
|
|
749
|
+
const raw = env.STIM_REMOTE_BUILD_MODE || loadConfig$1()?.remote?.buildMode;
|
|
454
750
|
return OFFLOAD_MODES.includes(raw) ? raw : "auto";
|
|
455
751
|
}
|
|
456
752
|
function buildPlacementCandidates(selected) {
|
|
@@ -478,30 +774,30 @@ function placementLoad(choice) {
|
|
|
478
774
|
})} there`;
|
|
479
775
|
}
|
|
480
776
|
/**
|
|
481
|
-
* Whether a build may go to a
|
|
482
|
-
* this Mac has a free `concurrency.maxBuilds` slot and its load per core is under `
|
|
777
|
+
* Whether a build may go to a remote Mac at all, before any machine is asked. `auto` keeps the build here while
|
|
778
|
+
* this Mac has a free `concurrency.maxBuilds` slot and its load per core is under `server.maxLoadPerCore`.
|
|
483
779
|
* A named selection ignores local capacity and mode, and refuses an unsupported build or missing pairing.
|
|
484
780
|
*/
|
|
485
781
|
function offloadPlacement({ mode, machines, here, unsupported, selected = "auto" }) {
|
|
486
782
|
if (namedBuildMachine(selected)) {
|
|
487
|
-
if (unsupported) throw new OffloadRefusal(selected, `${unsupported}, so a named
|
|
488
|
-
if (machines === 0) throw new OffloadRefusal(selected, "no
|
|
783
|
+
if (unsupported) throw new OffloadRefusal(selected, `${unsupported}, so a named remote Mac cannot take it`);
|
|
784
|
+
if (machines === 0) throw new OffloadRefusal(selected, "no remote Mac is paired");
|
|
489
785
|
return {
|
|
490
786
|
offload: true,
|
|
491
|
-
reason: `selected with --build
|
|
787
|
+
reason: `selected with --remote-build ${selected}`
|
|
492
788
|
};
|
|
493
789
|
}
|
|
494
790
|
if (selected === "local") return {
|
|
495
791
|
offload: false,
|
|
496
|
-
reason: "--build
|
|
792
|
+
reason: "--remote-build local"
|
|
497
793
|
};
|
|
498
794
|
if (mode === "off") return {
|
|
499
795
|
offload: false,
|
|
500
|
-
reason: "
|
|
796
|
+
reason: "remote.buildMode is off"
|
|
501
797
|
};
|
|
502
798
|
if (machines === 0) return {
|
|
503
799
|
offload: false,
|
|
504
|
-
reason: "no
|
|
800
|
+
reason: "no remote Mac is paired"
|
|
505
801
|
};
|
|
506
802
|
if (unsupported) return {
|
|
507
803
|
offload: false,
|
|
@@ -509,7 +805,7 @@ function offloadPlacement({ mode, machines, here, unsupported, selected = "auto"
|
|
|
509
805
|
};
|
|
510
806
|
if (mode === "force") return {
|
|
511
807
|
offload: true,
|
|
512
|
-
reason: "
|
|
808
|
+
reason: "remote.buildMode is force"
|
|
513
809
|
};
|
|
514
810
|
const busy = saturation(here);
|
|
515
811
|
return busy ? {
|
|
@@ -797,7 +1093,7 @@ var BuildConnection = class BuildConnection {
|
|
|
797
1093
|
function replyError(reply) {
|
|
798
1094
|
return "error" in reply ? `${reply.error.code}: ${reply.error.message}` : null;
|
|
799
1095
|
}
|
|
800
|
-
/** The repository on the
|
|
1096
|
+
/** The repository on the remote Mac: one area per git common dir, shared by all its worktrees. */
|
|
801
1097
|
function repoIdentity(projectRoot) {
|
|
802
1098
|
const run = getExecutor().runFile;
|
|
803
1099
|
const repoRoot = realpathSync(run("git", [
|
|
@@ -937,7 +1233,7 @@ async function chooseBuildMachine({ projectRoot, target, mode, here, note, machi
|
|
|
937
1233
|
asked.forEach((each, at) => {
|
|
938
1234
|
if (!order.includes(at) && "connection" in each) each.connection.close();
|
|
939
1235
|
});
|
|
940
|
-
if (order.length === 0) return reasons.length ? reasons.join("; ") : "no
|
|
1236
|
+
if (order.length === 0) return reasons.length ? reasons.join("; ") : "no remote Mac is paired";
|
|
941
1237
|
for (const reason of reasons) note(reason);
|
|
942
1238
|
const [first, ...rest] = order.map((at) => {
|
|
943
1239
|
const pick = asked[at];
|
|
@@ -1366,261 +1662,4 @@ function offloadCheck(projectRoot, targets) {
|
|
|
1366
1662
|
};
|
|
1367
1663
|
}
|
|
1368
1664
|
//#endregion
|
|
1369
|
-
|
|
1370
|
-
function note(title, detail, fix = null) {
|
|
1371
|
-
return {
|
|
1372
|
-
code: "device-host-machine",
|
|
1373
|
-
level: "note",
|
|
1374
|
-
title,
|
|
1375
|
-
detail,
|
|
1376
|
-
fix
|
|
1377
|
-
};
|
|
1378
|
-
}
|
|
1379
|
-
const approval = (machine, id) => `A person on ${machine} approves it with \`stim-server devices grant ${id} --device-host\`.`;
|
|
1380
|
-
function configuredMachines() {
|
|
1381
|
-
const hosting = loadConfig$1()?.hosting;
|
|
1382
|
-
if (hosting !== void 0 && !isJsonObject(hosting)) return null;
|
|
1383
|
-
const machines = hosting?.machines;
|
|
1384
|
-
if (machines === void 0) return [];
|
|
1385
|
-
return acceptsShape(settingDefinition("hosting.machines"), machines) ? machines : null;
|
|
1386
|
-
}
|
|
1387
|
-
function store(machines) {
|
|
1388
|
-
withDirLock(deviceHostMachinesLock(), () => {
|
|
1389
|
-
const file = deviceHostMachinesFile();
|
|
1390
|
-
const tmp = `${file}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
|
|
1391
|
-
try {
|
|
1392
|
-
writeFileSync(tmp, `${JSON.stringify({
|
|
1393
|
-
version: 1,
|
|
1394
|
-
machines
|
|
1395
|
-
}, null, 2)}\n`, { mode: 384 });
|
|
1396
|
-
renameSync(tmp, file);
|
|
1397
|
-
} finally {
|
|
1398
|
-
rmSync(tmp, { force: true });
|
|
1399
|
-
}
|
|
1400
|
-
}, { ensureParent: () => mkdirSync(getConfigDir(), { recursive: true }) });
|
|
1401
|
-
}
|
|
1402
|
-
async function request(machine, peer, port, deviceName, io) {
|
|
1403
|
-
const ticket = readAccessTicket();
|
|
1404
|
-
const reply = await io.hello(endpoint(peer, port), {
|
|
1405
|
-
request: "device-host",
|
|
1406
|
-
deviceName,
|
|
1407
|
-
...ticket ? { setupTicket: ticket.ticket } : {}
|
|
1408
|
-
});
|
|
1409
|
-
if (!("result" in reply) || reply.result?.approval?.state !== "pending" || typeof reply.result.deviceToken !== "string" || !reply.result.deviceToken || typeof reply.result.device?.id !== "string" || !reply.result.device.id || typeof reply.result.approval.expiresAt !== "string" || !reply.result.approval.expiresAt) return {
|
|
1410
|
-
report: {
|
|
1411
|
-
machine,
|
|
1412
|
-
dnsName: peer.dnsName,
|
|
1413
|
-
state: "unreachable"
|
|
1414
|
-
},
|
|
1415
|
-
finding: note(`Could not ask ${machine} for hosting access`, "The worker did not return a pending request.")
|
|
1416
|
-
};
|
|
1417
|
-
const credential = {
|
|
1418
|
-
machine,
|
|
1419
|
-
nodeId: peer.nodeId,
|
|
1420
|
-
dnsName: peer.dnsName,
|
|
1421
|
-
deviceId: reply.result.device.id,
|
|
1422
|
-
deviceToken: reply.result.deviceToken,
|
|
1423
|
-
state: "pending",
|
|
1424
|
-
requestedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1425
|
-
...ticket ? { ticketHash: ticket.ticketHash } : {}
|
|
1426
|
-
};
|
|
1427
|
-
return {
|
|
1428
|
-
report: {
|
|
1429
|
-
machine,
|
|
1430
|
-
dnsName: peer.dnsName,
|
|
1431
|
-
state: "pending",
|
|
1432
|
-
deviceId: credential.deviceId,
|
|
1433
|
-
requestedAt: credential.requestedAt
|
|
1434
|
-
},
|
|
1435
|
-
finding: note(`Asked ${machine} for hosting access`, `The request lapses at ${reply.result.approval.expiresAt}.`, approval(machine, credential.deviceId)),
|
|
1436
|
-
credential
|
|
1437
|
-
};
|
|
1438
|
-
}
|
|
1439
|
-
/** Reports separately approved hosting machines. Only --fix asks or forgets credentials; every token stays pinned. */
|
|
1440
|
-
async function inspectDeviceHostMachines({ fix }, io = realIo, entries = configuredMachines()) {
|
|
1441
|
-
if (entries === null) return {
|
|
1442
|
-
findings: [note("Invalid hosting.machines setting", "Use a hosting object with machines as an array of tailnet names. Existing hosting credentials and pinned nodes are preserved.", "Run `stim guide settings` and correct the setting before running doctor again.")],
|
|
1443
|
-
machines: []
|
|
1444
|
-
};
|
|
1445
|
-
const unavailable = (state, finding) => ({
|
|
1446
|
-
findings: [finding],
|
|
1447
|
-
machines: entries.map((machine) => ({
|
|
1448
|
-
machine,
|
|
1449
|
-
state
|
|
1450
|
-
}))
|
|
1451
|
-
});
|
|
1452
|
-
const unreadable = () => unavailable("credentials-unavailable", note("Hosting credentials are unreadable", `Stim preserves ${deviceHostMachinesFile()} and its pinned nodes.`, "Restore the existing file from a trusted backup or restore read access before running doctor again."));
|
|
1453
|
-
if (!entries.length) {
|
|
1454
|
-
if (!fix) return {
|
|
1455
|
-
findings: [],
|
|
1456
|
-
machines: []
|
|
1457
|
-
};
|
|
1458
|
-
try {
|
|
1459
|
-
if (!readDeviceHostMachines().length) return {
|
|
1460
|
-
findings: [],
|
|
1461
|
-
machines: []
|
|
1462
|
-
};
|
|
1463
|
-
} catch {
|
|
1464
|
-
return unreadable();
|
|
1465
|
-
}
|
|
1466
|
-
}
|
|
1467
|
-
const claim = tryAcquireClaim({
|
|
1468
|
-
root: deviceHostMachinesClaims(),
|
|
1469
|
-
mode: "exclusive",
|
|
1470
|
-
label: "hosting machine approval"
|
|
1471
|
-
});
|
|
1472
|
-
if (!claim.acquired) {
|
|
1473
|
-
releaseClaim(claim.pending);
|
|
1474
|
-
return unavailable("busy", note("Hosting machine approval is in use", "Another doctor run holds the hosting credential claim. Run doctor again after it finishes."));
|
|
1475
|
-
}
|
|
1476
|
-
try {
|
|
1477
|
-
let credentials;
|
|
1478
|
-
try {
|
|
1479
|
-
credentials = readDeviceHostMachines();
|
|
1480
|
-
} catch {
|
|
1481
|
-
return unreadable();
|
|
1482
|
-
}
|
|
1483
|
-
if (fix) {
|
|
1484
|
-
const kept = credentials.filter((each) => entries.includes(each.machine));
|
|
1485
|
-
if (kept.length !== credentials.length) {
|
|
1486
|
-
store(kept);
|
|
1487
|
-
credentials = kept;
|
|
1488
|
-
}
|
|
1489
|
-
}
|
|
1490
|
-
if (!entries.length) return {
|
|
1491
|
-
findings: [],
|
|
1492
|
-
machines: []
|
|
1493
|
-
};
|
|
1494
|
-
const status = io.status();
|
|
1495
|
-
if (!isJsonObject(status)) return unavailable("tailscale-off", note("Hosting machines are unreachable", "hosting.machines names hosting machines, but Tailscale is not running."));
|
|
1496
|
-
const self = isJsonObject(status.Self) ? status.Self : {};
|
|
1497
|
-
const deviceName = (typeof self.HostName === "string" && self.HostName ? self.HostName : hostname()).replace(/[\p{Cc}\p{Cf}]/gu, "").trim().slice(0, 64) || "Mac";
|
|
1498
|
-
const inspected = {
|
|
1499
|
-
findings: [],
|
|
1500
|
-
machines: []
|
|
1501
|
-
};
|
|
1502
|
-
const namedNodes = /* @__PURE__ */ new Set();
|
|
1503
|
-
for (const machine of new Set(entries)) {
|
|
1504
|
-
const parsed = parseMachine$1(machine);
|
|
1505
|
-
if (!parsed) {
|
|
1506
|
-
inspected.machines.push({
|
|
1507
|
-
machine,
|
|
1508
|
-
state: "invalid"
|
|
1509
|
-
});
|
|
1510
|
-
inspected.findings.push(note(`Hosting machine ${machine} is not a tailnet name`, "Expected `name` or `name:port`."));
|
|
1511
|
-
continue;
|
|
1512
|
-
}
|
|
1513
|
-
const current = io.status();
|
|
1514
|
-
if (!isJsonObject(current)) {
|
|
1515
|
-
inspected.machines.push({
|
|
1516
|
-
machine,
|
|
1517
|
-
state: "tailscale-off"
|
|
1518
|
-
});
|
|
1519
|
-
inspected.findings.push(note(`Hosting machine ${machine} is unreachable`, "Tailscale stopped before this connection."));
|
|
1520
|
-
continue;
|
|
1521
|
-
}
|
|
1522
|
-
const peer = findPeer$1(current, parsed.name);
|
|
1523
|
-
if (peer === "missing" || peer === "ambiguous") {
|
|
1524
|
-
inspected.machines.push({
|
|
1525
|
-
machine,
|
|
1526
|
-
state: "not-on-tailnet"
|
|
1527
|
-
});
|
|
1528
|
-
inspected.findings.push(note(`Hosting machine ${machine} is not on this tailnet`, peer === "missing" ? "No peer has that name." : "Several peers match that name."));
|
|
1529
|
-
continue;
|
|
1530
|
-
}
|
|
1531
|
-
const nodeRoute = `${peer.nodeId}:${parsed.port}`;
|
|
1532
|
-
if (namedNodes.has(nodeRoute)) {
|
|
1533
|
-
inspected.machines.push({
|
|
1534
|
-
machine,
|
|
1535
|
-
dnsName: peer.dnsName,
|
|
1536
|
-
state: "invalid"
|
|
1537
|
-
});
|
|
1538
|
-
inspected.findings.push(note(`Hosting machine ${machine} repeats a named node`, "Name each tailnet node and serve port only once in hosting.machines."));
|
|
1539
|
-
continue;
|
|
1540
|
-
}
|
|
1541
|
-
namedNodes.add(nodeRoute);
|
|
1542
|
-
const credential = credentials.find((each) => each.machine === machine);
|
|
1543
|
-
const known = {
|
|
1544
|
-
machine,
|
|
1545
|
-
dnsName: peer.dnsName,
|
|
1546
|
-
...credential ? {
|
|
1547
|
-
deviceId: credential.deviceId,
|
|
1548
|
-
requestedAt: credential.requestedAt
|
|
1549
|
-
} : {}
|
|
1550
|
-
};
|
|
1551
|
-
if (credential && credential.nodeId !== peer.nodeId) {
|
|
1552
|
-
inspected.machines.push({
|
|
1553
|
-
...known,
|
|
1554
|
-
state: "node-changed"
|
|
1555
|
-
});
|
|
1556
|
-
inspected.findings.push(note(`Hosting machine ${machine} is a different tailnet node`, "Stim refuses to connect or send the saved token.", `If that Mac was replaced, remove ${machine} from hosting.machines, run \`stim doctor --fix\`, then add it back and run \`stim doctor --fix\` again to request approval on the new node.`));
|
|
1557
|
-
continue;
|
|
1558
|
-
}
|
|
1559
|
-
let ask = !credential && fix;
|
|
1560
|
-
if (credential) {
|
|
1561
|
-
const reply = await io.hello(endpoint(peer, parsed.port), { deviceToken: credential.deviceToken });
|
|
1562
|
-
if ("result" in reply && reply.result?.device?.id === credential.deviceId && Array.isArray(reply.result.capabilities) && reply.result.capabilities.includes("device-host")) {
|
|
1563
|
-
if (credential.state !== "approved") {
|
|
1564
|
-
credentials = credentials.map((each) => each.machine === machine ? {
|
|
1565
|
-
...each,
|
|
1566
|
-
state: "approved"
|
|
1567
|
-
} : each);
|
|
1568
|
-
store(credentials);
|
|
1569
|
-
}
|
|
1570
|
-
const host = readHostPermissions(reply.result.host);
|
|
1571
|
-
if (host && (host.screenRecording === false || host.accessibility === false)) {
|
|
1572
|
-
const panes = [...host.screenRecording === false ? ["Screen & System Audio Recording (Screen Recording on macOS 14)"] : [], ...host.accessibility === false ? ["Device Control and Data Access (Accessibility on macOS 26 and earlier)"] : []];
|
|
1573
|
-
const detail = [...host.screenRecording === false ? ["Viewing hosted macOS apps needs Screen Recording."] : [], ...host.accessibility === false ? ["Controlling them needs the control permission."] : []];
|
|
1574
|
-
inspected.findings.push(note(`Hosting machine ${machine} needs ${panes.join(" and ")} for ${host.name}`, detail.join(" "), `On ${machine}, approve ${host.name} in System Settings > Privacy & Security, or run \`stim-server service install\` there again to show the requests.`));
|
|
1575
|
-
}
|
|
1576
|
-
inspected.machines.push({
|
|
1577
|
-
...known,
|
|
1578
|
-
state: "approved",
|
|
1579
|
-
...host ? { host } : {}
|
|
1580
|
-
});
|
|
1581
|
-
continue;
|
|
1582
|
-
}
|
|
1583
|
-
const pending = "error" in reply && reply.error?.code === "approval-pending";
|
|
1584
|
-
const revoked = "error" in reply && reply.error?.code === "unauthorized";
|
|
1585
|
-
const ticket = fix && pending && credential.state !== "approved" ? readAccessTicket() : void 0;
|
|
1586
|
-
ask = fix && (revoked || !!ticket && credential.ticketHash !== ticket.ticketHash);
|
|
1587
|
-
if (pending && !ask) {
|
|
1588
|
-
inspected.machines.push({
|
|
1589
|
-
...known,
|
|
1590
|
-
state: "pending"
|
|
1591
|
-
});
|
|
1592
|
-
inspected.findings.push(note(`Hosting machine ${machine} has not approved this Mac yet`, `Requested at ${credential.requestedAt}.`, approval(machine, credential.deviceId)));
|
|
1593
|
-
continue;
|
|
1594
|
-
}
|
|
1595
|
-
if (!ask) {
|
|
1596
|
-
inspected.machines.push({
|
|
1597
|
-
...known,
|
|
1598
|
-
state: revoked ? "revoked" : "unreachable"
|
|
1599
|
-
});
|
|
1600
|
-
inspected.findings.push(note(`Hosting machine ${machine} ${revoked ? "no longer accepts this Mac" : "did not confirm hosting access"}`, "The saved token and pinned node are preserved.", revoked ? "Run `stim doctor --fix` to ask again." : `Check stim-server and its tailnet serve route on ${machine}, then run doctor again.`));
|
|
1601
|
-
continue;
|
|
1602
|
-
}
|
|
1603
|
-
}
|
|
1604
|
-
if (ask) {
|
|
1605
|
-
const result = await request(machine, peer, parsed.port, deviceName, io);
|
|
1606
|
-
if (result.credential) {
|
|
1607
|
-
credentials = [...credentials.filter((each) => each.machine !== machine), result.credential];
|
|
1608
|
-
store(credentials);
|
|
1609
|
-
}
|
|
1610
|
-
inspected.machines.push(result.report);
|
|
1611
|
-
inspected.findings.push(result.finding);
|
|
1612
|
-
} else {
|
|
1613
|
-
inspected.machines.push({
|
|
1614
|
-
...known,
|
|
1615
|
-
state: "not-asked"
|
|
1616
|
-
});
|
|
1617
|
-
inspected.findings.push(note(`Hosting machine ${machine} has not approved this Mac`, "This Mac has not asked for hosting access.", "Run `stim doctor --fix` to ask."));
|
|
1618
|
-
}
|
|
1619
|
-
}
|
|
1620
|
-
return inspected;
|
|
1621
|
-
} finally {
|
|
1622
|
-
releaseClaim(claim.acquired);
|
|
1623
|
-
}
|
|
1624
|
-
}
|
|
1625
|
-
//#endregion
|
|
1626
|
-
export { resolveBuildPlacement as _, chooseBuildMachine as a, pinnedEndpoint$1 as b, offloadCheck as c, remotePhaseText as d, simulatorRuntime as f, resolveBuildMachine as g, parseBuildMachineOption as h, buildPlacementCandidates as i, offloadPlacement as l, namedBuildMachine as m, inspectDeviceHostMachines as n, closeOffload as o, OffloadRefusal as p, BuildConnection as r, offloadBuild as s, configuredMachines as t, placementLoad as u, inspectBuildMachines as v, realIo as x, parseMachine$1 as y };
|
|
1665
|
+
export { realIo as S, configuredMachines as _, offloadBuild as a, parseMachine$1 as b, placementLoad as c, OffloadRefusal as d, namedBuildMachine as f, inspectBuildMachines as g, resolveBuildPlacement as h, closeOffload as i, remotePhaseText as l, resolveBuildMachine as m, buildPlacementCandidates as n, offloadCheck as o, parseBuildMachineOption as p, chooseBuildMachine as r, offloadPlacement as s, BuildConnection as t, simulatorRuntime as u, hostingMachines as v, pinnedEndpoint$1 as x, inspectDeviceHostMachines as y };
|