@ycodium-ai/plugin-browser-workbench 0.2.2706 → 0.2.2743
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 +45 -14
- package/dist/index.js +645 -475
- package/locales/en.json +3 -0
- package/locales/zh-CN.json +3 -0
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -87,10 +87,376 @@ import * as NodeFSP5 from "node:fs/promises";
|
|
|
87
87
|
|
|
88
88
|
// src/attach.ts
|
|
89
89
|
import * as NodeNet from "node:net";
|
|
90
|
+
|
|
91
|
+
// src/paths.ts
|
|
92
|
+
import * as NodeCrypto from "node:crypto";
|
|
93
|
+
import * as NodeFSP from "node:fs/promises";
|
|
94
|
+
import * as NodePath from "node:path";
|
|
95
|
+
|
|
96
|
+
// src/platform.ts
|
|
97
|
+
import * as NodeProcess from "node:process";
|
|
98
|
+
var CLI_PLATFORMS = [
|
|
99
|
+
"linux-x64",
|
|
100
|
+
"linux-arm64",
|
|
101
|
+
"darwin-x64",
|
|
102
|
+
"darwin-arm64",
|
|
103
|
+
"win32-x64"
|
|
104
|
+
];
|
|
105
|
+
var CLI_DOWNLOAD_BASE_URL = "https://update.code.visualstudio.com";
|
|
106
|
+
var cliRelativePath = (platform3 = NodeProcess.platform) => platform3 === "win32" ? "bin/code.exe" : "bin/code";
|
|
107
|
+
var cliProgramSelector = (platform3 = NodeProcess.platform) => `data:${cliRelativePath(platform3)}`;
|
|
108
|
+
var detectCliPlatform = (platform3 = NodeProcess.platform, arch3 = NodeProcess.arch) => {
|
|
109
|
+
if (platform3 === "linux" && arch3 === "x64") return "linux-x64";
|
|
110
|
+
if (platform3 === "linux" && arch3 === "arm64") return "linux-arm64";
|
|
111
|
+
if (platform3 === "darwin" && arch3 === "x64") return "darwin-x64";
|
|
112
|
+
if (platform3 === "darwin" && arch3 === "arm64") return "darwin-arm64";
|
|
113
|
+
if (platform3 === "win32" && arch3 === "x64") return "win32-x64";
|
|
114
|
+
return null;
|
|
115
|
+
};
|
|
116
|
+
var cliDownloadUrl = (cliPlatform, version) => `${CLI_DOWNLOAD_BASE_URL}/${version}/cli-${cliPlatform}/stable`;
|
|
117
|
+
var cliArchiveKind = (cliPlatform) => cliPlatform.startsWith("linux") ? "tar.gz" : "zip";
|
|
118
|
+
|
|
119
|
+
// src/paths.ts
|
|
120
|
+
var workbenchPaths = (dataDir) => ({
|
|
121
|
+
dataDir,
|
|
122
|
+
binDir: NodePath.join(dataDir, "bin"),
|
|
123
|
+
cliPath: NodePath.join(dataDir, cliRelativePath()),
|
|
124
|
+
cliVerifiedPath: NodePath.join(dataDir, "bin", "cli-verified.json"),
|
|
125
|
+
cliDataDir: NodePath.join(dataDir, "cli-data"),
|
|
126
|
+
serverDataDir: NodePath.join(dataDir, "server-data"),
|
|
127
|
+
tokenPath: NodePath.join(dataDir, "serve-web-token"),
|
|
128
|
+
instancePath: NodePath.join(dataDir, "instance.json"),
|
|
129
|
+
licensePath: NodePath.join(dataDir, "license-accepted.json"),
|
|
130
|
+
configPath: NodePath.join(dataDir, "config.json")
|
|
131
|
+
});
|
|
132
|
+
var writeAtomic = async (filePath, contents, mode) => {
|
|
133
|
+
await NodeFSP.mkdir(NodePath.dirname(filePath), { recursive: true });
|
|
134
|
+
const tmp = `${filePath}.${NodeCrypto.randomBytes(8).toString("hex")}.tmp`;
|
|
135
|
+
const payload = typeof contents === "string" ? contents : Buffer.from(contents);
|
|
136
|
+
try {
|
|
137
|
+
await NodeFSP.writeFile(tmp, payload, mode === void 0 ? {} : { mode });
|
|
138
|
+
await NodeFSP.rename(tmp, filePath);
|
|
139
|
+
if (mode !== void 0) await NodeFSP.chmod(filePath, mode);
|
|
140
|
+
} catch (error) {
|
|
141
|
+
await NodeFSP.unlink(tmp).catch(() => void 0);
|
|
142
|
+
throw error;
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
var fileExists = async (filePath) => {
|
|
146
|
+
try {
|
|
147
|
+
await NodeFSP.access(filePath);
|
|
148
|
+
return true;
|
|
149
|
+
} catch {
|
|
150
|
+
return false;
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
var readJsonFile = async (filePath) => {
|
|
154
|
+
try {
|
|
155
|
+
const text = await NodeFSP.readFile(filePath, "utf8");
|
|
156
|
+
return JSON.parse(text);
|
|
157
|
+
} catch {
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
var writeJsonFile = async (filePath, value, mode) => {
|
|
162
|
+
await writeAtomic(filePath, `${JSON.stringify(value)}
|
|
163
|
+
`, mode);
|
|
164
|
+
};
|
|
165
|
+
var removeFile = async (filePath) => {
|
|
166
|
+
await NodeFSP.unlink(filePath).catch(() => void 0);
|
|
167
|
+
};
|
|
168
|
+
var readInstance = (paths) => readJsonFile(paths.instancePath);
|
|
169
|
+
var writeInstance = (paths, record) => writeJsonFile(paths.instancePath, record);
|
|
170
|
+
var readConfigFile = (paths) => readJsonFile(paths.configPath);
|
|
171
|
+
var writeConfigFile = (paths, value) => writeJsonFile(paths.configPath, value);
|
|
172
|
+
var writeLicenseAccepted = async (paths) => {
|
|
173
|
+
await writeJsonFile(paths.licensePath, { acceptedAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
174
|
+
};
|
|
175
|
+
var TOKEN_BYTES = 32;
|
|
176
|
+
var readServeWebToken = async (paths) => {
|
|
177
|
+
try {
|
|
178
|
+
const text = (await NodeFSP.readFile(paths.tokenPath, "utf8")).trim();
|
|
179
|
+
return text.length > 0 ? text : null;
|
|
180
|
+
} catch {
|
|
181
|
+
return null;
|
|
182
|
+
}
|
|
183
|
+
};
|
|
184
|
+
var ensureServeWebToken = async (paths) => {
|
|
185
|
+
const existing = await readServeWebToken(paths);
|
|
186
|
+
if (existing !== null) return existing;
|
|
187
|
+
const token = NodeCrypto.randomBytes(TOKEN_BYTES).toString("base64url");
|
|
188
|
+
await writeAtomic(paths.tokenPath, `${token}
|
|
189
|
+
`, 384);
|
|
190
|
+
return token;
|
|
191
|
+
};
|
|
192
|
+
var ensureDataDirs = async (paths) => {
|
|
193
|
+
await NodeFSP.mkdir(paths.binDir, { recursive: true });
|
|
194
|
+
await NodeFSP.mkdir(paths.cliDataDir, { recursive: true });
|
|
195
|
+
await NodeFSP.mkdir(paths.serverDataDir, { recursive: true });
|
|
196
|
+
};
|
|
197
|
+
var directorySize = async (dir) => {
|
|
198
|
+
let total = 0;
|
|
199
|
+
const walk = async (current) => {
|
|
200
|
+
let entries;
|
|
201
|
+
try {
|
|
202
|
+
entries = await NodeFSP.readdir(current, { withFileTypes: true });
|
|
203
|
+
} catch {
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
for (const entry of entries) {
|
|
207
|
+
const child = NodePath.join(current, entry.name);
|
|
208
|
+
if (entry.isDirectory()) {
|
|
209
|
+
await walk(child);
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
try {
|
|
213
|
+
total += (await NodeFSP.stat(child)).size;
|
|
214
|
+
} catch {
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
};
|
|
218
|
+
await walk(dir);
|
|
219
|
+
return total;
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
// src/types.ts
|
|
223
|
+
var WORKBENCH_PROGRESS_STAGES = [
|
|
224
|
+
"license",
|
|
225
|
+
"check-cli",
|
|
226
|
+
"download-cli",
|
|
227
|
+
"start-server",
|
|
228
|
+
"download-server",
|
|
229
|
+
"register-port"
|
|
230
|
+
];
|
|
231
|
+
var WORKBENCH_PROGRESS_STATUSES = [
|
|
232
|
+
"started",
|
|
233
|
+
"progress",
|
|
234
|
+
"output",
|
|
235
|
+
"succeeded",
|
|
236
|
+
"skipped",
|
|
237
|
+
"failed"
|
|
238
|
+
];
|
|
239
|
+
var WORKBENCH_FAILURE_KINDS = [
|
|
240
|
+
"license-required",
|
|
241
|
+
"download-failed",
|
|
242
|
+
"disk-full",
|
|
243
|
+
"launch-failed",
|
|
244
|
+
"port-in-use",
|
|
245
|
+
"server-download-failed",
|
|
246
|
+
"grant-expired",
|
|
247
|
+
"host-key-mismatch",
|
|
248
|
+
"channel-failed",
|
|
249
|
+
"unknown"
|
|
250
|
+
];
|
|
251
|
+
var WorkbenchFailure = class extends Error {
|
|
252
|
+
code;
|
|
253
|
+
constructor(code, message) {
|
|
254
|
+
super(message);
|
|
255
|
+
this.name = "WorkbenchFailure";
|
|
256
|
+
this.code = code;
|
|
257
|
+
}
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
// src/util.ts
|
|
261
|
+
var messageOf = (error) => error instanceof Error ? error.message : String(error);
|
|
262
|
+
var isDiskFull = (error) => {
|
|
263
|
+
if (typeof error !== "object" || error === null || !("code" in error)) return false;
|
|
264
|
+
return error.code === "ENOSPC";
|
|
265
|
+
};
|
|
266
|
+
var parsePort = (value) => {
|
|
267
|
+
const asNumber = (input) => Number.isInteger(input) && input >= 1 && input <= 65535 ? input : null;
|
|
268
|
+
if (typeof value === "number") return asNumber(value);
|
|
269
|
+
if (typeof value === "string" && value.trim() !== "") {
|
|
270
|
+
const parsed = Number(value);
|
|
271
|
+
return Number.isFinite(parsed) ? asNumber(parsed) : null;
|
|
272
|
+
}
|
|
273
|
+
return null;
|
|
274
|
+
};
|
|
275
|
+
var redactWorkbenchSecrets = (line) => line.replaceAll(/([?&]tkn=)[^&\s]+/gi, "$1<redacted>");
|
|
276
|
+
var sleep = (ms, signal) => new Promise((resolve, reject) => {
|
|
277
|
+
if (signal?.aborted) {
|
|
278
|
+
reject(signal.reason instanceof Error ? signal.reason : new Error("aborted"));
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
const timer = setTimeout(() => {
|
|
282
|
+
signal?.removeEventListener("abort", onAbort);
|
|
283
|
+
resolve();
|
|
284
|
+
}, ms);
|
|
285
|
+
const onAbort = () => {
|
|
286
|
+
clearTimeout(timer);
|
|
287
|
+
reject(signal?.reason instanceof Error ? signal.reason : new Error("aborted"));
|
|
288
|
+
};
|
|
289
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
// src/serveWeb.ts
|
|
293
|
+
var START_SERVER_TIMEOUT_MS = 3e4;
|
|
294
|
+
var DOWNLOAD_SERVER_TIMEOUT_MS = 10 * 6e4;
|
|
295
|
+
var DOWNLOAD_SERVER_POLL_MS = 1e3;
|
|
296
|
+
var WEB_UI_AVAILABLE_PATTERN = /Web UI available at http:\/\/127\.0\.0\.1:(\d+)/;
|
|
297
|
+
var SERVER_DOWNLOADING_PATTERN = /Downloading server\b/;
|
|
298
|
+
var SERVER_STARTING_PATTERN = /Starting server\b/;
|
|
299
|
+
var parseWebUiPort = (line) => {
|
|
300
|
+
const match = WEB_UI_AVAILABLE_PATTERN.exec(line);
|
|
301
|
+
if (match === null) return null;
|
|
302
|
+
const port = Number(match[1]);
|
|
303
|
+
return Number.isInteger(port) && port >= 1 && port <= 65535 ? port : null;
|
|
304
|
+
};
|
|
305
|
+
var isServerLogLine = (line) => SERVER_DOWNLOADING_PATTERN.test(line) || SERVER_STARTING_PATTERN.test(line);
|
|
306
|
+
var serveWebArgs = (paths) => [
|
|
307
|
+
"serve-web",
|
|
308
|
+
"--host",
|
|
309
|
+
"127.0.0.1",
|
|
310
|
+
"--port",
|
|
311
|
+
"0",
|
|
312
|
+
"--connection-token-file",
|
|
313
|
+
paths.tokenPath,
|
|
314
|
+
"--accept-server-license-terms",
|
|
315
|
+
"--server-data-dir",
|
|
316
|
+
paths.serverDataDir,
|
|
317
|
+
"--cli-data-dir",
|
|
318
|
+
paths.cliDataDir,
|
|
319
|
+
"--disable-telemetry"
|
|
320
|
+
];
|
|
321
|
+
var splitLines = (text) => {
|
|
322
|
+
const parts = text.split(/\r?\n/);
|
|
323
|
+
const rest = parts.pop() ?? "";
|
|
324
|
+
return { complete: parts, rest };
|
|
325
|
+
};
|
|
326
|
+
var consumeProcessLines = (stream, onLine) => {
|
|
327
|
+
const decoder = new TextDecoder();
|
|
328
|
+
let pending = "";
|
|
329
|
+
const run = async () => {
|
|
330
|
+
for await (const chunk of stream) {
|
|
331
|
+
pending += decoder.decode(chunk, { stream: true });
|
|
332
|
+
const split = splitLines(pending);
|
|
333
|
+
pending = split.rest;
|
|
334
|
+
for (const line of split.complete) onLine(redactWorkbenchSecrets(line));
|
|
335
|
+
}
|
|
336
|
+
pending += decoder.decode();
|
|
337
|
+
if (pending.length > 0) onLine(redactWorkbenchSecrets(pending));
|
|
338
|
+
};
|
|
339
|
+
return run();
|
|
340
|
+
};
|
|
341
|
+
var waitForServeWebPort = (input) => {
|
|
342
|
+
const timeoutMs = input.timeoutMs ?? START_SERVER_TIMEOUT_MS;
|
|
343
|
+
const stderrChunks = [];
|
|
344
|
+
let settled = false;
|
|
345
|
+
let resolvePort = () => void 0;
|
|
346
|
+
let rejectPort = () => void 0;
|
|
347
|
+
const portPromise = new Promise((resolve, reject) => {
|
|
348
|
+
resolvePort = resolve;
|
|
349
|
+
rejectPort = reject;
|
|
350
|
+
});
|
|
351
|
+
const finishError = (error) => {
|
|
352
|
+
if (settled) return;
|
|
353
|
+
settled = true;
|
|
354
|
+
rejectPort(error);
|
|
355
|
+
};
|
|
356
|
+
const finishPort = (port) => {
|
|
357
|
+
if (settled) return;
|
|
358
|
+
settled = true;
|
|
359
|
+
resolvePort(port);
|
|
360
|
+
};
|
|
361
|
+
const timer = setTimeout(() => {
|
|
362
|
+
finishError(
|
|
363
|
+
new WorkbenchFailure(
|
|
364
|
+
"launch-failed",
|
|
365
|
+
"The VS Code CLI started but did not publish a local address in time."
|
|
366
|
+
)
|
|
367
|
+
);
|
|
368
|
+
}, timeoutMs);
|
|
369
|
+
void consumeProcessLines(input.handle.stdout, (line) => {
|
|
370
|
+
input.onStdoutLine(line);
|
|
371
|
+
const port = parseWebUiPort(line);
|
|
372
|
+
if (port !== null) finishPort(port);
|
|
373
|
+
}).catch((error) => {
|
|
374
|
+
finishError(error instanceof Error ? error : new Error(String(error)));
|
|
375
|
+
});
|
|
376
|
+
void consumeProcessLines(input.handle.stderr, (line) => {
|
|
377
|
+
stderrChunks.push(line);
|
|
378
|
+
input.onStderrLine(line);
|
|
379
|
+
}).catch(() => void 0);
|
|
380
|
+
void input.handle.exited.then((code) => {
|
|
381
|
+
if (settled) return;
|
|
382
|
+
const tail = stderrChunks.slice(-20).join("\n");
|
|
383
|
+
finishError(
|
|
384
|
+
new WorkbenchFailure(
|
|
385
|
+
"launch-failed",
|
|
386
|
+
tail.length > 0 ? `The VS Code CLI exited before it published a local address.
|
|
387
|
+
${tail}` : `The VS Code CLI exited before it published a local address (code ${String(code)}).`
|
|
388
|
+
)
|
|
389
|
+
);
|
|
390
|
+
});
|
|
391
|
+
return portPromise.then(
|
|
392
|
+
(port) => {
|
|
393
|
+
clearTimeout(timer);
|
|
394
|
+
return { port, stderr: stderrChunks.join("\n") };
|
|
395
|
+
},
|
|
396
|
+
(error) => {
|
|
397
|
+
clearTimeout(timer);
|
|
398
|
+
throw error;
|
|
399
|
+
}
|
|
400
|
+
);
|
|
401
|
+
};
|
|
402
|
+
var serverReadyStatus = (status) => {
|
|
403
|
+
if (status === 202) return "downloading";
|
|
404
|
+
if (status === 200 || status === 301 || status === 302 || status === 307 || status === 308) {
|
|
405
|
+
return "ready";
|
|
406
|
+
}
|
|
407
|
+
if (status === 403) return "forbidden";
|
|
408
|
+
return "other";
|
|
409
|
+
};
|
|
410
|
+
var waitForServerDownload = async (input) => {
|
|
411
|
+
const timeoutMs = input.timeoutMs ?? DOWNLOAD_SERVER_TIMEOUT_MS;
|
|
412
|
+
const pollMs = input.pollMs ?? DOWNLOAD_SERVER_POLL_MS;
|
|
413
|
+
const startedAtMs = Date.now();
|
|
414
|
+
const deadline = startedAtMs + timeoutMs;
|
|
415
|
+
const url = `http://127.0.0.1:${String(input.port)}/?tkn=${input.token}`;
|
|
416
|
+
const serveWebDir = `${input.cliDataDir}/serve-web`;
|
|
417
|
+
while (Date.now() < deadline) {
|
|
418
|
+
if (input.signal?.aborted) {
|
|
419
|
+
throw new WorkbenchFailure("server-download-failed", "The server download was cancelled.");
|
|
420
|
+
}
|
|
421
|
+
try {
|
|
422
|
+
const response = await input.http(
|
|
423
|
+
url,
|
|
424
|
+
input.signal === void 0 ? { redirect: "manual" } : { redirect: "manual", signal: input.signal }
|
|
425
|
+
);
|
|
426
|
+
const kind = serverReadyStatus(response.status);
|
|
427
|
+
if (kind === "ready") return;
|
|
428
|
+
if (kind === "forbidden") {
|
|
429
|
+
throw new WorkbenchFailure(
|
|
430
|
+
"server-download-failed",
|
|
431
|
+
"The VS Code server rejected the connection token."
|
|
432
|
+
);
|
|
433
|
+
}
|
|
434
|
+
} catch (error) {
|
|
435
|
+
if (error instanceof WorkbenchFailure) throw error;
|
|
436
|
+
}
|
|
437
|
+
const transferredBytes = await directorySize(serveWebDir);
|
|
438
|
+
input.onTransfer(
|
|
439
|
+
computeTransfer({
|
|
440
|
+
transferredBytes,
|
|
441
|
+
totalBytes: null,
|
|
442
|
+
startedAtMs,
|
|
443
|
+
nowMs: Date.now()
|
|
444
|
+
})
|
|
445
|
+
);
|
|
446
|
+
await sleep(pollMs, input.signal);
|
|
447
|
+
}
|
|
448
|
+
throw new WorkbenchFailure(
|
|
449
|
+
"server-download-failed",
|
|
450
|
+
"The VS Code server did not become ready in time."
|
|
451
|
+
);
|
|
452
|
+
};
|
|
453
|
+
|
|
454
|
+
// src/attach.ts
|
|
90
455
|
var ATTACH_PROBE_TIMEOUT_MS = 1e3;
|
|
91
456
|
var probeLoopback = (port, timeoutMs = ATTACH_PROBE_TIMEOUT_MS) => new Promise((resolve) => {
|
|
92
457
|
const socket = NodeNet.connect({ host: "127.0.0.1", port });
|
|
93
458
|
const finish = (ok) => {
|
|
459
|
+
clearTimeout(timer);
|
|
94
460
|
socket.removeAllListeners();
|
|
95
461
|
socket.destroy();
|
|
96
462
|
resolve(ok);
|
|
@@ -105,6 +471,31 @@ var probeLoopback = (port, timeoutMs = ATTACH_PROBE_TIMEOUT_MS) => new Promise((
|
|
|
105
471
|
finish(false);
|
|
106
472
|
});
|
|
107
473
|
});
|
|
474
|
+
var OWNERSHIP_PROBE_TIMEOUT_MS = 1e3;
|
|
475
|
+
var probeServeWebOwnership = async (input) => {
|
|
476
|
+
const http = input.http ?? fetch;
|
|
477
|
+
const timeoutMs = input.timeoutMs ?? OWNERSHIP_PROBE_TIMEOUT_MS;
|
|
478
|
+
const ask = async (tkn) => {
|
|
479
|
+
try {
|
|
480
|
+
const response = await http(`http://127.0.0.1:${String(input.port)}/?tkn=${tkn}`, {
|
|
481
|
+
redirect: "manual",
|
|
482
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
483
|
+
});
|
|
484
|
+
return serverReadyStatus(response.status);
|
|
485
|
+
} catch {
|
|
486
|
+
return "down";
|
|
487
|
+
}
|
|
488
|
+
};
|
|
489
|
+
const [withToken, withWrong] = await Promise.all([ask(input.token), ask("not-the-token")]);
|
|
490
|
+
if (withWrong === "forbidden" && (withToken === "ready" || withToken === "downloading")) {
|
|
491
|
+
return "ours";
|
|
492
|
+
}
|
|
493
|
+
if (withWrong === "downloading" && (withToken === "downloading" || withToken === "ready")) {
|
|
494
|
+
return "pending";
|
|
495
|
+
}
|
|
496
|
+
if (withWrong === "ready") return "foreign";
|
|
497
|
+
return "unknown";
|
|
498
|
+
};
|
|
108
499
|
|
|
109
500
|
// src/catalog.ts
|
|
110
501
|
var catalog = {
|
|
@@ -125,12 +516,16 @@ var catalog = {
|
|
|
125
516
|
},
|
|
126
517
|
running: {
|
|
127
518
|
stopFirst: "Stop the workbench before updating the CLI."
|
|
519
|
+
},
|
|
520
|
+
adopt: {
|
|
521
|
+
notOurs: "The workbench server found from the last run does not accept this machine's token, so Ycodium let go of it. Open again to start a fresh one."
|
|
128
522
|
}
|
|
129
523
|
};
|
|
130
524
|
var failureMessage = (kind) => catalog.failure[kind];
|
|
131
525
|
var attachNoListenerMessage = (port) => catalog.attach.noListener.replace("{{port}}", String(port));
|
|
132
526
|
|
|
133
527
|
// src/cli.ts
|
|
528
|
+
import * as NodeCrypto2 from "node:crypto";
|
|
134
529
|
import * as NodeFS from "node:fs";
|
|
135
530
|
import * as NodeFSP2 from "node:fs/promises";
|
|
136
531
|
import * as NodeProcess2 from "node:process";
|
|
@@ -139,46 +534,6 @@ import * as NodeStreamPromises from "node:stream/promises";
|
|
|
139
534
|
|
|
140
535
|
// src/archive.ts
|
|
141
536
|
import * as NodeZlib from "node:zlib";
|
|
142
|
-
|
|
143
|
-
// src/types.ts
|
|
144
|
-
var WORKBENCH_PROGRESS_STAGES = [
|
|
145
|
-
"license",
|
|
146
|
-
"check-cli",
|
|
147
|
-
"download-cli",
|
|
148
|
-
"start-server",
|
|
149
|
-
"download-server",
|
|
150
|
-
"register-port"
|
|
151
|
-
];
|
|
152
|
-
var WORKBENCH_PROGRESS_STATUSES = [
|
|
153
|
-
"started",
|
|
154
|
-
"progress",
|
|
155
|
-
"output",
|
|
156
|
-
"succeeded",
|
|
157
|
-
"skipped",
|
|
158
|
-
"failed"
|
|
159
|
-
];
|
|
160
|
-
var WORKBENCH_FAILURE_KINDS = [
|
|
161
|
-
"license-required",
|
|
162
|
-
"download-failed",
|
|
163
|
-
"disk-full",
|
|
164
|
-
"launch-failed",
|
|
165
|
-
"port-in-use",
|
|
166
|
-
"server-download-failed",
|
|
167
|
-
"grant-expired",
|
|
168
|
-
"host-key-mismatch",
|
|
169
|
-
"channel-failed",
|
|
170
|
-
"unknown"
|
|
171
|
-
];
|
|
172
|
-
var WorkbenchFailure = class extends Error {
|
|
173
|
-
code;
|
|
174
|
-
constructor(code, message) {
|
|
175
|
-
super(message);
|
|
176
|
-
this.name = "WorkbenchFailure";
|
|
177
|
-
this.code = code;
|
|
178
|
-
}
|
|
179
|
-
};
|
|
180
|
-
|
|
181
|
-
// src/archive.ts
|
|
182
537
|
var TAR_BLOCK = 512;
|
|
183
538
|
var ZIP_LOCAL = 67324752;
|
|
184
539
|
var ZIP_CENTRAL = 33639248;
|
|
@@ -299,166 +654,15 @@ var extractCliBinary = async (archive, kind) => {
|
|
|
299
654
|
return pickCliBinary(await extractZip(archive));
|
|
300
655
|
};
|
|
301
656
|
|
|
302
|
-
// src/
|
|
303
|
-
|
|
304
|
-
var
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
if (platform3 === "darwin" && arch3 === "x64") return "darwin-x64";
|
|
311
|
-
if (platform3 === "darwin" && arch3 === "arm64") return "darwin-arm64";
|
|
312
|
-
if (platform3 === "win32" && arch3 === "x64") return "win32-x64";
|
|
313
|
-
return null;
|
|
314
|
-
};
|
|
315
|
-
var cliDownloadUrl = (cliPlatform) => `${CLI_DOWNLOAD_BASE_URL}/cli-${cliPlatform}/stable`;
|
|
316
|
-
var cliArchiveKind = (cliPlatform) => cliPlatform.startsWith("win32") ? "zip" : "tar.gz";
|
|
317
|
-
|
|
318
|
-
// src/paths.ts
|
|
319
|
-
import * as NodeCrypto from "node:crypto";
|
|
320
|
-
import * as NodeFSP from "node:fs/promises";
|
|
321
|
-
import * as NodePath from "node:path";
|
|
322
|
-
var workbenchPaths = (dataDir) => ({
|
|
323
|
-
dataDir,
|
|
324
|
-
binDir: NodePath.join(dataDir, "bin"),
|
|
325
|
-
cliPath: NodePath.join(dataDir, cliRelativePath()),
|
|
326
|
-
cliDataDir: NodePath.join(dataDir, "cli-data"),
|
|
327
|
-
serverDataDir: NodePath.join(dataDir, "server-data"),
|
|
328
|
-
tokenPath: NodePath.join(dataDir, "serve-web-token"),
|
|
329
|
-
instancePath: NodePath.join(dataDir, "instance.json"),
|
|
330
|
-
licensePath: NodePath.join(dataDir, "license-accepted.json"),
|
|
331
|
-
configPath: NodePath.join(dataDir, "config.json")
|
|
332
|
-
});
|
|
333
|
-
var writeAtomic = async (filePath, contents, mode) => {
|
|
334
|
-
await NodeFSP.mkdir(NodePath.dirname(filePath), { recursive: true });
|
|
335
|
-
const tmp = `${filePath}.${NodeCrypto.randomBytes(8).toString("hex")}.tmp`;
|
|
336
|
-
const payload = typeof contents === "string" ? contents : Buffer.from(contents);
|
|
337
|
-
try {
|
|
338
|
-
await NodeFSP.writeFile(tmp, payload, mode === void 0 ? {} : { mode });
|
|
339
|
-
await NodeFSP.rename(tmp, filePath);
|
|
340
|
-
if (mode !== void 0) await NodeFSP.chmod(filePath, mode);
|
|
341
|
-
} catch (error) {
|
|
342
|
-
await NodeFSP.unlink(tmp).catch(() => void 0);
|
|
343
|
-
throw error;
|
|
344
|
-
}
|
|
345
|
-
};
|
|
346
|
-
var fileExists = async (filePath) => {
|
|
347
|
-
try {
|
|
348
|
-
await NodeFSP.access(filePath);
|
|
349
|
-
return true;
|
|
350
|
-
} catch {
|
|
351
|
-
return false;
|
|
352
|
-
}
|
|
353
|
-
};
|
|
354
|
-
var readJsonFile = async (filePath) => {
|
|
355
|
-
try {
|
|
356
|
-
const text = await NodeFSP.readFile(filePath, "utf8");
|
|
357
|
-
return JSON.parse(text);
|
|
358
|
-
} catch {
|
|
359
|
-
return null;
|
|
360
|
-
}
|
|
361
|
-
};
|
|
362
|
-
var writeJsonFile = async (filePath, value, mode) => {
|
|
363
|
-
await writeAtomic(filePath, `${JSON.stringify(value)}
|
|
364
|
-
`, mode);
|
|
365
|
-
};
|
|
366
|
-
var removeFile = async (filePath) => {
|
|
367
|
-
await NodeFSP.unlink(filePath).catch(() => void 0);
|
|
368
|
-
};
|
|
369
|
-
var readInstance = (paths) => readJsonFile(paths.instancePath);
|
|
370
|
-
var writeInstance = (paths, record) => writeJsonFile(paths.instancePath, record);
|
|
371
|
-
var readConfigFile = (paths) => readJsonFile(paths.configPath);
|
|
372
|
-
var writeConfigFile = (paths, value) => writeJsonFile(paths.configPath, value);
|
|
373
|
-
var writeLicenseAccepted = async (paths) => {
|
|
374
|
-
await writeJsonFile(paths.licensePath, { acceptedAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
375
|
-
};
|
|
376
|
-
var TOKEN_BYTES = 32;
|
|
377
|
-
var readServeWebToken = async (paths) => {
|
|
378
|
-
try {
|
|
379
|
-
const text = (await NodeFSP.readFile(paths.tokenPath, "utf8")).trim();
|
|
380
|
-
return text.length > 0 ? text : null;
|
|
381
|
-
} catch {
|
|
382
|
-
return null;
|
|
383
|
-
}
|
|
384
|
-
};
|
|
385
|
-
var ensureServeWebToken = async (paths) => {
|
|
386
|
-
const existing = await readServeWebToken(paths);
|
|
387
|
-
if (existing !== null) return existing;
|
|
388
|
-
const token = NodeCrypto.randomBytes(TOKEN_BYTES).toString("base64url");
|
|
389
|
-
await writeAtomic(paths.tokenPath, `${token}
|
|
390
|
-
`, 384);
|
|
391
|
-
return token;
|
|
392
|
-
};
|
|
393
|
-
var ensureDataDirs = async (paths) => {
|
|
394
|
-
await NodeFSP.mkdir(paths.binDir, { recursive: true });
|
|
395
|
-
await NodeFSP.mkdir(paths.cliDataDir, { recursive: true });
|
|
396
|
-
await NodeFSP.mkdir(paths.serverDataDir, { recursive: true });
|
|
397
|
-
};
|
|
398
|
-
var directorySize = async (dir) => {
|
|
399
|
-
let total = 0;
|
|
400
|
-
const walk = async (current) => {
|
|
401
|
-
let entries;
|
|
402
|
-
try {
|
|
403
|
-
entries = await NodeFSP.readdir(current, { withFileTypes: true });
|
|
404
|
-
} catch {
|
|
405
|
-
return;
|
|
406
|
-
}
|
|
407
|
-
for (const entry of entries) {
|
|
408
|
-
const child = NodePath.join(current, entry.name);
|
|
409
|
-
if (entry.isDirectory()) {
|
|
410
|
-
await walk(child);
|
|
411
|
-
continue;
|
|
412
|
-
}
|
|
413
|
-
try {
|
|
414
|
-
total += (await NodeFSP.stat(child)).size;
|
|
415
|
-
} catch {
|
|
416
|
-
}
|
|
417
|
-
}
|
|
418
|
-
};
|
|
419
|
-
await walk(dir);
|
|
420
|
-
return total;
|
|
421
|
-
};
|
|
422
|
-
|
|
423
|
-
// src/util.ts
|
|
424
|
-
var messageOf = (error) => error instanceof Error ? error.message : String(error);
|
|
425
|
-
var isDiskFull = (error) => {
|
|
426
|
-
if (typeof error !== "object" || error === null || !("code" in error)) return false;
|
|
427
|
-
return error.code === "ENOSPC";
|
|
428
|
-
};
|
|
429
|
-
var parsePort = (value) => {
|
|
430
|
-
const asNumber = (input) => Number.isInteger(input) && input >= 1 && input <= 65535 ? input : null;
|
|
431
|
-
if (typeof value === "number") return asNumber(value);
|
|
432
|
-
if (typeof value === "string" && value.trim() !== "") {
|
|
433
|
-
const parsed = Number(value);
|
|
434
|
-
return Number.isFinite(parsed) ? asNumber(parsed) : null;
|
|
435
|
-
}
|
|
436
|
-
return null;
|
|
437
|
-
};
|
|
438
|
-
var redactWorkbenchSecrets = (line) => line.replaceAll(/([?&]tkn=)[^&\s]+/gi, "$1<redacted>");
|
|
439
|
-
var isProcessAlive = (pid) => {
|
|
440
|
-
try {
|
|
441
|
-
process.kill(pid, 0);
|
|
442
|
-
return true;
|
|
443
|
-
} catch {
|
|
444
|
-
return false;
|
|
445
|
-
}
|
|
657
|
+
// src/cliRelease.ts
|
|
658
|
+
var PINNED_CLI_VERSION = "1.139.1";
|
|
659
|
+
var PINNED_CLI_SHA256 = {
|
|
660
|
+
"linux-x64": "e079ddb70377cca515ab0cf5ca1ee4095a14e1f510af4b10a75b01c6018f9f02",
|
|
661
|
+
"linux-arm64": "5bc80c3622a628a8aee4be15967bdc7a039235d867c3d1bedf1b373d2d731dc7",
|
|
662
|
+
"darwin-x64": "7b8f895cf37909814aa3f1b71aa01fa58a468a71d1267098c1346bae3c8ac25e",
|
|
663
|
+
"darwin-arm64": "43ec2f4506beafde0783134de0b4b5fb94fd0e017d47a7b4ee6559b8bc363dea",
|
|
664
|
+
"win32-x64": "046b75c3e8ddad7692e05e20c0f8cab87ecfa66ec2fc92d55cc4e32044b522bf"
|
|
446
665
|
};
|
|
447
|
-
var sleep = (ms, signal) => new Promise((resolve, reject) => {
|
|
448
|
-
if (signal?.aborted) {
|
|
449
|
-
reject(signal.reason instanceof Error ? signal.reason : new Error("aborted"));
|
|
450
|
-
return;
|
|
451
|
-
}
|
|
452
|
-
const timer = setTimeout(() => {
|
|
453
|
-
signal?.removeEventListener("abort", onAbort);
|
|
454
|
-
resolve();
|
|
455
|
-
}, ms);
|
|
456
|
-
const onAbort = () => {
|
|
457
|
-
clearTimeout(timer);
|
|
458
|
-
reject(signal?.reason instanceof Error ? signal.reason : new Error("aborted"));
|
|
459
|
-
};
|
|
460
|
-
signal?.addEventListener("abort", onAbort, { once: true });
|
|
461
|
-
});
|
|
462
666
|
|
|
463
667
|
// src/cli.ts
|
|
464
668
|
var CLI_DOWNLOAD_SUFFIX = ".download";
|
|
@@ -468,7 +672,6 @@ var parseCliVersion = (stdout) => {
|
|
|
468
672
|
const version = match?.[1];
|
|
469
673
|
return version === void 0 || version.length === 0 ? null : version;
|
|
470
674
|
};
|
|
471
|
-
var cliIsInstalled = (paths) => fileExists(paths.cliPath);
|
|
472
675
|
var readInstalledCliVersion = async (processApi) => {
|
|
473
676
|
const result = await processApi.run({
|
|
474
677
|
program: cliProgramSelector(),
|
|
@@ -480,24 +683,65 @@ var readInstalledCliVersion = async (processApi) => {
|
|
|
480
683
|
}
|
|
481
684
|
return parseCliVersion(result.stdout) ?? parseCliVersion(result.stderr) ?? "unknown";
|
|
482
685
|
};
|
|
686
|
+
var sha256Hex = (bytes) => NodeCrypto2.createHash("sha256").update(bytes).digest("hex");
|
|
687
|
+
var fileSha256 = async (filePath) => {
|
|
688
|
+
const hash = NodeCrypto2.createHash("sha256");
|
|
689
|
+
await NodeStreamPromises.pipeline(NodeFS.createReadStream(filePath), hash);
|
|
690
|
+
return hash.digest("hex");
|
|
691
|
+
};
|
|
692
|
+
var isVerifiedCliInstall = (value) => {
|
|
693
|
+
if (typeof value !== "object" || value === null) return false;
|
|
694
|
+
const record = value;
|
|
695
|
+
return typeof record["version"] === "string" && typeof record["archiveSha256"] === "string" && typeof record["binarySha256"] === "string" && CLI_PLATFORMS.some((platform3) => platform3 === record["platform"]);
|
|
696
|
+
};
|
|
697
|
+
var readVerifiedCli = async (paths, options) => {
|
|
698
|
+
const platform3 = options.platform === void 0 ? detectCliPlatform() : options.platform;
|
|
699
|
+
const record = await readJsonFile(paths.cliVerifiedPath);
|
|
700
|
+
if (!isVerifiedCliInstall(record)) return null;
|
|
701
|
+
if (record.version !== PINNED_CLI_VERSION || record.platform !== platform3) return null;
|
|
702
|
+
if (!await fileExists(paths.cliPath)) return null;
|
|
703
|
+
if (!options.checkBinary) return record;
|
|
704
|
+
const actual = await fileSha256(paths.cliPath).catch(() => null);
|
|
705
|
+
return actual === record.binarySha256 ? record : null;
|
|
706
|
+
};
|
|
707
|
+
var writeVerifiedCli = (paths, install) => writeJsonFile(paths.cliVerifiedPath, install);
|
|
708
|
+
var clearVerifiedCli = (paths) => removeFile(paths.cliVerifiedPath);
|
|
483
709
|
var contentLengthOf = (response) => {
|
|
484
710
|
const raw = response.headers.get("content-length");
|
|
485
711
|
if (raw === null || raw.trim() === "") return null;
|
|
486
712
|
const parsed = Number(raw);
|
|
487
713
|
return Number.isFinite(parsed) && parsed >= 0 ? parsed : null;
|
|
488
714
|
};
|
|
715
|
+
var hostOf = (url) => {
|
|
716
|
+
try {
|
|
717
|
+
return new URL(url).host;
|
|
718
|
+
} catch {
|
|
719
|
+
return url;
|
|
720
|
+
}
|
|
721
|
+
};
|
|
722
|
+
var describeNetworkError = (error) => {
|
|
723
|
+
const message = messageOf(error);
|
|
724
|
+
if (typeof error !== "object" || error === null || !("cause" in error)) return message;
|
|
725
|
+
const cause = error.cause;
|
|
726
|
+
if (typeof cause !== "object" || cause === null) return message;
|
|
727
|
+
const code = cause.code;
|
|
728
|
+
const causeMessage = messageOf(cause);
|
|
729
|
+
return `${message}: ${typeof code === "string" && !causeMessage.includes(code) ? `${code} ` : ""}${causeMessage}`;
|
|
730
|
+
};
|
|
489
731
|
var streamResponseToFile = async (input) => {
|
|
490
732
|
if (input.response.body === null) {
|
|
491
733
|
throw new WorkbenchFailure("download-failed", "The CLI download had no body.");
|
|
492
734
|
}
|
|
493
735
|
const totalBytes = contentLengthOf(input.response);
|
|
494
736
|
const startedAtMs = Date.now();
|
|
737
|
+
const hash = NodeCrypto2.createHash("sha256");
|
|
495
738
|
let transferredBytes = 0;
|
|
496
739
|
const readable = NodeStream.Readable.fromWeb(
|
|
497
740
|
input.response.body
|
|
498
741
|
);
|
|
499
742
|
readable.on("data", (chunk) => {
|
|
500
743
|
transferredBytes += chunk.length;
|
|
744
|
+
hash.update(chunk);
|
|
501
745
|
input.onTransfer(
|
|
502
746
|
computeTransfer({
|
|
503
747
|
transferredBytes,
|
|
@@ -514,8 +758,13 @@ var streamResponseToFile = async (input) => {
|
|
|
514
758
|
if (isDiskFull(error)) {
|
|
515
759
|
throw new WorkbenchFailure("disk-full", messageOf(error));
|
|
516
760
|
}
|
|
517
|
-
|
|
761
|
+
const of = totalBytes === null ? "" : ` of ${String(totalBytes)}`;
|
|
762
|
+
throw new WorkbenchFailure(
|
|
763
|
+
"download-failed",
|
|
764
|
+
`The VS Code CLI download stopped after ${String(transferredBytes)}${of} bytes (${describeNetworkError(error)}).`
|
|
765
|
+
);
|
|
518
766
|
}
|
|
767
|
+
return hash.digest("hex");
|
|
519
768
|
};
|
|
520
769
|
var downloadCliBinary = async (input) => {
|
|
521
770
|
const platform3 = input.platform === void 0 ? detectCliPlatform() : input.platform;
|
|
@@ -525,31 +774,44 @@ var downloadCliBinary = async (input) => {
|
|
|
525
774
|
`This machine's platform (${NodeProcess2.platform}/${NodeProcess2.arch}) has no VS Code CLI build.`
|
|
526
775
|
);
|
|
527
776
|
}
|
|
528
|
-
const
|
|
777
|
+
const version = PINNED_CLI_VERSION;
|
|
778
|
+
const expectedSha256 = input.expectedSha256 ?? PINNED_CLI_SHA256[platform3];
|
|
779
|
+
const url = input.url ?? cliDownloadUrl(platform3, version);
|
|
529
780
|
let response;
|
|
530
781
|
try {
|
|
531
782
|
response = await input.http(url, input.signal === void 0 ? {} : { signal: input.signal });
|
|
532
783
|
} catch (error) {
|
|
533
784
|
if (isDiskFull(error)) throw new WorkbenchFailure("disk-full", messageOf(error));
|
|
534
|
-
throw new WorkbenchFailure(
|
|
785
|
+
throw new WorkbenchFailure(
|
|
786
|
+
"download-failed",
|
|
787
|
+
`Could not reach ${hostOf(url)} to download VS Code CLI ${version} (${describeNetworkError(error)}). Check this machine's internet access and proxy settings.`
|
|
788
|
+
);
|
|
535
789
|
}
|
|
536
790
|
if (!response.ok) {
|
|
791
|
+
const host = hostOf(response.url === "" ? url : response.url);
|
|
537
792
|
throw new WorkbenchFailure(
|
|
538
793
|
"download-failed",
|
|
539
|
-
|
|
794
|
+
`${host} answered HTTP ${String(response.status)} for VS Code CLI ${version}. If this machine reaches the internet through a proxy or firewall, check that it allows ${host}.`
|
|
540
795
|
);
|
|
541
796
|
}
|
|
542
797
|
await NodeFSP2.mkdir(input.paths.binDir, { recursive: true });
|
|
543
798
|
const archivePath = `${input.destPath}${CLI_DOWNLOAD_SUFFIX}`;
|
|
544
799
|
try {
|
|
545
|
-
await streamResponseToFile({
|
|
800
|
+
const archiveSha256 = await streamResponseToFile({
|
|
546
801
|
response,
|
|
547
802
|
filePath: archivePath,
|
|
548
803
|
onTransfer: input.onTransfer
|
|
549
804
|
});
|
|
805
|
+
if (archiveSha256 !== expectedSha256) {
|
|
806
|
+
throw new WorkbenchFailure(
|
|
807
|
+
"download-failed",
|
|
808
|
+
`The downloaded VS Code CLI ${version} (${platform3}) did not match its published SHA-256 (expected ${expectedSha256.slice(0, 12)}\u2026, got ${archiveSha256.slice(0, 12)}\u2026) and was discarded. If this machine uses a proxy, make sure it does not alter downloads.`
|
|
809
|
+
);
|
|
810
|
+
}
|
|
550
811
|
const archive = await NodeFSP2.readFile(archivePath);
|
|
551
812
|
const binary = await extractCliBinary(archive, cliArchiveKind(platform3));
|
|
552
813
|
await writeAtomic(input.destPath, binary, 493);
|
|
814
|
+
return { version, platform: platform3, archiveSha256, binarySha256: sha256Hex(binary) };
|
|
553
815
|
} catch (error) {
|
|
554
816
|
if (error instanceof WorkbenchFailure) throw error;
|
|
555
817
|
if (isDiskFull(error)) throw new WorkbenchFailure("disk-full", messageOf(error));
|
|
@@ -559,180 +821,31 @@ var downloadCliBinary = async (input) => {
|
|
|
559
821
|
}
|
|
560
822
|
};
|
|
561
823
|
|
|
562
|
-
// src/serveWeb.ts
|
|
563
|
-
var START_SERVER_TIMEOUT_MS = 3e4;
|
|
564
|
-
var DOWNLOAD_SERVER_TIMEOUT_MS = 10 * 6e4;
|
|
565
|
-
var DOWNLOAD_SERVER_POLL_MS = 1e3;
|
|
566
|
-
var WEB_UI_AVAILABLE_PATTERN = /Web UI available at http:\/\/127\.0\.0\.1:(\d+)/;
|
|
567
|
-
var SERVER_DOWNLOADING_PATTERN = /Downloading server\b/;
|
|
568
|
-
var SERVER_STARTING_PATTERN = /Starting server\b/;
|
|
569
|
-
var parseWebUiPort = (line) => {
|
|
570
|
-
const match = WEB_UI_AVAILABLE_PATTERN.exec(line);
|
|
571
|
-
if (match === null) return null;
|
|
572
|
-
const port = Number(match[1]);
|
|
573
|
-
return Number.isInteger(port) && port >= 1 && port <= 65535 ? port : null;
|
|
574
|
-
};
|
|
575
|
-
var isServerLogLine = (line) => SERVER_DOWNLOADING_PATTERN.test(line) || SERVER_STARTING_PATTERN.test(line);
|
|
576
|
-
var serveWebArgs = (paths) => [
|
|
577
|
-
"serve-web",
|
|
578
|
-
"--host",
|
|
579
|
-
"127.0.0.1",
|
|
580
|
-
"--port",
|
|
581
|
-
"0",
|
|
582
|
-
"--connection-token-file",
|
|
583
|
-
paths.tokenPath,
|
|
584
|
-
"--accept-server-license-terms",
|
|
585
|
-
"--server-data-dir",
|
|
586
|
-
paths.serverDataDir,
|
|
587
|
-
"--cli-data-dir",
|
|
588
|
-
paths.cliDataDir,
|
|
589
|
-
"--disable-telemetry"
|
|
590
|
-
];
|
|
591
|
-
var splitLines = (text) => {
|
|
592
|
-
const parts = text.split(/\r?\n/);
|
|
593
|
-
const rest = parts.pop() ?? "";
|
|
594
|
-
return { complete: parts, rest };
|
|
595
|
-
};
|
|
596
|
-
var consumeProcessLines = (stream, onLine) => {
|
|
597
|
-
const decoder = new TextDecoder();
|
|
598
|
-
let pending = "";
|
|
599
|
-
const run = async () => {
|
|
600
|
-
for await (const chunk of stream) {
|
|
601
|
-
pending += decoder.decode(chunk, { stream: true });
|
|
602
|
-
const split = splitLines(pending);
|
|
603
|
-
pending = split.rest;
|
|
604
|
-
for (const line of split.complete) onLine(redactWorkbenchSecrets(line));
|
|
605
|
-
}
|
|
606
|
-
pending += decoder.decode();
|
|
607
|
-
if (pending.length > 0) onLine(redactWorkbenchSecrets(pending));
|
|
608
|
-
};
|
|
609
|
-
return run();
|
|
610
|
-
};
|
|
611
|
-
var waitForServeWebPort = (input) => {
|
|
612
|
-
const timeoutMs = input.timeoutMs ?? START_SERVER_TIMEOUT_MS;
|
|
613
|
-
const stderrChunks = [];
|
|
614
|
-
let settled = false;
|
|
615
|
-
let resolvePort = () => void 0;
|
|
616
|
-
let rejectPort = () => void 0;
|
|
617
|
-
const portPromise = new Promise((resolve, reject) => {
|
|
618
|
-
resolvePort = resolve;
|
|
619
|
-
rejectPort = reject;
|
|
620
|
-
});
|
|
621
|
-
const finishError = (error) => {
|
|
622
|
-
if (settled) return;
|
|
623
|
-
settled = true;
|
|
624
|
-
rejectPort(error);
|
|
625
|
-
};
|
|
626
|
-
const finishPort = (port) => {
|
|
627
|
-
if (settled) return;
|
|
628
|
-
settled = true;
|
|
629
|
-
resolvePort(port);
|
|
630
|
-
};
|
|
631
|
-
const timer = setTimeout(() => {
|
|
632
|
-
finishError(
|
|
633
|
-
new WorkbenchFailure(
|
|
634
|
-
"launch-failed",
|
|
635
|
-
"The VS Code CLI started but did not publish a local address in time."
|
|
636
|
-
)
|
|
637
|
-
);
|
|
638
|
-
}, timeoutMs);
|
|
639
|
-
void consumeProcessLines(input.handle.stdout, (line) => {
|
|
640
|
-
input.onStdoutLine(line);
|
|
641
|
-
const port = parseWebUiPort(line);
|
|
642
|
-
if (port !== null) finishPort(port);
|
|
643
|
-
}).catch((error) => {
|
|
644
|
-
finishError(error instanceof Error ? error : new Error(String(error)));
|
|
645
|
-
});
|
|
646
|
-
void consumeProcessLines(input.handle.stderr, (line) => {
|
|
647
|
-
stderrChunks.push(line);
|
|
648
|
-
input.onStderrLine(line);
|
|
649
|
-
}).catch(() => void 0);
|
|
650
|
-
void input.handle.exited.then((code) => {
|
|
651
|
-
if (settled) return;
|
|
652
|
-
const tail = stderrChunks.slice(-20).join("\n");
|
|
653
|
-
finishError(
|
|
654
|
-
new WorkbenchFailure(
|
|
655
|
-
"launch-failed",
|
|
656
|
-
tail.length > 0 ? `The VS Code CLI exited before it published a local address.
|
|
657
|
-
${tail}` : `The VS Code CLI exited before it published a local address (code ${String(code)}).`
|
|
658
|
-
)
|
|
659
|
-
);
|
|
660
|
-
});
|
|
661
|
-
return portPromise.then(
|
|
662
|
-
(port) => {
|
|
663
|
-
clearTimeout(timer);
|
|
664
|
-
return { port, stderr: stderrChunks.join("\n") };
|
|
665
|
-
},
|
|
666
|
-
(error) => {
|
|
667
|
-
clearTimeout(timer);
|
|
668
|
-
throw error;
|
|
669
|
-
}
|
|
670
|
-
);
|
|
671
|
-
};
|
|
672
|
-
var serverReadyStatus = (status) => {
|
|
673
|
-
if (status === 202) return "downloading";
|
|
674
|
-
if (status === 200 || status === 301 || status === 302 || status === 307 || status === 308) {
|
|
675
|
-
return "ready";
|
|
676
|
-
}
|
|
677
|
-
if (status === 403) return "forbidden";
|
|
678
|
-
return "other";
|
|
679
|
-
};
|
|
680
|
-
var waitForServerDownload = async (input) => {
|
|
681
|
-
const timeoutMs = input.timeoutMs ?? DOWNLOAD_SERVER_TIMEOUT_MS;
|
|
682
|
-
const pollMs = input.pollMs ?? DOWNLOAD_SERVER_POLL_MS;
|
|
683
|
-
const startedAtMs = Date.now();
|
|
684
|
-
const deadline = startedAtMs + timeoutMs;
|
|
685
|
-
const url = `http://127.0.0.1:${String(input.port)}/?tkn=${input.token}`;
|
|
686
|
-
const serveWebDir = `${input.cliDataDir}/serve-web`;
|
|
687
|
-
while (Date.now() < deadline) {
|
|
688
|
-
if (input.signal?.aborted) {
|
|
689
|
-
throw new WorkbenchFailure("server-download-failed", "The server download was cancelled.");
|
|
690
|
-
}
|
|
691
|
-
try {
|
|
692
|
-
const response = await input.http(
|
|
693
|
-
url,
|
|
694
|
-
input.signal === void 0 ? { redirect: "manual" } : { redirect: "manual", signal: input.signal }
|
|
695
|
-
);
|
|
696
|
-
const kind = serverReadyStatus(response.status);
|
|
697
|
-
if (kind === "ready") return;
|
|
698
|
-
if (kind === "forbidden") {
|
|
699
|
-
throw new WorkbenchFailure(
|
|
700
|
-
"server-download-failed",
|
|
701
|
-
"The VS Code server rejected the connection token."
|
|
702
|
-
);
|
|
703
|
-
}
|
|
704
|
-
} catch (error) {
|
|
705
|
-
if (error instanceof WorkbenchFailure) throw error;
|
|
706
|
-
}
|
|
707
|
-
const transferredBytes = await directorySize(serveWebDir);
|
|
708
|
-
input.onTransfer(
|
|
709
|
-
computeTransfer({
|
|
710
|
-
transferredBytes,
|
|
711
|
-
totalBytes: null,
|
|
712
|
-
startedAtMs,
|
|
713
|
-
nowMs: Date.now()
|
|
714
|
-
})
|
|
715
|
-
);
|
|
716
|
-
await sleep(pollMs, input.signal);
|
|
717
|
-
}
|
|
718
|
-
throw new WorkbenchFailure(
|
|
719
|
-
"server-download-failed",
|
|
720
|
-
"The VS Code server did not become ready in time."
|
|
721
|
-
);
|
|
722
|
-
};
|
|
723
|
-
|
|
724
824
|
// src/instance.ts
|
|
725
|
-
var
|
|
726
|
-
|
|
825
|
+
var classifyRecord = async (record, paths) => {
|
|
826
|
+
const token = await readServeWebToken(paths);
|
|
827
|
+
if (token === null) return "stale";
|
|
828
|
+
const ownership = await probeServeWebOwnership({ port: record.port, token });
|
|
829
|
+
if (ownership !== "unknown") return ownership;
|
|
830
|
+
return await probeLoopback(record.port) ? "unresponsive" : "stale";
|
|
831
|
+
};
|
|
832
|
+
var reconcileInstanceRecord = async (paths, log) => {
|
|
727
833
|
const record = await readInstance(paths);
|
|
728
834
|
if (record === null) return;
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
835
|
+
const state = await classifyRecord(record, paths);
|
|
836
|
+
if (state === "foreign" || state === "stale") {
|
|
837
|
+
log?.info("dropping a recorded workbench instance that is not ours or not listening", {
|
|
838
|
+
port: record.port,
|
|
839
|
+
state
|
|
840
|
+
});
|
|
841
|
+
await removeFile(paths.instancePath);
|
|
842
|
+
return;
|
|
843
|
+
}
|
|
844
|
+
if (state === "unresponsive") {
|
|
845
|
+
log?.info("recorded workbench port is listening but does not answer yet; keeping its record", {
|
|
846
|
+
port: record.port
|
|
847
|
+
});
|
|
734
848
|
}
|
|
735
|
-
await removeFile(paths.instancePath);
|
|
736
849
|
};
|
|
737
850
|
var startServeWeb = async (input) => {
|
|
738
851
|
await ensureDataDirs(input.paths);
|
|
@@ -759,26 +872,23 @@ var startServeWeb = async (input) => {
|
|
|
759
872
|
startedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
760
873
|
};
|
|
761
874
|
await writeInstance(input.paths, record);
|
|
762
|
-
return { handle, record };
|
|
875
|
+
return { owned: true, handle, record };
|
|
763
876
|
} catch (error) {
|
|
764
877
|
await handle.kill("SIGTERM").catch(() => void 0);
|
|
765
878
|
throw error;
|
|
766
879
|
}
|
|
767
880
|
};
|
|
768
881
|
var STOP_GRACE_MS = 5e3;
|
|
769
|
-
var
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
await managed.handle.exited.catch(() => void 0);
|
|
779
|
-
}
|
|
882
|
+
var stopOwnedHandle = async (handle) => {
|
|
883
|
+
await handle.kill("SIGTERM").catch(() => void 0);
|
|
884
|
+
const timedOut = await Promise.race([
|
|
885
|
+
handle.exited.then(() => false).catch(() => false),
|
|
886
|
+
sleep(STOP_GRACE_MS).then(() => true)
|
|
887
|
+
]);
|
|
888
|
+
if (timedOut) {
|
|
889
|
+
await handle.kill("SIGKILL").catch(() => void 0);
|
|
890
|
+
await handle.exited.catch(() => void 0);
|
|
780
891
|
}
|
|
781
|
-
await removeFile(paths.instancePath);
|
|
782
892
|
};
|
|
783
893
|
|
|
784
894
|
// src/productPatch.ts
|
|
@@ -900,6 +1010,8 @@ var windowTitleTemplate = (machineLabel) => {
|
|
|
900
1010
|
// src/runtime.ts
|
|
901
1011
|
var MANAGED_LABEL = "VS Code";
|
|
902
1012
|
var EXTERNAL_LABEL = "code-server";
|
|
1013
|
+
var UNRESPONSIVE_GRACE_MS = START_SERVER_TIMEOUT_MS;
|
|
1014
|
+
var UNRESPONSIVE_POLL_MS = 1e3;
|
|
903
1015
|
var WorkbenchRuntime = class {
|
|
904
1016
|
bus = new ProgressBus();
|
|
905
1017
|
ctx;
|
|
@@ -910,13 +1022,15 @@ var WorkbenchRuntime = class {
|
|
|
910
1022
|
externalPort = null;
|
|
911
1023
|
externalListenPort = null;
|
|
912
1024
|
outputStage = "start-server";
|
|
913
|
-
|
|
1025
|
+
unresponsiveGraceMs;
|
|
1026
|
+
constructor(ctx, options = {}) {
|
|
914
1027
|
this.ctx = ctx;
|
|
1028
|
+
this.unresponsiveGraceMs = options.unresponsiveGraceMs ?? UNRESPONSIVE_GRACE_MS;
|
|
915
1029
|
this.paths = workbenchPaths(ctx.paths.dataDir);
|
|
916
1030
|
}
|
|
917
|
-
/**
|
|
1031
|
+
/** 激活时收拾实例记录(不对任何进程发信号)、按设置接外接端口。不能让 activate 失败。 */
|
|
918
1032
|
async start() {
|
|
919
|
-
await
|
|
1033
|
+
await reconcileInstanceRecord(this.paths, this.ctx.log);
|
|
920
1034
|
const desired = await this.desiredExternalPort();
|
|
921
1035
|
if (desired === null) return;
|
|
922
1036
|
try {
|
|
@@ -930,7 +1044,7 @@ var WorkbenchRuntime = class {
|
|
|
930
1044
|
}
|
|
931
1045
|
async dispose() {
|
|
932
1046
|
await this.enqueue(async () => {
|
|
933
|
-
await this.stopManaged();
|
|
1047
|
+
await this.stopManaged("keep");
|
|
934
1048
|
await this.clearExternal();
|
|
935
1049
|
});
|
|
936
1050
|
}
|
|
@@ -941,10 +1055,7 @@ var WorkbenchRuntime = class {
|
|
|
941
1055
|
return this.enqueue(() => this.inRun(() => this.runOpen(payload)));
|
|
942
1056
|
}
|
|
943
1057
|
stop() {
|
|
944
|
-
return this.enqueue(
|
|
945
|
-
await this.stopManaged();
|
|
946
|
-
return {};
|
|
947
|
-
});
|
|
1058
|
+
return this.enqueue(() => this.stopManaged("stop"));
|
|
948
1059
|
}
|
|
949
1060
|
update() {
|
|
950
1061
|
return this.enqueue(() => this.inRun(() => this.runUpdate()));
|
|
@@ -1000,7 +1111,8 @@ var WorkbenchRuntime = class {
|
|
|
1000
1111
|
return parsePort(file?.attachPort);
|
|
1001
1112
|
}
|
|
1002
1113
|
async readStatus() {
|
|
1003
|
-
const
|
|
1114
|
+
const verified = await readVerifiedCli(this.paths, { checkBinary: false });
|
|
1115
|
+
const installed = verified !== null;
|
|
1004
1116
|
const live = await this.managedIsLive();
|
|
1005
1117
|
const external = this.externalPort !== null && this.externalListenPort !== null ? { port: this.externalListenPort, portId: this.externalPort.portId } : void 0;
|
|
1006
1118
|
if (live && this.managed !== null) {
|
|
@@ -1022,30 +1134,27 @@ var WorkbenchRuntime = class {
|
|
|
1022
1134
|
external
|
|
1023
1135
|
};
|
|
1024
1136
|
}
|
|
1025
|
-
let cliVersion;
|
|
1026
|
-
if (installed) {
|
|
1027
|
-
try {
|
|
1028
|
-
cliVersion = await readInstalledCliVersion(this.ctx.process);
|
|
1029
|
-
} catch {
|
|
1030
|
-
cliVersion = void 0;
|
|
1031
|
-
}
|
|
1032
|
-
}
|
|
1033
1137
|
return {
|
|
1034
1138
|
mode: "none",
|
|
1035
1139
|
installed,
|
|
1036
1140
|
running: false,
|
|
1037
|
-
...
|
|
1141
|
+
...verified === null ? {} : { cliVersion: verified.version }
|
|
1038
1142
|
};
|
|
1039
1143
|
}
|
|
1144
|
+
/**
|
|
1145
|
+
* 工作台算在跑:端口上的服务只收我们的令牌(ours),或还在下载、令牌对错都回 202(pending)。
|
|
1146
|
+
* 没有句柄时只按 ours 领养,pending 不足以证明是我们的,留给 Open 等下载完再复判。
|
|
1147
|
+
* 这里不改任何状态:一轮 503/超时、或下载阶段的 202,都不能抹掉本次 spawn 的句柄。
|
|
1148
|
+
*/
|
|
1040
1149
|
async managedIsLive() {
|
|
1041
1150
|
if (this.managed !== null) {
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
return false;
|
|
1151
|
+
const state = await classifyRecord(this.managed.record, this.paths);
|
|
1152
|
+
return state === "ours" || state === "pending";
|
|
1045
1153
|
}
|
|
1046
1154
|
const record = await readInstance(this.paths);
|
|
1047
|
-
if (record === null) return false;
|
|
1048
|
-
|
|
1155
|
+
if (record === null || await classifyRecord(record, this.paths) !== "ours") return false;
|
|
1156
|
+
this.managed = { owned: false, record };
|
|
1157
|
+
return true;
|
|
1049
1158
|
}
|
|
1050
1159
|
async runOpen(payload) {
|
|
1051
1160
|
const desiredExternal = await this.desiredExternalPort();
|
|
@@ -1110,7 +1219,7 @@ var WorkbenchRuntime = class {
|
|
|
1110
1219
|
}
|
|
1111
1220
|
if (!patched) return;
|
|
1112
1221
|
this.ctx.log.info("restarting the workbench server to apply the patched product.json");
|
|
1113
|
-
await this.stopManaged();
|
|
1222
|
+
await this.stopManaged("keep");
|
|
1114
1223
|
await this.stepStartServer(cliVersion);
|
|
1115
1224
|
await this.stepDownloadServer(token);
|
|
1116
1225
|
}
|
|
@@ -1124,31 +1233,44 @@ var WorkbenchRuntime = class {
|
|
|
1124
1233
|
await writeLicenseAccepted(this.paths);
|
|
1125
1234
|
this.stage("license", "succeeded");
|
|
1126
1235
|
}
|
|
1236
|
+
/**
|
|
1237
|
+
* 只复用校验记录与二进制都对得上钉死版本的 CLI。旧版插件装的(没有记录)、版本不同的、
|
|
1238
|
+
* 装好后被换掉的,一律重下钉死的版本,不运行它。
|
|
1239
|
+
*/
|
|
1127
1240
|
async stepCheckAndDownloadCli() {
|
|
1128
1241
|
this.stage("check-cli", "started");
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
return version;
|
|
1135
|
-
} catch (error) {
|
|
1136
|
-
this.fail("check-cli", "unknown", messageOf(error));
|
|
1137
|
-
}
|
|
1242
|
+
const verified = await readVerifiedCli(this.paths, { checkBinary: true });
|
|
1243
|
+
if (verified !== null) {
|
|
1244
|
+
this.stage("check-cli", "succeeded", { detail: verified.version });
|
|
1245
|
+
this.stage("download-cli", "skipped", { skipReason: "already-installed" });
|
|
1246
|
+
return verified.version;
|
|
1138
1247
|
}
|
|
1139
|
-
this.stage(
|
|
1248
|
+
this.stage(
|
|
1249
|
+
"check-cli",
|
|
1250
|
+
"succeeded",
|
|
1251
|
+
await fileExists(this.paths.cliPath) ? {
|
|
1252
|
+
detail: `The VS Code CLI on this machine is not the verified ${PINNED_CLI_VERSION} build; downloading that build to replace it.`
|
|
1253
|
+
} : {}
|
|
1254
|
+
);
|
|
1140
1255
|
return this.stepDownloadCli(this.paths.cliPath);
|
|
1141
1256
|
}
|
|
1142
|
-
|
|
1257
|
+
/** 下载、校验并装上钉死的版本:`stagingPath` 与 `cliPath` 不同时校验通过后才改名换上。 */
|
|
1258
|
+
async stepDownloadCli(stagingPath) {
|
|
1143
1259
|
this.stage("download-cli", "started");
|
|
1144
1260
|
try {
|
|
1145
|
-
await
|
|
1261
|
+
await clearVerifiedCli(this.paths);
|
|
1262
|
+
const install = await downloadCliBinary({
|
|
1146
1263
|
http: this.ctx.http,
|
|
1147
1264
|
paths: this.paths,
|
|
1148
|
-
destPath,
|
|
1265
|
+
destPath: stagingPath,
|
|
1149
1266
|
onTransfer: (transfer) => this.stage("download-cli", "progress", { transfer })
|
|
1150
1267
|
});
|
|
1268
|
+
if (stagingPath !== this.paths.cliPath) await NodeFSP5.rename(stagingPath, this.paths.cliPath);
|
|
1269
|
+
await writeVerifiedCli(this.paths, install);
|
|
1151
1270
|
} catch (error) {
|
|
1271
|
+
if (stagingPath !== this.paths.cliPath) {
|
|
1272
|
+
await NodeFSP5.unlink(stagingPath).catch(() => void 0);
|
|
1273
|
+
}
|
|
1152
1274
|
this.rethrowAs(error, "download-cli");
|
|
1153
1275
|
}
|
|
1154
1276
|
try {
|
|
@@ -1162,15 +1284,14 @@ var WorkbenchRuntime = class {
|
|
|
1162
1284
|
async stepStartServer(cliVersion) {
|
|
1163
1285
|
this.stage("start-server", "started");
|
|
1164
1286
|
this.outputStage = "start-server";
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
return;
|
|
1287
|
+
const record = this.managed?.record ?? await readInstance(this.paths);
|
|
1288
|
+
if (record !== null) {
|
|
1289
|
+
const state = await this.settleRecord(record);
|
|
1290
|
+
if (state !== "forgotten") {
|
|
1291
|
+
if (this.managed === null) this.managed = { owned: false, record };
|
|
1292
|
+
this.stage("start-server", "skipped", { skipReason: "already-running" });
|
|
1293
|
+
return;
|
|
1294
|
+
}
|
|
1174
1295
|
}
|
|
1175
1296
|
try {
|
|
1176
1297
|
this.managed = await startServeWeb({
|
|
@@ -1189,6 +1310,39 @@ var WorkbenchRuntime = class {
|
|
|
1189
1310
|
detail: `127.0.0.1:${String(started.record.port)}`
|
|
1190
1311
|
});
|
|
1191
1312
|
}
|
|
1313
|
+
/**
|
|
1314
|
+
* 判定一条记录能不能接管。ours / pending 原样返回;foreign / stale 与等满时限仍答不出的
|
|
1315
|
+
* 记录一律忘掉(删记录;若是本次自己 spawn 的实例,先停它自己的句柄),返回 "forgotten"。
|
|
1316
|
+
* 不依赖记录里的 pid,也不对任何别的进程发信号。
|
|
1317
|
+
*/
|
|
1318
|
+
async settleRecord(record) {
|
|
1319
|
+
let state = await classifyRecord(record, this.paths);
|
|
1320
|
+
if (state === "unresponsive") {
|
|
1321
|
+
this.stage("start-server", "output", {
|
|
1322
|
+
detail: "Waiting for the previous workbench server to answer..."
|
|
1323
|
+
});
|
|
1324
|
+
const deadline = Date.now() + this.unresponsiveGraceMs;
|
|
1325
|
+
const pollMs = Math.min(UNRESPONSIVE_POLL_MS, Math.max(1, this.unresponsiveGraceMs / 4));
|
|
1326
|
+
while (state === "unresponsive" && Date.now() < deadline) {
|
|
1327
|
+
await sleep(pollMs);
|
|
1328
|
+
state = await classifyRecord(record, this.paths);
|
|
1329
|
+
}
|
|
1330
|
+
}
|
|
1331
|
+
if (state === "ours" || state === "pending") return state;
|
|
1332
|
+
await this.forgetRecord(record, state);
|
|
1333
|
+
return "forgotten";
|
|
1334
|
+
}
|
|
1335
|
+
async forgetRecord(record, state) {
|
|
1336
|
+
this.ctx.log.warn("forgetting a recorded workbench instance", { port: record.port, state });
|
|
1337
|
+
if (this.managedPort !== null) {
|
|
1338
|
+
await this.managedPort.unregister().catch(() => void 0);
|
|
1339
|
+
this.managedPort = null;
|
|
1340
|
+
}
|
|
1341
|
+
const managed = this.managed;
|
|
1342
|
+
this.managed = null;
|
|
1343
|
+
if (managed?.owned === true) await stopOwnedHandle(managed.handle);
|
|
1344
|
+
await removeFile(this.paths.instancePath);
|
|
1345
|
+
}
|
|
1192
1346
|
onProcessLine(line) {
|
|
1193
1347
|
if (line.trim().length === 0) return;
|
|
1194
1348
|
const stage = isServerLogLine(line) ? "download-server" : this.outputStage;
|
|
@@ -1208,8 +1362,19 @@ var WorkbenchRuntime = class {
|
|
|
1208
1362
|
onTransfer: (transfer) => this.stage("download-server", "progress", { transfer })
|
|
1209
1363
|
});
|
|
1210
1364
|
} catch (error) {
|
|
1365
|
+
if (!managed.owned) {
|
|
1366
|
+
const state = await classifyRecord(managed.record, this.paths);
|
|
1367
|
+
if (state !== "ours" && state !== "pending") await this.forgetRecord(managed.record, state);
|
|
1368
|
+
}
|
|
1211
1369
|
this.rethrowAs(error, "download-server", "server-download-failed");
|
|
1212
1370
|
}
|
|
1371
|
+
if (!managed.owned) {
|
|
1372
|
+
const state = await classifyRecord(managed.record, this.paths);
|
|
1373
|
+
if (state !== "ours") {
|
|
1374
|
+
await this.forgetRecord(managed.record, state);
|
|
1375
|
+
this.fail("download-server", "unknown", catalog.adopt.notOurs);
|
|
1376
|
+
}
|
|
1377
|
+
}
|
|
1213
1378
|
this.stage("download-server", "succeeded");
|
|
1214
1379
|
}
|
|
1215
1380
|
async stepRegisterManaged() {
|
|
@@ -1234,27 +1399,20 @@ var WorkbenchRuntime = class {
|
|
|
1234
1399
|
this.stage("register-port", "succeeded");
|
|
1235
1400
|
return this.managedPort.portId;
|
|
1236
1401
|
}
|
|
1402
|
+
/**
|
|
1403
|
+
* 「更新」装的是插件钉死的版本,不是官方最新版:已装且校验通过就什么都不下;
|
|
1404
|
+
* 否则下到 `bin/code.new`,校验通过后才换上,失败时原来的二进制不动。
|
|
1405
|
+
*/
|
|
1237
1406
|
async runUpdate() {
|
|
1238
1407
|
if (await this.managedIsLive()) {
|
|
1239
1408
|
throw new WorkbenchFailure("unknown", catalog.running.stopFirst);
|
|
1240
1409
|
}
|
|
1241
|
-
this.
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
http: this.ctx.http,
|
|
1246
|
-
paths: this.paths,
|
|
1247
|
-
destPath,
|
|
1248
|
-
onTransfer: (transfer) => this.stage("download-cli", "progress", { transfer })
|
|
1249
|
-
});
|
|
1250
|
-
await NodeFSP5.rename(destPath, this.paths.cliPath);
|
|
1251
|
-
} catch (error) {
|
|
1252
|
-
await NodeFSP5.unlink(destPath).catch(() => void 0);
|
|
1253
|
-
this.rethrowAs(error, "download-cli");
|
|
1410
|
+
const verified = await readVerifiedCli(this.paths, { checkBinary: true });
|
|
1411
|
+
if (verified !== null) {
|
|
1412
|
+
this.stage("download-cli", "skipped", { skipReason: "already-installed" });
|
|
1413
|
+
return { cliVersion: verified.version };
|
|
1254
1414
|
}
|
|
1255
|
-
this.
|
|
1256
|
-
const version = await readInstalledCliVersion(this.ctx.process);
|
|
1257
|
-
return { cliVersion: version };
|
|
1415
|
+
return { cliVersion: await this.stepDownloadCli(`${this.paths.cliPath}${CLI_NEW_SUFFIX}`) };
|
|
1258
1416
|
}
|
|
1259
1417
|
async attachPort(port) {
|
|
1260
1418
|
if (!await probeLoopback(port)) {
|
|
@@ -1278,21 +1436,32 @@ var WorkbenchRuntime = class {
|
|
|
1278
1436
|
this.externalPort = null;
|
|
1279
1437
|
this.externalListenPort = null;
|
|
1280
1438
|
}
|
|
1281
|
-
|
|
1439
|
+
/**
|
|
1440
|
+
* 停托管实例。只停本次运行自己 spawn 的句柄;记录里的 pid 不对它发信号:跨过一次重启
|
|
1441
|
+
* 它可能已被系统重用,令牌验的是端口上的服务,不是那个 pid。
|
|
1442
|
+
* - "keep"(插件停用、补丁后的内部重启):保留仍在服务的记录,下次激活或紧接着的
|
|
1443
|
+
* start-server 可以接上;
|
|
1444
|
+
* - "stop"(用户 Stop):句柄停完后记录仍在,说明端口上有个无法证明能安全停的服务
|
|
1445
|
+
* (领养来的,或孙进程脱离了句柄)。放弃接管:删记录、状态不再显示在跑,把端口报给
|
|
1446
|
+
* 调用方。否则 Stop 停不掉、Update 又要求先 Stop,形成单向门。
|
|
1447
|
+
*/
|
|
1448
|
+
async stopManaged(mode) {
|
|
1282
1449
|
if (this.managedPort !== null) {
|
|
1283
1450
|
await this.managedPort.unregister().catch(() => void 0);
|
|
1284
1451
|
this.managedPort = null;
|
|
1285
1452
|
}
|
|
1286
|
-
|
|
1453
|
+
const managed = this.managed;
|
|
1287
1454
|
this.managed = null;
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
}
|
|
1294
|
-
}
|
|
1455
|
+
if (managed?.owned === true) await stopOwnedHandle(managed.handle);
|
|
1456
|
+
await reconcileInstanceRecord(this.paths, this.ctx.log);
|
|
1457
|
+
if (mode === "keep") return {};
|
|
1458
|
+
const record = await readInstance(this.paths);
|
|
1459
|
+
if (record === null) return {};
|
|
1295
1460
|
await removeFile(this.paths.instancePath);
|
|
1461
|
+
this.ctx.log.warn("released a workbench server it cannot stop; it may still be running", {
|
|
1462
|
+
port: record.port
|
|
1463
|
+
});
|
|
1464
|
+
return { released: { port: record.port } };
|
|
1296
1465
|
}
|
|
1297
1466
|
rethrowAs(error, stage, fallback = "download-failed") {
|
|
1298
1467
|
if (error instanceof WorkbenchFailure) {
|
|
@@ -1306,19 +1475,6 @@ var capabilityReason = (error) => {
|
|
|
1306
1475
|
const reason = error.reason;
|
|
1307
1476
|
return typeof reason === "string" ? reason : null;
|
|
1308
1477
|
};
|
|
1309
|
-
var orphanHandle = (pid) => ({
|
|
1310
|
-
pid,
|
|
1311
|
-
stdout: emptyBytes(),
|
|
1312
|
-
stderr: emptyBytes(),
|
|
1313
|
-
writeStdin: async () => void 0,
|
|
1314
|
-
closeStdin: async () => void 0,
|
|
1315
|
-
kill: async (signal) => {
|
|
1316
|
-
if (isProcessAlive(pid)) process.kill(pid, signal ?? "SIGTERM");
|
|
1317
|
-
},
|
|
1318
|
-
exited: Promise.resolve(null)
|
|
1319
|
-
});
|
|
1320
|
-
async function* emptyBytes() {
|
|
1321
|
-
}
|
|
1322
1478
|
|
|
1323
1479
|
// src/schemas.ts
|
|
1324
1480
|
var emptyObject = {
|
|
@@ -1389,6 +1545,20 @@ var statusResultSchema = {
|
|
|
1389
1545
|
}
|
|
1390
1546
|
}
|
|
1391
1547
|
};
|
|
1548
|
+
var stopResultSchema = {
|
|
1549
|
+
type: "object",
|
|
1550
|
+
additionalProperties: false,
|
|
1551
|
+
properties: {
|
|
1552
|
+
released: {
|
|
1553
|
+
type: "object",
|
|
1554
|
+
additionalProperties: false,
|
|
1555
|
+
required: ["port"],
|
|
1556
|
+
properties: {
|
|
1557
|
+
port: { type: "integer", minimum: 1, maximum: 65535 }
|
|
1558
|
+
}
|
|
1559
|
+
}
|
|
1560
|
+
}
|
|
1561
|
+
};
|
|
1392
1562
|
var updateResultSchema = {
|
|
1393
1563
|
type: "object",
|
|
1394
1564
|
additionalProperties: false,
|
|
@@ -1433,7 +1603,7 @@ var progressItemSchema = {
|
|
|
1433
1603
|
// src/index.ts
|
|
1434
1604
|
var index_default = definePlugin({
|
|
1435
1605
|
id: "browser-workbench",
|
|
1436
|
-
apiVersion:
|
|
1606
|
+
apiVersion: 3,
|
|
1437
1607
|
activate: (ctx) => {
|
|
1438
1608
|
const runtime = new WorkbenchRuntime(ctx);
|
|
1439
1609
|
void runtime.start().catch((error) => {
|
|
@@ -1489,10 +1659,10 @@ var index_default = definePlugin({
|
|
|
1489
1659
|
});
|
|
1490
1660
|
ctx.command({
|
|
1491
1661
|
name: "stop",
|
|
1492
|
-
description: "Stop the managed VS Code CLI and unregister its port.",
|
|
1662
|
+
description: "Stop the managed VS Code CLI and unregister its port. A server this run did not start is released, not killed; the result then carries its port.",
|
|
1493
1663
|
scope: "operate",
|
|
1494
1664
|
payloadSchema: emptyPayloadSchema,
|
|
1495
|
-
resultSchema:
|
|
1665
|
+
resultSchema: stopResultSchema,
|
|
1496
1666
|
handle: () => runtime.stop()
|
|
1497
1667
|
});
|
|
1498
1668
|
ctx.command({
|