@dev.fast/whiteboard 0.2.5-preview.20261013.113 → 0.2.5-preview.20261015.115
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/THIRD_PARTY_NOTICES.md +29 -0
- package/dist/{account-alias-D0hF0sKk.js → account-alias-ChZR3RIU.js} +94 -8
- package/dist/{agent-cli-04xbDj23.js → agent-cli-CR2PnA9G.js} +9 -12
- package/dist/agent-cli-CRPPmS3l.js +2 -0
- package/dist/{agent-client-P3J4y8Zn.js → agent-client-DVd-prfp.js} +31 -14
- package/dist/{authoring-tools-fdA0RPIT.js → authoring-tools-BXKEwhfi.js} +5 -5
- package/dist/background-server-Cw7CcfaZ.js +156 -0
- package/dist/background-server-D2Rrr_e2.js +2 -0
- package/dist/build-info.json +1 -1
- package/dist/{cli-C0ziP6hg.js → cli-CSrz_wGk.js} +1 -1
- package/dist/{cli-runner-C3men22A.js → cli-runner-PlGqBeMw.js} +368 -252
- package/dist/cli.js +3 -2
- package/dist/{client-DzcSt6pV.js → client-DhZzPePs.js} +5 -3
- package/dist/{process-error-telemetry-BCBYp2t4.js → headless-host-CVXTSIlx.js} +3285 -2585
- package/dist/headless-host-Cp37C-v0.js +2 -0
- package/dist/input-error-OLgB_h31.js +11 -0
- package/dist/{local-data-Dy1pNtLF.js → local-data-yfLp5PiJ.js} +113 -62
- package/dist/{mcp-B8kuRSXM.js → mcp-DBbDwG4y.js} +4 -4
- package/dist/{package-paths-B9PDoAl6.js → package-paths-BsuI4WUf.js} +1 -1
- package/dist/profile-BSOkA6A-.js +2 -0
- package/dist/{profile-tbGWuWHu.js → profile-pAzg7fxC.js} +3 -3
- package/dist/remote-attach-BAt2eZx3.js +137 -0
- package/dist/remote-extensions-2Cxf3hMo.js +2 -0
- package/dist/remote-extensions-BiNx6NQK.js +323 -0
- package/dist/remote-language-server-DQgWUNeV.js +252 -0
- package/dist/remote-uninstall-DLtDG0o7.js +153 -0
- package/dist/{request-origin-DA4K1qWY.js → request-origin-DaRIV3cB.js} +5 -2
- package/dist/review-app-launcher-D7I4DGoH.js +222 -0
- package/dist/{review-home-paths-Y9mYWkCI.js → review-home-paths-zwevIq4I.js} +7 -7
- package/dist/{review-telemetry-CmM6tCbj.js → review-telemetry-BqB4CiTD.js} +5 -5
- package/dist/{s3-SzLpeCG6.js → s3-77-qkEhs.js} +1 -1
- package/dist/{s3-config-BgxkSoOy.js → s3-config-0xuCh-F_.js} +1 -1
- package/dist/{s3-config-CAxhO9u_.js → s3-config-DDWWMh5x.js} +5 -1
- package/dist/server/desktop-host.js +1663 -266
- package/dist/{desktop-discovery-Ba3dD5Cn.js → server-discovery-CifQ3yQS.js} +88 -3
- package/dist/sharing/index.d.ts +88 -32
- package/dist/sharing/index.js +1 -1
- package/dist/{src-DiHP11l3.js → src-BMPuxWv4.js} +6 -11
- package/dist/{src-WCbabvdS.js → src-Bgvf1sEN.js} +128 -8
- package/dist/{stored-document-migration-u4L_1VZa.js → stored-document-migration-CFOvjRwW.js} +5 -18
- package/dist/{tool-failure-CJHBCHMh.js → tool-failure-BXWzQG8T.js} +2 -2
- package/instructions/authoring.md +16 -12
- package/instructions/file-lenses.md +4 -2
- package/package.json +9 -14
- package/src/agent-connect.ts +507 -0
- package/src/agent-selection.ts +4 -0
- package/src/ask/agents.ts +59 -13
- package/src/ask/pi-mcp.ts +7 -3
- package/src/ask/thread-state.ts +9 -4
- package/src/ask/thread.ts +88 -50
- package/src/ask/threads.ts +53 -7
- package/src/cli-install.ts +3 -1
- package/src/cli-runner.ts +472 -81
- package/src/cli.ts +1 -1
- package/src/connect-agents.ts +285 -0
- package/src/connect-prompts.ts +60 -6
- package/src/desktop-discovery.ts +24 -0
- package/src/pinned-download.ts +118 -0
- package/src/remote-attach.ts +130 -0
- package/src/remote-extensions.ts +359 -0
- package/src/remote-language-server.ts +428 -0
- package/src/remote-toolchains.ts +107 -0
- package/src/remote-uninstall.ts +280 -0
- package/src/review-api/README.md +1 -3
- package/src/review-api/agent-cli.ts +4 -4
- package/src/review-api/agent-client.ts +23 -0
- package/src/review-api/ask-history.ts +31 -16
- package/src/review-api/authoring-tools.ts +1 -1
- package/src/review-api/blocks/index.ts +2 -4
- package/src/review-api/document.ts +1 -4
- package/src/review-api/http.ts +265 -237
- package/src/review-api/local-data.ts +18 -3
- package/src/review-api/onboarding.ts +478 -0
- package/src/review-api/origin.ts +7 -0
- package/src/review-api/recent-folders.ts +173 -0
- package/src/review-api/store.ts +56 -10
- package/src/review-api/workspaces.ts +56 -11
- package/src/review-app-launcher.ts +61 -3
- package/src/review-stack.ts +162 -36
- package/src/server/background-server.ts +250 -0
- package/src/server/desktop-host-shutdown.ts +24 -0
- package/src/server/desktop-host.ts +39 -24
- package/src/server/desktop-server.ts +68 -118
- package/src/server/global-verb-relay.ts +16 -0
- package/src/server/headless-host.ts +128 -3
- package/src/server/hono-http.ts +6 -1
- package/src/server/legacy-tutorial-cleanup.ts +38 -0
- package/src/server/review-gateway-hosts.ts +379 -0
- package/src/server/review-gateway-list.ts +109 -0
- package/src/server/review-gateway-memory.ts +130 -0
- package/src/server/review-gateway-pushes.ts +142 -0
- package/src/server/review-gateway-streams.ts +415 -0
- package/src/server/review-gateway-transport.ts +204 -0
- package/src/server/review-gateway.ts +433 -0
- package/src/server/review-server-core.ts +36 -6
- package/src/server-discovery.ts +11 -0
- package/src/sharing/import.ts +1 -0
- package/src/ui-telemetry-events.ts +5 -1
- package/src/vsix.ts +122 -0
- package/dist/agent-cli-BR_Jw5BL.js +0 -2
- package/dist/headless-host-BLLJ7Dyu.js +0 -95
- package/dist/profile-aMSw1oCM.js +0 -2
- package/dist/server-discovery-ZVi8Ay8J.js +0 -66
- package/src/fixtures/blocks/tutorial.json +0 -14
- package/src/review-api/blocks/tutorial.ts +0 -23
- package/src/server/tutorial-service.ts +0 -217
- package/tutorial/document.json +0 -216
- package/tutorial/lenses.json +0 -10
- package/tutorial/runtime-manifest.json +0 -11
- package/tutorial/sample-service/package.json +0 -9
- package/tutorial/sample-service/src/api/checkout-api.ts +0 -17
- package/tutorial/sample-service/src/app.ts +0 -26
- package/tutorial/sample-service/src/database/drizzle-orm.d.ts +0 -16
- package/tutorial/sample-service/src/database/schema.ts +0 -9
- package/tutorial/sample-service/src/fulfillment/fulfillment-queue.ts +0 -15
- package/tutorial/sample-service/src/fulfillment/fulfillment-worker.ts +0 -24
- package/tutorial/sample-service/src/inventory/forklift-mood.ts +0 -4
- package/tutorial/sample-service/src/inventory/inventory-service.ts +0 -28
- package/tutorial/sample-service/src/inventory/warehouse-lore.ts +0 -11
- package/tutorial/sample-service/src/orders/order-service.ts +0 -35
- package/tutorial/sample-service/src/orders/order.ts +0 -43
- package/tutorial/sample-service/src/orders/orders-repository.ts +0 -24
- package/tutorial/sample-service/src/payments/fraud-vibes.ts +0 -4
- package/tutorial/sample-service/src/payments/payment-gateway.ts +0 -29
- package/tutorial/sample-service/src/payments/raccoon-ledger.ts +0 -9
- package/tutorial/sample-service/src/shipping/shipping-gateway.ts +0 -15
- package/tutorial/sample-service/tsconfig.json +0 -12
|
@@ -0,0 +1,428 @@
|
|
|
1
|
+
import { randomBytes } from "node:crypto";
|
|
2
|
+
import { mkdir, readFile, rename, stat, writeFile } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { setTimeout as delay } from "node:timers/promises";
|
|
5
|
+
|
|
6
|
+
import { processStartIdentity, withFileLock } from "@dev.fast/trace-core";
|
|
7
|
+
import { z } from "zod";
|
|
8
|
+
|
|
9
|
+
import { findReviewPackageRoot } from "./package-paths";
|
|
10
|
+
import { ensureRemoteExtensions, remoteServerPaths } from "./remote-extensions";
|
|
11
|
+
import { cliSpawn, spawnDetached } from "./server/background-server";
|
|
12
|
+
|
|
13
|
+
const START_TIMEOUT_MS = 15_000;
|
|
14
|
+
|
|
15
|
+
const VERSION_TIMEOUT_MS = 3_000;
|
|
16
|
+
|
|
17
|
+
const INSTALL_TIMEOUT_MS = 35_000;
|
|
18
|
+
|
|
19
|
+
const SHUTDOWN_WITHOUT_DELAY_ENV =
|
|
20
|
+
"DEV_FAST_REVIEW_REMOTE_SHUTDOWN_WITHOUT_DELAY";
|
|
21
|
+
|
|
22
|
+
const GRACE_ENV = "DEV_FAST_REVIEW_REMOTE_RECONNECTION_GRACE_SECONDS";
|
|
23
|
+
|
|
24
|
+
const PENDING = {
|
|
25
|
+
languageServer: null,
|
|
26
|
+
languageServerDetail:
|
|
27
|
+
"Installing the language extensions on this host; they will be available on the next connection.",
|
|
28
|
+
languageServerPending: true,
|
|
29
|
+
} as const;
|
|
30
|
+
|
|
31
|
+
function idleArgs(env: NodeJS.ProcessEnv) {
|
|
32
|
+
const grace = env[GRACE_ENV] ?? "";
|
|
33
|
+
|
|
34
|
+
return [
|
|
35
|
+
"--enable-remote-auto-shutdown",
|
|
36
|
+
"--reconnection-grace-time",
|
|
37
|
+
/^[1-9]\d{0,3}$/.test(grace) ? grace : "600",
|
|
38
|
+
];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const LISTENING = /Extension host agent listening on (\d+)/;
|
|
42
|
+
|
|
43
|
+
const LOCK = {
|
|
44
|
+
retryMs: 100,
|
|
45
|
+
staleMs: 60_000,
|
|
46
|
+
unownedGraceMs: 5_000,
|
|
47
|
+
identifyOwner: true,
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const installingSchema = z.object({
|
|
51
|
+
pid: z.number(),
|
|
52
|
+
started: z.string().nullable(),
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
const runningSchema = z.object({
|
|
56
|
+
pid: z.number(),
|
|
57
|
+
started: z.string().nullable(),
|
|
58
|
+
port: z.number(),
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
type Running = z.infer<typeof runningSchema>;
|
|
62
|
+
|
|
63
|
+
export interface RemoteLanguageGroup {
|
|
64
|
+
group: string;
|
|
65
|
+
installed: boolean;
|
|
66
|
+
detail?: string;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export interface RemoteLanguageServer {
|
|
70
|
+
port: number;
|
|
71
|
+
connectionToken: string;
|
|
72
|
+
commit: string;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
interface EnsureExtensions {
|
|
76
|
+
(input: {
|
|
77
|
+
env: NodeJS.ProcessEnv;
|
|
78
|
+
groups?: string[];
|
|
79
|
+
signal?: AbortSignal;
|
|
80
|
+
}): Promise<{
|
|
81
|
+
failed: { id: string; error: string }[];
|
|
82
|
+
groups?: RemoteLanguageGroup[];
|
|
83
|
+
}>;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export interface EnsureRemoteLanguageServerInput {
|
|
87
|
+
env: NodeJS.ProcessEnv;
|
|
88
|
+
packageRoot?: string;
|
|
89
|
+
groups?: string[];
|
|
90
|
+
signal?: AbortSignal;
|
|
91
|
+
ensure?: EnsureExtensions;
|
|
92
|
+
timeoutMs?: number;
|
|
93
|
+
installTimeoutMs?: number;
|
|
94
|
+
cli?: readonly string[];
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function remoteLanguageServerFiles(env: NodeJS.ProcessEnv) {
|
|
98
|
+
const { serverDataDir } = remoteServerPaths(env);
|
|
99
|
+
|
|
100
|
+
return {
|
|
101
|
+
serverDataDir,
|
|
102
|
+
tokenFile: path.join(serverDataDir, "connection-token"),
|
|
103
|
+
runningFile: path.join(serverDataDir, "server.json"),
|
|
104
|
+
logFile: path.join(serverDataDir, "server.log"),
|
|
105
|
+
lock: path.join(serverDataDir, "start.lock"),
|
|
106
|
+
installingFile: path.join(serverDataDir, "install.json"),
|
|
107
|
+
installLog: path.join(serverDataDir, "install.log"),
|
|
108
|
+
installLock: path.join(serverDataDir, "install.lock"),
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export async function ensureRemoteLanguageServer(
|
|
113
|
+
input: EnsureRemoteLanguageServerInput,
|
|
114
|
+
): Promise<{
|
|
115
|
+
languageServer: RemoteLanguageServer | null;
|
|
116
|
+
languageServerDetail?: string;
|
|
117
|
+
languageServerPending?: true;
|
|
118
|
+
languageGroups: RemoteLanguageGroup[];
|
|
119
|
+
}> {
|
|
120
|
+
const root = path.join(
|
|
121
|
+
input.packageRoot ?? findReviewPackageRoot(import.meta.url),
|
|
122
|
+
"vscode-server",
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
const files = remoteLanguageServerFiles(input.env);
|
|
126
|
+
const capped = new AbortController();
|
|
127
|
+
|
|
128
|
+
let languageGroups: RemoteLanguageGroup[] = (input.groups ?? []).map(
|
|
129
|
+
(group) => ({ group, installed: false }),
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
const cap = setTimeout(
|
|
133
|
+
() => capped.abort(),
|
|
134
|
+
input.installTimeoutMs ?? INSTALL_TIMEOUT_MS,
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
try {
|
|
138
|
+
const commit = await readCommit(root);
|
|
139
|
+
const ensure = input.ensure ?? ensureRemoteExtensions;
|
|
140
|
+
await mkdir(files.serverDataDir, { recursive: true, mode: 0o700 });
|
|
141
|
+
|
|
142
|
+
if (await installing(files)) return { ...PENDING, languageGroups };
|
|
143
|
+
|
|
144
|
+
const { failed, groups } = await ensure({
|
|
145
|
+
env: input.env,
|
|
146
|
+
groups: input.groups,
|
|
147
|
+
signal: AbortSignal.any([
|
|
148
|
+
capped.signal,
|
|
149
|
+
...(input.signal ? [input.signal] : []),
|
|
150
|
+
]),
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
if (
|
|
154
|
+
failed.length > 0 &&
|
|
155
|
+
capped.signal.aborted &&
|
|
156
|
+
!input.signal?.aborted &&
|
|
157
|
+
(await installDetached(files, input))
|
|
158
|
+
)
|
|
159
|
+
return { ...PENDING, languageGroups };
|
|
160
|
+
|
|
161
|
+
if (groups) languageGroups = groups;
|
|
162
|
+
|
|
163
|
+
if (failed.length > 0)
|
|
164
|
+
throw new Error(
|
|
165
|
+
`Could not install the language extensions: ${failed.map(({ id, error }) => `${id}: ${error}`).join("; ")}`,
|
|
166
|
+
);
|
|
167
|
+
|
|
168
|
+
const outcome = await withFileLock(
|
|
169
|
+
files.lock,
|
|
170
|
+
{ ...LOCK, timeoutMs: (input.timeoutMs ?? START_TIMEOUT_MS) * 2 },
|
|
171
|
+
async () => {
|
|
172
|
+
const running = await healthy(files.runningFile, commit);
|
|
173
|
+
|
|
174
|
+
const port =
|
|
175
|
+
running?.port ??
|
|
176
|
+
(await startServer(root, files, input.env, input.timeoutMs));
|
|
177
|
+
|
|
178
|
+
return {
|
|
179
|
+
port,
|
|
180
|
+
connectionToken: (await readFile(files.tokenFile, "utf8")).trim(),
|
|
181
|
+
commit,
|
|
182
|
+
};
|
|
183
|
+
},
|
|
184
|
+
);
|
|
185
|
+
|
|
186
|
+
if (!outcome.acquired)
|
|
187
|
+
throw new Error("Another start of the VS Code server did not finish.");
|
|
188
|
+
|
|
189
|
+
return { languageServer: outcome.result, languageGroups };
|
|
190
|
+
} catch (error) {
|
|
191
|
+
return {
|
|
192
|
+
languageServer: null,
|
|
193
|
+
languageServerDetail:
|
|
194
|
+
error instanceof Error ? error.message : String(error),
|
|
195
|
+
languageGroups,
|
|
196
|
+
};
|
|
197
|
+
} finally {
|
|
198
|
+
clearTimeout(cap);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
async function readInstalling(
|
|
203
|
+
files: ReturnType<typeof remoteLanguageServerFiles>,
|
|
204
|
+
) {
|
|
205
|
+
return readFile(files.installingFile, "utf8")
|
|
206
|
+
.then((text) => installingSchema.safeParse(JSON.parse(text)).data)
|
|
207
|
+
.catch(() => undefined);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
async function installing(files: ReturnType<typeof remoteLanguageServerFiles>) {
|
|
211
|
+
const running = await readInstalling(files);
|
|
212
|
+
|
|
213
|
+
return running !== undefined && ours(running);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export async function remoteLanguageServerGroups(env: NodeJS.ProcessEnv) {
|
|
217
|
+
const files = remoteLanguageServerFiles(env);
|
|
218
|
+
|
|
219
|
+
const recorded = await Promise.all([
|
|
220
|
+
readRunning(files.runningFile),
|
|
221
|
+
readInstalling(files),
|
|
222
|
+
]);
|
|
223
|
+
|
|
224
|
+
return recorded.flatMap((entry) => (entry && ours(entry) ? [entry.pid] : []));
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
export async function stopProcessGroup(leader: number) {
|
|
228
|
+
for (const signal of ["SIGTERM", "SIGKILL"] as const) {
|
|
229
|
+
try {
|
|
230
|
+
process.kill(-leader, signal);
|
|
231
|
+
} catch {
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
for (let i = 0; i < 50; i++) {
|
|
236
|
+
if (!groupAlive(leader)) return;
|
|
237
|
+
await delay(100);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function groupAlive(leader: number) {
|
|
243
|
+
try {
|
|
244
|
+
process.kill(-leader, 0);
|
|
245
|
+
|
|
246
|
+
return true;
|
|
247
|
+
} catch (error) {
|
|
248
|
+
return error instanceof Error && "code" in error && error.code === "EPERM";
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
async function installDetached(
|
|
253
|
+
files: ReturnType<typeof remoteLanguageServerFiles>,
|
|
254
|
+
input: EnsureRemoteLanguageServerInput,
|
|
255
|
+
) {
|
|
256
|
+
const outcome = await withFileLock(
|
|
257
|
+
files.installLock,
|
|
258
|
+
{ ...LOCK, timeoutMs: 10_000 },
|
|
259
|
+
async () => {
|
|
260
|
+
if (await installing(files)) return;
|
|
261
|
+
const { command, args, env } = cliSpawn(input.cli, input.env);
|
|
262
|
+
|
|
263
|
+
const child = await spawnDetached({
|
|
264
|
+
command,
|
|
265
|
+
args: [
|
|
266
|
+
...args,
|
|
267
|
+
"remote",
|
|
268
|
+
"extensions",
|
|
269
|
+
"ensure",
|
|
270
|
+
"--json",
|
|
271
|
+
...(input.groups?.length ? ["--groups", input.groups.join(",")] : []),
|
|
272
|
+
],
|
|
273
|
+
cwd: files.serverDataDir,
|
|
274
|
+
env,
|
|
275
|
+
log: files.installLog,
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
if (child.pid !== undefined)
|
|
279
|
+
await writeFile(
|
|
280
|
+
files.installingFile,
|
|
281
|
+
`${JSON.stringify({ pid: child.pid, started: processStartIdentity(child.pid) })}\n`,
|
|
282
|
+
{ mode: 0o600 },
|
|
283
|
+
);
|
|
284
|
+
},
|
|
285
|
+
);
|
|
286
|
+
|
|
287
|
+
return outcome.acquired;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
async function readCommit(root: string) {
|
|
291
|
+
const file = path.join(root, "product.json");
|
|
292
|
+
|
|
293
|
+
const text = await readFile(file, "utf8").catch(() => {
|
|
294
|
+
throw new Error(
|
|
295
|
+
`This Whiteboard install has no VS Code server (${file} is missing).`,
|
|
296
|
+
);
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
const { commit } = z
|
|
300
|
+
.object({ commit: z.string().regex(/^[0-9a-f]{40}$/) })
|
|
301
|
+
.parse(JSON.parse(text));
|
|
302
|
+
|
|
303
|
+
return commit;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
async function readRunning(file: string) {
|
|
307
|
+
return readFile(file, "utf8")
|
|
308
|
+
.then((text) => runningSchema.safeParse(JSON.parse(text)).data)
|
|
309
|
+
.catch(() => undefined);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
const ours = (running: { pid: number; started: string | null }) =>
|
|
313
|
+
running.started !== null &&
|
|
314
|
+
processStartIdentity(running.pid) === running.started;
|
|
315
|
+
|
|
316
|
+
async function healthy(file: string, commit: string) {
|
|
317
|
+
const running = await readRunning(file);
|
|
318
|
+
|
|
319
|
+
if (!running) return undefined;
|
|
320
|
+
|
|
321
|
+
if (ours(running) && (await version(running.port)) === commit) return running;
|
|
322
|
+
|
|
323
|
+
await stop(running);
|
|
324
|
+
|
|
325
|
+
return undefined;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
async function version(port: number) {
|
|
329
|
+
return fetch(`http://127.0.0.1:${port}/version`, {
|
|
330
|
+
signal: AbortSignal.timeout(VERSION_TIMEOUT_MS),
|
|
331
|
+
})
|
|
332
|
+
.then((response) => (response.ok ? response.text() : undefined))
|
|
333
|
+
.catch(() => undefined);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
async function stop(running: Running) {
|
|
337
|
+
if (!ours(running)) return;
|
|
338
|
+
|
|
339
|
+
try {
|
|
340
|
+
process.kill(running.pid, "SIGTERM");
|
|
341
|
+
} catch {
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
for (let i = 0; i < 50 && processStartIdentity(running.pid) !== null; i++)
|
|
346
|
+
await delay(100);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
async function startServer(
|
|
350
|
+
root: string,
|
|
351
|
+
files: ReturnType<typeof remoteLanguageServerFiles>,
|
|
352
|
+
env: NodeJS.ProcessEnv,
|
|
353
|
+
timeoutMs = START_TIMEOUT_MS,
|
|
354
|
+
) {
|
|
355
|
+
const { extensionsDir, serverDataDir } = remoteServerPaths(env);
|
|
356
|
+
|
|
357
|
+
const temporary = `${files.tokenFile}.${process.pid}.tmp`;
|
|
358
|
+
await writeFile(temporary, randomBytes(32).toString("hex"), { mode: 0o600 });
|
|
359
|
+
await rename(temporary, files.tokenFile);
|
|
360
|
+
|
|
361
|
+
const logStart = (await stat(files.logFile).catch(() => null))?.size ?? 0;
|
|
362
|
+
|
|
363
|
+
const child = await spawnDetached({
|
|
364
|
+
command: process.execPath,
|
|
365
|
+
args: [
|
|
366
|
+
path.join(root, "out", "server-main.js"),
|
|
367
|
+
"--host",
|
|
368
|
+
"127.0.0.1",
|
|
369
|
+
"--port",
|
|
370
|
+
"0",
|
|
371
|
+
"--connection-token-file",
|
|
372
|
+
files.tokenFile,
|
|
373
|
+
"--server-data-dir",
|
|
374
|
+
serverDataDir,
|
|
375
|
+
"--extensions-dir",
|
|
376
|
+
extensionsDir,
|
|
377
|
+
"--accept-server-license-terms",
|
|
378
|
+
...idleArgs(env),
|
|
379
|
+
...(env[SHUTDOWN_WITHOUT_DELAY_ENV] === "1"
|
|
380
|
+
? ["--remote-auto-shutdown-without-delay"]
|
|
381
|
+
: []),
|
|
382
|
+
],
|
|
383
|
+
cwd: serverDataDir,
|
|
384
|
+
env,
|
|
385
|
+
log: files.logFile,
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
const deadline = Date.now() + timeoutMs;
|
|
389
|
+
|
|
390
|
+
while (Date.now() < deadline && !child.exited) {
|
|
391
|
+
const output = (await readFile(files.logFile))
|
|
392
|
+
.subarray(logStart)
|
|
393
|
+
.toString("utf8");
|
|
394
|
+
|
|
395
|
+
const port = Number(LISTENING.exec(output)?.[1]);
|
|
396
|
+
|
|
397
|
+
if (port && child.pid !== undefined) {
|
|
398
|
+
const running: Running = {
|
|
399
|
+
pid: child.pid,
|
|
400
|
+
started: processStartIdentity(child.pid),
|
|
401
|
+
port,
|
|
402
|
+
};
|
|
403
|
+
|
|
404
|
+
await writeFile(files.runningFile, `${JSON.stringify(running)}\n`, {
|
|
405
|
+
mode: 0o600,
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
return port;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
await delay(100);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
if (!child.exited && child.pid !== undefined)
|
|
415
|
+
process.kill(child.pid, "SIGTERM");
|
|
416
|
+
|
|
417
|
+
const tail = (await readFile(files.logFile))
|
|
418
|
+
.subarray(logStart)
|
|
419
|
+
.toString("utf8")
|
|
420
|
+
.trimEnd()
|
|
421
|
+
.split("\n")
|
|
422
|
+
.slice(-10)
|
|
423
|
+
.join("\n");
|
|
424
|
+
|
|
425
|
+
throw new Error(
|
|
426
|
+
`The VS Code server did not start${child.error ? `: ${child.error.message}` : child.exited ? "" : ` within ${timeoutMs / 1_000} s`}. The end of ${files.logFile}:\n${tail}`,
|
|
427
|
+
);
|
|
428
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
const PROBE_TIMEOUT_MS = 10_000;
|
|
5
|
+
|
|
6
|
+
type Toolchains = Record<string, readonly (readonly string[])[]>;
|
|
7
|
+
|
|
8
|
+
const TOOLCHAINS = {
|
|
9
|
+
rust: [
|
|
10
|
+
["cargo", "--version"],
|
|
11
|
+
["rustc", "--version"],
|
|
12
|
+
],
|
|
13
|
+
swift: [["swift", "--version"]],
|
|
14
|
+
csharp: [["dotnet", "--version"]],
|
|
15
|
+
go: [["go", "version"]],
|
|
16
|
+
} satisfies Toolchains;
|
|
17
|
+
|
|
18
|
+
type Outcome = "found" | "missing" | string;
|
|
19
|
+
|
|
20
|
+
function probe(
|
|
21
|
+
command: readonly string[],
|
|
22
|
+
env: NodeJS.ProcessEnv,
|
|
23
|
+
timeoutMs: number,
|
|
24
|
+
) {
|
|
25
|
+
const shell = env.SHELL || "/bin/sh";
|
|
26
|
+
|
|
27
|
+
const flags = ["csh", "tcsh"].includes(path.basename(shell))
|
|
28
|
+
? ["-ic"]
|
|
29
|
+
: ["-i", "-l", "-c"];
|
|
30
|
+
|
|
31
|
+
return new Promise<Outcome>((resolve) => {
|
|
32
|
+
const child = spawn(shell, [...flags, command.join(" ")], {
|
|
33
|
+
env,
|
|
34
|
+
detached: true,
|
|
35
|
+
stdio: "ignore",
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
const timer = setTimeout(() => {
|
|
39
|
+
child.removeAllListeners("exit");
|
|
40
|
+
|
|
41
|
+
try {
|
|
42
|
+
process.kill(-child.pid!, "SIGKILL");
|
|
43
|
+
} catch {}
|
|
44
|
+
|
|
45
|
+
resolve(
|
|
46
|
+
`${command.join(" ")} did not answer within ${timeoutMs / 1_000} s`,
|
|
47
|
+
);
|
|
48
|
+
}, timeoutMs);
|
|
49
|
+
|
|
50
|
+
child.once("error", (error) => {
|
|
51
|
+
clearTimeout(timer);
|
|
52
|
+
resolve(`the login shell ${shell} did not run: ${error.message}`);
|
|
53
|
+
});
|
|
54
|
+
child.once("exit", (code) => {
|
|
55
|
+
clearTimeout(timer);
|
|
56
|
+
resolve(
|
|
57
|
+
code === 0
|
|
58
|
+
? "found"
|
|
59
|
+
: code === 127
|
|
60
|
+
? "missing"
|
|
61
|
+
: `${command.join(" ")} failed with exit code ${code ?? "none"}`,
|
|
62
|
+
);
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export async function missingToolchains(
|
|
68
|
+
groups: readonly string[],
|
|
69
|
+
env: NodeJS.ProcessEnv,
|
|
70
|
+
{
|
|
71
|
+
timeoutMs = PROBE_TIMEOUT_MS,
|
|
72
|
+
toolchains = TOOLCHAINS,
|
|
73
|
+
}: { timeoutMs?: number; toolchains?: Toolchains } = {},
|
|
74
|
+
) {
|
|
75
|
+
const entries = await Promise.all(
|
|
76
|
+
groups.map(async (group) => {
|
|
77
|
+
const commands = Object.hasOwn(toolchains, group)
|
|
78
|
+
? toolchains[group]
|
|
79
|
+
: [];
|
|
80
|
+
|
|
81
|
+
const outcomes = await Promise.all(
|
|
82
|
+
commands.map((command) => probe(command, env, timeoutMs)),
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
const missing = commands
|
|
86
|
+
.filter((_, i) => outcomes[i] === "missing")
|
|
87
|
+
.map(([tool]) => tool);
|
|
88
|
+
|
|
89
|
+
const failures = outcomes.filter(
|
|
90
|
+
(outcome) => outcome !== "found" && outcome !== "missing",
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
const details = [
|
|
94
|
+
...(missing.length > 0
|
|
95
|
+
? [
|
|
96
|
+
`${missing.join(" and ")} ${missing.length > 1 ? "were" : "was"} not found on the login shell's PATH`,
|
|
97
|
+
]
|
|
98
|
+
: []),
|
|
99
|
+
...failures,
|
|
100
|
+
];
|
|
101
|
+
|
|
102
|
+
return details.length > 0 ? [[group, details.join("; ")] as const] : [];
|
|
103
|
+
}),
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
return new Map(entries.flat());
|
|
107
|
+
}
|