@devframes/plugin-code-server 0.9.8 → 0.9.10
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 +10 -10
- package/bin.mjs +1 -1
- package/dist/{constants-DtQz38ex.d.mts → constants-DTS2klFC.d.mts} +2 -2
- package/dist/{cli.d.mts → node/cli.d.mts} +3 -3
- package/dist/{cli.mjs → node/cli.mjs} +4 -4
- package/dist/{constants.d.mts → node/constants.d.mts} +1 -1
- package/dist/{constants.mjs → node/constants.mjs} +2 -2
- package/dist/node/index.d.mts +15 -473
- package/dist/node/index.mjs +3 -761
- package/dist/{rpc → node/rpc}/index.d.mts +1 -1
- package/dist/node/rpc/index.mjs +2 -0
- package/dist/node/setup.d.mts +501 -0
- package/dist/node/setup.mjs +801 -0
- package/dist/{types.d.mts → node/types.d.mts} +10 -10
- package/dist/{vite.d.mts → node/vite.d.mts} +1 -1
- package/dist/{vite.mjs → node/vite.mjs} +2 -2
- package/dist/{src-CNJYQ3UQ.mjs → node-C_wU5QYh.mjs} +20 -5
- package/dist/{rpc-EXj5vBYf.mjs → rpc-Df9IRgPd.mjs} +3 -3
- package/package.json +22 -18
- package/dist/client/index.d.mts +0 -13
- package/dist/client/index.mjs +0 -21
- package/dist/index.d.mts +0 -23
- package/dist/index.mjs +0 -3
- package/dist/rpc/index.mjs +0 -2
- /package/dist/{types.mjs → node/types.mjs} +0 -0
package/dist/node/index.mjs
CHANGED
|
@@ -1,761 +1,3 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
import { createHash, randomBytes } from "node:crypto";
|
|
5
|
-
import { request } from "node:http";
|
|
6
|
-
import { hostname } from "node:os";
|
|
7
|
-
import process from "node:process";
|
|
8
|
-
import { getPort } from "devframe/utils/get-port";
|
|
9
|
-
//#region src/node/backends.ts
|
|
10
|
-
const sha256 = (value) => createHash("sha256").update(value).digest("hex");
|
|
11
|
-
/** Coder's open-source `code-server`, with password auth + session cookie handoff. */
|
|
12
|
-
const codeServerProfile = {
|
|
13
|
-
kind: "code-server",
|
|
14
|
-
backend: "code-server",
|
|
15
|
-
defaultBin: "code-server",
|
|
16
|
-
buildArgs: (c) => [
|
|
17
|
-
"--auth",
|
|
18
|
-
"password",
|
|
19
|
-
"--bind-addr",
|
|
20
|
-
`${c.host}:${c.port}`,
|
|
21
|
-
"--disable-telemetry",
|
|
22
|
-
"--disable-update-check",
|
|
23
|
-
...c.extraArgs,
|
|
24
|
-
c.folder
|
|
25
|
-
],
|
|
26
|
-
buildEnv: (c, base) => {
|
|
27
|
-
const env = { ...base };
|
|
28
|
-
delete env.PASSWORD;
|
|
29
|
-
env.HASHED_PASSWORD = sha256(c.secret);
|
|
30
|
-
return env;
|
|
31
|
-
},
|
|
32
|
-
healthPath: "/healthz",
|
|
33
|
-
matchPort: (line) => {
|
|
34
|
-
const m = line.match(/HTTP server listening on https?:\/\/(?:[^:]+|\[[^\]]+\]):(\d+)/);
|
|
35
|
-
return m ? Number.parseInt(m[1], 10) : void 0;
|
|
36
|
-
},
|
|
37
|
-
connect: (c) => ({
|
|
38
|
-
path: "/",
|
|
39
|
-
cookie: {
|
|
40
|
-
name: c.cookieName,
|
|
41
|
-
value: sha256(c.secret)
|
|
42
|
-
}
|
|
43
|
-
}),
|
|
44
|
-
connectReused: () => ({ path: "/" })
|
|
45
|
-
};
|
|
46
|
-
/** Microsoft's `code serve-web`, with a connection token passed via `?tkn=`. */
|
|
47
|
-
const serveWebProfile = {
|
|
48
|
-
kind: "serve-web",
|
|
49
|
-
backend: "ms-code-serve-web",
|
|
50
|
-
defaultBin: "code",
|
|
51
|
-
buildArgs: (c) => [
|
|
52
|
-
"serve-web",
|
|
53
|
-
"--accept-server-license-terms",
|
|
54
|
-
"--host",
|
|
55
|
-
c.host,
|
|
56
|
-
"--port",
|
|
57
|
-
String(c.port),
|
|
58
|
-
"--connection-token",
|
|
59
|
-
c.secret,
|
|
60
|
-
...c.extraArgs
|
|
61
|
-
],
|
|
62
|
-
buildEnv: (_c, base) => ({ ...base }),
|
|
63
|
-
healthPath: "/",
|
|
64
|
-
matchPort: (line) => {
|
|
65
|
-
const m = line.match(/available at https?:\/\/[^:]+:(\d+)/i);
|
|
66
|
-
return m ? Number.parseInt(m[1], 10) : void 0;
|
|
67
|
-
},
|
|
68
|
-
connect: (c) => ({ path: `/?tkn=${encodeURIComponent(c.secret)}&folder=${encodeURIComponent(c.folder)}` }),
|
|
69
|
-
connectReused: () => ({ path: "/" })
|
|
70
|
-
};
|
|
71
|
-
const TUNNEL_URL_RE = /(https:\/\/vscode\.dev\/tunnel\/\S+)/;
|
|
72
|
-
const DEVICE_LOGIN_RE = /log in(?:to)? (https?:\/\/\S+) and use code ([A-Z0-9-]+)/i;
|
|
73
|
-
/** Microsoft's `code tunnel`, embedding the hosted `vscode.dev` editor. */
|
|
74
|
-
const tunnelProfile = {
|
|
75
|
-
kind: "tunnel",
|
|
76
|
-
backend: "ms-code-serve-web",
|
|
77
|
-
defaultBin: "code",
|
|
78
|
-
buildArgs: (c) => [
|
|
79
|
-
"tunnel",
|
|
80
|
-
"--accept-server-license-terms",
|
|
81
|
-
"--name",
|
|
82
|
-
c.tunnelName,
|
|
83
|
-
...c.extraArgs
|
|
84
|
-
],
|
|
85
|
-
buildEnv: (_c, base) => ({ ...base }),
|
|
86
|
-
healthPath: null,
|
|
87
|
-
matchReadyUrl: (line) => {
|
|
88
|
-
const m = line.match(TUNNEL_URL_RE);
|
|
89
|
-
return m ? m[1].replace(/[)\].,]+$/, "") : void 0;
|
|
90
|
-
},
|
|
91
|
-
matchLogin: (line) => {
|
|
92
|
-
const m = line.match(DEVICE_LOGIN_RE);
|
|
93
|
-
return m ? {
|
|
94
|
-
url: m[1],
|
|
95
|
-
code: m[2]
|
|
96
|
-
} : void 0;
|
|
97
|
-
},
|
|
98
|
-
connect: (c) => ({ url: c.readyUrl ?? `https://vscode.dev/tunnel/${c.tunnelName}${c.folder}` }),
|
|
99
|
-
connectReused: () => ({})
|
|
100
|
-
};
|
|
101
|
-
const LOCAL_PROFILES = {
|
|
102
|
-
"code-server": codeServerProfile,
|
|
103
|
-
"ms-code-serve-web": serveWebProfile
|
|
104
|
-
};
|
|
105
|
-
/** The ordered auto-detection candidates for `mode: 'local'` with no explicit backend. */
|
|
106
|
-
const AUTO_DETECT_ORDER = ["code-server", "ms-code-serve-web"];
|
|
107
|
-
/** Resolve the launch profile for a mode + backend. */
|
|
108
|
-
function resolveProfile(mode, backend) {
|
|
109
|
-
return mode === "tunnel" ? tunnelProfile : LOCAL_PROFILES[backend];
|
|
110
|
-
}
|
|
111
|
-
//#endregion
|
|
112
|
-
//#region src/node/detect.ts
|
|
113
|
-
/**
|
|
114
|
-
* Probe the host for a usable code-server binary by running
|
|
115
|
-
* `<bin> --version`. Resolves to `installed: false` when the binary is
|
|
116
|
-
* missing (ENOENT), errors, or exits non-zero — never throws — so the
|
|
117
|
-
* launcher can fall back to install instructions.
|
|
118
|
-
*
|
|
119
|
-
* `code-server --version` prints e.g. `4.96.4 abc123 with Code 1.96.4`; the
|
|
120
|
-
* first semver-looking token is taken as the version. Matching a semver
|
|
121
|
-
* pattern (rather than the leading whitespace token) keeps cold-start i18n
|
|
122
|
-
* noise — e.g. an `i18next: …` initialization line printed before the version
|
|
123
|
-
* — from leaking into the reported version.
|
|
124
|
-
*/
|
|
125
|
-
function detectCodeServer(bin = "code-server", timeoutMs = 5e3) {
|
|
126
|
-
return new Promise((resolve) => {
|
|
127
|
-
let settled = false;
|
|
128
|
-
let stdout = "";
|
|
129
|
-
const finish = (result) => {
|
|
130
|
-
if (settled) return;
|
|
131
|
-
settled = true;
|
|
132
|
-
resolve(result);
|
|
133
|
-
};
|
|
134
|
-
let child;
|
|
135
|
-
try {
|
|
136
|
-
child = spawn(bin, ["--version"], {
|
|
137
|
-
windowsHide: true,
|
|
138
|
-
stdio: [
|
|
139
|
-
"ignore",
|
|
140
|
-
"pipe",
|
|
141
|
-
"ignore"
|
|
142
|
-
],
|
|
143
|
-
shell: process.platform === "win32" && bin.endsWith(".cmd")
|
|
144
|
-
});
|
|
145
|
-
} catch {
|
|
146
|
-
finish({
|
|
147
|
-
installed: false,
|
|
148
|
-
bin
|
|
149
|
-
});
|
|
150
|
-
return;
|
|
151
|
-
}
|
|
152
|
-
const timer = setTimeout(() => {
|
|
153
|
-
child.kill();
|
|
154
|
-
finish({
|
|
155
|
-
installed: false,
|
|
156
|
-
bin
|
|
157
|
-
});
|
|
158
|
-
}, timeoutMs);
|
|
159
|
-
timer.unref?.();
|
|
160
|
-
child.stdout?.on("data", (chunk) => {
|
|
161
|
-
stdout += chunk.toString("utf8");
|
|
162
|
-
});
|
|
163
|
-
child.on("error", () => {
|
|
164
|
-
clearTimeout(timer);
|
|
165
|
-
finish({
|
|
166
|
-
installed: false,
|
|
167
|
-
bin
|
|
168
|
-
});
|
|
169
|
-
});
|
|
170
|
-
child.on("exit", (code) => {
|
|
171
|
-
clearTimeout(timer);
|
|
172
|
-
if (code === 0) {
|
|
173
|
-
const version = stdout.match(/\d+\.\d+\.\d\S*/)?.[0] ?? (stdout.trim().split(/\s+/)[0] || void 0);
|
|
174
|
-
finish({
|
|
175
|
-
installed: true,
|
|
176
|
-
version,
|
|
177
|
-
bin
|
|
178
|
-
});
|
|
179
|
-
} else finish({
|
|
180
|
-
installed: false,
|
|
181
|
-
bin
|
|
182
|
-
});
|
|
183
|
-
});
|
|
184
|
-
});
|
|
185
|
-
}
|
|
186
|
-
//#endregion
|
|
187
|
-
//#region src/node/supervisor.ts
|
|
188
|
-
/** Recent output lines retained for surfacing startup failures. */
|
|
189
|
-
const LOG_BUFFER_LINES = 200;
|
|
190
|
-
/** Interval between readiness probes. */
|
|
191
|
-
const PROBE_INTERVAL = 250;
|
|
192
|
-
/**
|
|
193
|
-
* Owns the lifecycle of a single editor child process. Resolves a launch
|
|
194
|
-
* {@link CodeServerProfile} (Coder `code-server`, Microsoft `code serve-web`,
|
|
195
|
-
* or `code tunnel`), detects the binary, launches it with freshly generated
|
|
196
|
-
* auth material, waits for readiness, and mirrors a secret-free status into
|
|
197
|
-
* shared state. The connect descriptor (session cookie / connection token /
|
|
198
|
-
* tunnel URL) is handed back only through `start()` / `status()` so the
|
|
199
|
-
* already-authorized client can open the editor without a login page.
|
|
200
|
-
*
|
|
201
|
-
* Depends only on the core devframe context (shared state), not on the hub.
|
|
202
|
-
*/
|
|
203
|
-
var CodeServerSupervisor = class {
|
|
204
|
-
ctx;
|
|
205
|
-
mode;
|
|
206
|
-
explicitBackend;
|
|
207
|
-
explicitBin;
|
|
208
|
-
workspace;
|
|
209
|
-
host;
|
|
210
|
-
forcedPort;
|
|
211
|
-
extraArgs;
|
|
212
|
-
extraEnv;
|
|
213
|
-
cookieSuffix;
|
|
214
|
-
cookieName;
|
|
215
|
-
startTimeout;
|
|
216
|
-
reuseExistingServer;
|
|
217
|
-
tunnelName;
|
|
218
|
-
/** Resolved after the first detection. */
|
|
219
|
-
backend;
|
|
220
|
-
bin;
|
|
221
|
-
profile;
|
|
222
|
-
state;
|
|
223
|
-
detection;
|
|
224
|
-
server = { status: "stopped" };
|
|
225
|
-
proc;
|
|
226
|
-
/** Context of the live launch, used to compute the client connect descriptor. */
|
|
227
|
-
launchCtx;
|
|
228
|
-
/** Whether the running server was adopted (reused) rather than launched. */
|
|
229
|
-
adopted = false;
|
|
230
|
-
/** Captured `vscode.dev` URL for a running tunnel. */
|
|
231
|
-
readyUrl;
|
|
232
|
-
logBuffer = [];
|
|
233
|
-
exitHandler;
|
|
234
|
-
/** Stable id of the hub terminal session, reused across start/stop. */
|
|
235
|
-
sessionId;
|
|
236
|
-
/** The live hub terminal session when launched through `ctx.terminals`. */
|
|
237
|
-
session;
|
|
238
|
-
constructor(ctx, options = {}) {
|
|
239
|
-
this.ctx = ctx;
|
|
240
|
-
this.mode = options.mode ?? "local";
|
|
241
|
-
this.explicitBackend = options.backend;
|
|
242
|
-
this.explicitBin = options.bin;
|
|
243
|
-
this.workspace = options.cwd ?? ctx.cwd;
|
|
244
|
-
this.host = options.host ?? "127.0.0.1";
|
|
245
|
-
this.forcedPort = options.serverPort;
|
|
246
|
-
this.extraArgs = options.args ?? [];
|
|
247
|
-
this.extraEnv = options.env ?? {};
|
|
248
|
-
this.cookieSuffix = options.cookieSuffix;
|
|
249
|
-
this.cookieName = getCookieSessionName(options.cookieSuffix);
|
|
250
|
-
this.startTimeout = options.startTimeout ?? 3e4;
|
|
251
|
-
this.reuseExistingServer = options.reuseExistingServer ?? false;
|
|
252
|
-
this.tunnelName = options.tunnel?.name || hostname().split(".").join("") || "devframe";
|
|
253
|
-
this.sessionId = options.cookieSuffix ? `${PLUGIN_ID}:${options.cookieSuffix}` : PLUGIN_ID;
|
|
254
|
-
this.backend = this.explicitBackend ?? "code-server";
|
|
255
|
-
this.profile = resolveProfile(this.mode, this.backend);
|
|
256
|
-
this.bin = this.explicitBin ?? this.profile.defaultBin;
|
|
257
|
-
this.detection = {
|
|
258
|
-
checked: false,
|
|
259
|
-
installed: false,
|
|
260
|
-
bin: this.bin,
|
|
261
|
-
backend: this.backend,
|
|
262
|
-
mode: this.mode
|
|
263
|
-
};
|
|
264
|
-
}
|
|
265
|
-
/** Resolve shared state, register process-exit cleanup, run first detection. */
|
|
266
|
-
async init() {
|
|
267
|
-
if (this.state) return;
|
|
268
|
-
this.state = await this.ctx.rpc.sharedState.get(STATE_KEY, { initialValue: {
|
|
269
|
-
detection: this.detection,
|
|
270
|
-
server: this.server
|
|
271
|
-
} });
|
|
272
|
-
await this.detect();
|
|
273
|
-
}
|
|
274
|
-
/**
|
|
275
|
-
* Probe for a usable editor binary and publish the result. Resolves the
|
|
276
|
-
* backend + binary when the caller left them implicit: tunnel mode always
|
|
277
|
-
* uses `code`; an explicit backend or `bin` is honored as-is; otherwise the
|
|
278
|
-
* plugin tries each {@link AUTO_DETECT_ORDER} candidate and keeps the first
|
|
279
|
-
* that is installed.
|
|
280
|
-
*/
|
|
281
|
-
async detect() {
|
|
282
|
-
let installed = false;
|
|
283
|
-
let version;
|
|
284
|
-
if (this.mode === "tunnel" || this.explicitBackend || this.explicitBin) {
|
|
285
|
-
const result = await detectCodeServer(this.bin);
|
|
286
|
-
installed = result.installed;
|
|
287
|
-
version = result.version;
|
|
288
|
-
} else {
|
|
289
|
-
let resolved;
|
|
290
|
-
for (const backend of AUTO_DETECT_ORDER) {
|
|
291
|
-
const bin = resolveProfile("local", backend).defaultBin;
|
|
292
|
-
const result = await detectCodeServer(bin);
|
|
293
|
-
if (result.installed) {
|
|
294
|
-
resolved = {
|
|
295
|
-
backend,
|
|
296
|
-
bin,
|
|
297
|
-
version: result.version
|
|
298
|
-
};
|
|
299
|
-
break;
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
|
-
if (resolved) {
|
|
303
|
-
this.setResolved(resolved.backend, resolved.bin);
|
|
304
|
-
installed = true;
|
|
305
|
-
version = resolved.version;
|
|
306
|
-
}
|
|
307
|
-
}
|
|
308
|
-
this.detection = {
|
|
309
|
-
checked: true,
|
|
310
|
-
installed,
|
|
311
|
-
version,
|
|
312
|
-
bin: this.bin,
|
|
313
|
-
backend: this.backend,
|
|
314
|
-
mode: this.mode
|
|
315
|
-
};
|
|
316
|
-
this.publish();
|
|
317
|
-
return this.detection;
|
|
318
|
-
}
|
|
319
|
-
/** Current status (+ connect info when running) for the launcher UI. */
|
|
320
|
-
status() {
|
|
321
|
-
return {
|
|
322
|
-
detection: { ...this.detection },
|
|
323
|
-
server: { ...this.server },
|
|
324
|
-
connect: this.connectInfo()
|
|
325
|
-
};
|
|
326
|
-
}
|
|
327
|
-
/**
|
|
328
|
-
* Launch the editor (if not already up) and resolve once it is reachable.
|
|
329
|
-
* Idempotent while starting/running — returns the live status instead of
|
|
330
|
-
* spawning a second process. In tunnel mode it resolves as soon as either
|
|
331
|
-
* the `vscode.dev` URL or a device-login prompt is seen, so the action never
|
|
332
|
-
* blocks on interactive authentication.
|
|
333
|
-
*/
|
|
334
|
-
async start(req = {}) {
|
|
335
|
-
if (this.server.status === "running" || this.server.status === "starting") return this.status();
|
|
336
|
-
if (!this.detection.checked) await this.detect();
|
|
337
|
-
if (!this.detection.installed) throw diagnostics.DP_CODE_SERVER_0001({ bin: this.bin });
|
|
338
|
-
const folder = req.folder ?? this.workspace;
|
|
339
|
-
const isLocal = this.profile.kind !== "tunnel";
|
|
340
|
-
if (isLocal && this.reuseExistingServer) {
|
|
341
|
-
const target = this.forcedPort && this.forcedPort !== 0 ? this.forcedPort : DEFAULT_CODE_SERVER_PORT;
|
|
342
|
-
if (this.profile.healthPath && await probeHealth(target, this.profile.healthPath)) {
|
|
343
|
-
this.adopted = true;
|
|
344
|
-
this.launchCtx = this.baseCtx(folder, target, "");
|
|
345
|
-
this.server = {
|
|
346
|
-
status: "running",
|
|
347
|
-
port: target,
|
|
348
|
-
startedAt: Date.now()
|
|
349
|
-
};
|
|
350
|
-
this.publish();
|
|
351
|
-
return this.status();
|
|
352
|
-
}
|
|
353
|
-
}
|
|
354
|
-
const secret = randomBytes(32).toString("hex");
|
|
355
|
-
const initialPort = !isLocal ? 0 : this.forcedPort !== void 0 && this.forcedPort !== 0 ? this.forcedPort : this.forcedPort === 0 ? 0 : await getPort({
|
|
356
|
-
host: "127.0.0.1",
|
|
357
|
-
port: DEFAULT_CODE_SERVER_PORT
|
|
358
|
-
});
|
|
359
|
-
const ctx = this.baseCtx(folder, initialPort, secret);
|
|
360
|
-
this.launchCtx = ctx;
|
|
361
|
-
this.adopted = false;
|
|
362
|
-
this.readyUrl = void 0;
|
|
363
|
-
const args = this.profile.buildArgs(ctx);
|
|
364
|
-
const baseEnv = {};
|
|
365
|
-
for (const [k, v] of Object.entries(process.env)) if (v !== void 0) baseEnv[k] = v;
|
|
366
|
-
Object.assign(baseEnv, this.extraEnv);
|
|
367
|
-
const env = this.profile.buildEnv(ctx, baseEnv);
|
|
368
|
-
this.logBuffer = [];
|
|
369
|
-
let actualPort = initialPort;
|
|
370
|
-
let portResolver;
|
|
371
|
-
let portRejecter;
|
|
372
|
-
const portPromise = isLocal && initialPort === 0 ? new Promise((resolve, reject) => {
|
|
373
|
-
portResolver = resolve;
|
|
374
|
-
portRejecter = reject;
|
|
375
|
-
}) : Promise.resolve(initialPort);
|
|
376
|
-
let readyUrlResolver;
|
|
377
|
-
let loginResolver;
|
|
378
|
-
const readyUrlPromise = new Promise((resolve) => {
|
|
379
|
-
readyUrlResolver = resolve;
|
|
380
|
-
});
|
|
381
|
-
const loginPromise = new Promise((resolve) => {
|
|
382
|
-
loginResolver = resolve;
|
|
383
|
-
});
|
|
384
|
-
const child = await this.launchProcess(args, env, folder);
|
|
385
|
-
this.registerCleanup();
|
|
386
|
-
this.proc = child;
|
|
387
|
-
this.server = {
|
|
388
|
-
status: "starting",
|
|
389
|
-
port: isLocal ? actualPort || void 0 : void 0,
|
|
390
|
-
pid: child.pid,
|
|
391
|
-
startedAt: Date.now()
|
|
392
|
-
};
|
|
393
|
-
this.publish();
|
|
394
|
-
const capture = (chunk) => {
|
|
395
|
-
const text = chunk.toString("utf8");
|
|
396
|
-
this.appendLog(text);
|
|
397
|
-
for (const line of text.split("\n")) {
|
|
398
|
-
if (isLocal && actualPort === 0 && this.profile.matchPort) {
|
|
399
|
-
const port = this.profile.matchPort(line);
|
|
400
|
-
if (port) {
|
|
401
|
-
actualPort = port;
|
|
402
|
-
this.server.port = port;
|
|
403
|
-
this.publish();
|
|
404
|
-
portResolver?.(port);
|
|
405
|
-
portResolver = void 0;
|
|
406
|
-
}
|
|
407
|
-
}
|
|
408
|
-
if (this.profile.matchLogin) {
|
|
409
|
-
const login = this.profile.matchLogin(line);
|
|
410
|
-
if (login && this.proc === child) {
|
|
411
|
-
this.server = {
|
|
412
|
-
...this.server,
|
|
413
|
-
login
|
|
414
|
-
};
|
|
415
|
-
this.publish();
|
|
416
|
-
loginResolver?.(login);
|
|
417
|
-
}
|
|
418
|
-
}
|
|
419
|
-
if (this.profile.matchReadyUrl) {
|
|
420
|
-
const url = this.profile.matchReadyUrl(line);
|
|
421
|
-
if (url && this.proc === child) {
|
|
422
|
-
this.readyUrl = url;
|
|
423
|
-
readyUrlResolver?.(url);
|
|
424
|
-
}
|
|
425
|
-
}
|
|
426
|
-
}
|
|
427
|
-
};
|
|
428
|
-
child.stdout?.on("data", capture);
|
|
429
|
-
child.stderr?.on("data", capture);
|
|
430
|
-
child.on("error", (error) => {
|
|
431
|
-
if (this.proc !== child) return;
|
|
432
|
-
this.server = {
|
|
433
|
-
status: "error",
|
|
434
|
-
error: error.message
|
|
435
|
-
};
|
|
436
|
-
this.reset();
|
|
437
|
-
portRejecter?.(error);
|
|
438
|
-
this.publish();
|
|
439
|
-
});
|
|
440
|
-
child.on("exit", (code) => {
|
|
441
|
-
if (this.proc !== child) return;
|
|
442
|
-
const exitCode = code ?? 0;
|
|
443
|
-
const crashed = this.server.status !== "stopped" && exitCode !== 0;
|
|
444
|
-
this.reset();
|
|
445
|
-
this.server = crashed ? {
|
|
446
|
-
status: "error",
|
|
447
|
-
error: this.lastLog() || `${this.bin} exited with code ${exitCode}`
|
|
448
|
-
} : { status: "stopped" };
|
|
449
|
-
if (crashed) diagnostics.DP_CODE_SERVER_0005({ code: exitCode }, { method: "warn" });
|
|
450
|
-
this.reflectHub(crashed ? "error" : "stopped");
|
|
451
|
-
this.session = void 0;
|
|
452
|
-
portRejecter?.(/* @__PURE__ */ new Error(`${this.bin} exited before binding (code ${exitCode})`));
|
|
453
|
-
this.publish();
|
|
454
|
-
});
|
|
455
|
-
try {
|
|
456
|
-
if (!isLocal) return await this.awaitTunnel(child, readyUrlPromise, loginPromise);
|
|
457
|
-
const port = await Promise.race([portPromise, new Promise((_, reject) => setTimeout(() => reject(/* @__PURE__ */ new Error("timeout waiting for dynamic port allocation")), this.startTimeout))]);
|
|
458
|
-
if (!await this.waitForReady(port, this.profile.healthPath)) throw new Error(this.lastLog() || "startup timed out");
|
|
459
|
-
if (this.proc !== child) return this.status();
|
|
460
|
-
this.server = {
|
|
461
|
-
status: "running",
|
|
462
|
-
port,
|
|
463
|
-
pid: child.pid,
|
|
464
|
-
startedAt: this.server.startedAt
|
|
465
|
-
};
|
|
466
|
-
this.publish();
|
|
467
|
-
return this.status();
|
|
468
|
-
} catch (error) {
|
|
469
|
-
if (this.proc === child) {
|
|
470
|
-
this.terminate(child);
|
|
471
|
-
this.reflectHub("error");
|
|
472
|
-
this.session = void 0;
|
|
473
|
-
this.server = {
|
|
474
|
-
status: "error",
|
|
475
|
-
error: error instanceof Error ? error.message : String(error)
|
|
476
|
-
};
|
|
477
|
-
this.reset();
|
|
478
|
-
this.publish();
|
|
479
|
-
}
|
|
480
|
-
if (!isLocal) throw diagnostics.DP_CODE_SERVER_0006({ timeout: this.startTimeout });
|
|
481
|
-
throw diagnostics.DP_CODE_SERVER_0002({
|
|
482
|
-
port: actualPort,
|
|
483
|
-
timeout: this.startTimeout
|
|
484
|
-
});
|
|
485
|
-
}
|
|
486
|
-
}
|
|
487
|
-
/** Stop the editor process and reset to `stopped`. */
|
|
488
|
-
stop() {
|
|
489
|
-
const child = this.proc;
|
|
490
|
-
const wasAdopted = this.adopted;
|
|
491
|
-
this.reset();
|
|
492
|
-
this.server = { status: "stopped" };
|
|
493
|
-
if (child && !wasAdopted) this.terminate(child);
|
|
494
|
-
this.reflectHub("stopped");
|
|
495
|
-
this.session = void 0;
|
|
496
|
-
this.publish();
|
|
497
|
-
return this.status();
|
|
498
|
-
}
|
|
499
|
-
/** Kill the process on host shutdown / test teardown. */
|
|
500
|
-
dispose() {
|
|
501
|
-
if (this.proc && !this.adopted) this.terminate(this.proc);
|
|
502
|
-
this.reset();
|
|
503
|
-
this.session = void 0;
|
|
504
|
-
if (this.exitHandler) {
|
|
505
|
-
process.off("exit", this.exitHandler);
|
|
506
|
-
this.exitHandler = void 0;
|
|
507
|
-
}
|
|
508
|
-
}
|
|
509
|
-
/** Resolved backend for tests / callers. */
|
|
510
|
-
get resolvedBackend() {
|
|
511
|
-
return this.backend;
|
|
512
|
-
}
|
|
513
|
-
setResolved(backend, bin) {
|
|
514
|
-
this.backend = backend;
|
|
515
|
-
this.bin = bin;
|
|
516
|
-
this.profile = resolveProfile(this.mode, backend);
|
|
517
|
-
}
|
|
518
|
-
baseCtx(folder, port, secret) {
|
|
519
|
-
return {
|
|
520
|
-
host: this.host,
|
|
521
|
-
port,
|
|
522
|
-
folder,
|
|
523
|
-
secret,
|
|
524
|
-
cookieName: this.cookieName,
|
|
525
|
-
extraArgs: this.extraArgs,
|
|
526
|
-
tunnelName: this.tunnelName
|
|
527
|
-
};
|
|
528
|
-
}
|
|
529
|
-
/** Clear per-launch process state (keeps `server`/`detection`). */
|
|
530
|
-
reset() {
|
|
531
|
-
this.proc = void 0;
|
|
532
|
-
this.launchCtx = void 0;
|
|
533
|
-
this.adopted = false;
|
|
534
|
-
this.readyUrl = void 0;
|
|
535
|
-
}
|
|
536
|
-
/**
|
|
537
|
-
* Resolve start() for a tunnel: succeed as soon as the `vscode.dev` URL
|
|
538
|
-
* appears (→ running) or a device-login prompt is seen (→ starting, so the
|
|
539
|
-
* user can authenticate while the log stream continues to a running URL).
|
|
540
|
-
*/
|
|
541
|
-
async awaitTunnel(child, readyUrlPromise, loginPromise) {
|
|
542
|
-
const timeout = new Promise((_, reject) => setTimeout(() => reject(/* @__PURE__ */ new Error("timed out waiting for the tunnel")), this.startTimeout));
|
|
543
|
-
const outcome = await Promise.race([
|
|
544
|
-
readyUrlPromise.then(() => "ready"),
|
|
545
|
-
loginPromise.then(() => "login"),
|
|
546
|
-
timeout
|
|
547
|
-
]);
|
|
548
|
-
if (this.proc !== child) return this.status();
|
|
549
|
-
if (outcome === "ready") {
|
|
550
|
-
this.server = {
|
|
551
|
-
status: "running",
|
|
552
|
-
pid: child.pid,
|
|
553
|
-
startedAt: this.server.startedAt
|
|
554
|
-
};
|
|
555
|
-
this.publish();
|
|
556
|
-
return this.status();
|
|
557
|
-
}
|
|
558
|
-
readyUrlPromise.then(() => {
|
|
559
|
-
if (this.proc === child && this.server.status !== "running") {
|
|
560
|
-
this.server = {
|
|
561
|
-
status: "running",
|
|
562
|
-
pid: child.pid,
|
|
563
|
-
startedAt: this.server.startedAt,
|
|
564
|
-
login: void 0
|
|
565
|
-
};
|
|
566
|
-
this.publish();
|
|
567
|
-
}
|
|
568
|
-
});
|
|
569
|
-
return this.status();
|
|
570
|
-
}
|
|
571
|
-
connectInfo() {
|
|
572
|
-
if (this.server.status !== "running" || !this.launchCtx) return void 0;
|
|
573
|
-
if (this.adopted) return this.profile.connectReused({ port: this.launchCtx.port });
|
|
574
|
-
return this.profile.connect({
|
|
575
|
-
...this.launchCtx,
|
|
576
|
-
port: this.server.port ?? this.launchCtx.port,
|
|
577
|
-
readyUrl: this.readyUrl
|
|
578
|
-
});
|
|
579
|
-
}
|
|
580
|
-
terminate(child) {
|
|
581
|
-
if (this.session) {
|
|
582
|
-
this.session.terminate().catch(() => {});
|
|
583
|
-
return;
|
|
584
|
-
}
|
|
585
|
-
try {
|
|
586
|
-
if (process.platform === "win32" && this.bin.endsWith(".cmd") && child.pid) execSync(`taskkill /pid ${child.pid} /t /f`, { stdio: "ignore" });
|
|
587
|
-
else child.kill("SIGTERM");
|
|
588
|
-
} catch {}
|
|
589
|
-
}
|
|
590
|
-
/**
|
|
591
|
-
* Resolve the hub's terminals subsystem when this devframe is mounted in a
|
|
592
|
-
* hub. `ctx.terminals` only exists on a `DevframeHubContext`, so it is
|
|
593
|
-
* duck-typed — standalone runtimes (CLI / Vite / build) have no such property
|
|
594
|
-
* and fall back to a direct child process.
|
|
595
|
-
*/
|
|
596
|
-
resolveHubTerminals() {
|
|
597
|
-
const terminals = this.ctx.terminals;
|
|
598
|
-
return terminals && typeof terminals.startChildProcess === "function" ? terminals : void 0;
|
|
599
|
-
}
|
|
600
|
-
/** Update the mirrored hub terminal session's status, when one exists. */
|
|
601
|
-
reflectHub(status) {
|
|
602
|
-
const hub = this.resolveHubTerminals();
|
|
603
|
-
if (hub?.sessions.has(this.sessionId)) hub.update({
|
|
604
|
-
id: this.sessionId,
|
|
605
|
-
status
|
|
606
|
-
});
|
|
607
|
-
}
|
|
608
|
-
/**
|
|
609
|
-
* Launch the editor binary. In a hub, spawn it through `ctx.terminals` so it
|
|
610
|
-
* shows up as a read-only terminal session (proper icon + name) whose output
|
|
611
|
-
* the hub streams to its terminals panel; standalone, spawn it directly.
|
|
612
|
-
* Either way, return the underlying {@link ChildProcess} so the shared
|
|
613
|
-
* readiness / port / log wiring in `start()` is identical.
|
|
614
|
-
*/
|
|
615
|
-
async launchProcess(args, env, folder) {
|
|
616
|
-
const hub = this.resolveHubTerminals();
|
|
617
|
-
if (hub) {
|
|
618
|
-
const stale = hub.sessions.get(this.sessionId);
|
|
619
|
-
if (stale) hub.remove?.(stale);
|
|
620
|
-
try {
|
|
621
|
-
const session = await hub.startChildProcess({
|
|
622
|
-
command: this.bin,
|
|
623
|
-
args,
|
|
624
|
-
cwd: folder,
|
|
625
|
-
env: {
|
|
626
|
-
...env,
|
|
627
|
-
PASSWORD: ""
|
|
628
|
-
}
|
|
629
|
-
}, {
|
|
630
|
-
id: this.sessionId,
|
|
631
|
-
title: TERMINAL_SESSION_TITLE,
|
|
632
|
-
description: folder,
|
|
633
|
-
icon: TERMINAL_SESSION_ICON,
|
|
634
|
-
restartable: false
|
|
635
|
-
});
|
|
636
|
-
const child = session.getChildProcess();
|
|
637
|
-
if (!child) throw new Error("editor process handle was unavailable");
|
|
638
|
-
this.session = session;
|
|
639
|
-
return child;
|
|
640
|
-
} catch (error) {
|
|
641
|
-
const reason = error instanceof Error ? error.message : String(error);
|
|
642
|
-
const orphan = hub.sessions.get(this.sessionId);
|
|
643
|
-
if (orphan) hub.remove?.(orphan);
|
|
644
|
-
this.session = void 0;
|
|
645
|
-
this.server = {
|
|
646
|
-
status: "error",
|
|
647
|
-
error: reason
|
|
648
|
-
};
|
|
649
|
-
this.publish();
|
|
650
|
-
throw diagnostics.DP_CODE_SERVER_0003({
|
|
651
|
-
bin: this.bin,
|
|
652
|
-
reason
|
|
653
|
-
});
|
|
654
|
-
}
|
|
655
|
-
}
|
|
656
|
-
try {
|
|
657
|
-
return spawn(this.bin, args, {
|
|
658
|
-
cwd: folder,
|
|
659
|
-
env,
|
|
660
|
-
windowsHide: true,
|
|
661
|
-
stdio: [
|
|
662
|
-
"ignore",
|
|
663
|
-
"pipe",
|
|
664
|
-
"pipe"
|
|
665
|
-
],
|
|
666
|
-
shell: process.platform === "win32" && this.bin.endsWith(".cmd")
|
|
667
|
-
});
|
|
668
|
-
} catch (error) {
|
|
669
|
-
const reason = error instanceof Error ? error.message : String(error);
|
|
670
|
-
this.server = {
|
|
671
|
-
status: "error",
|
|
672
|
-
error: reason
|
|
673
|
-
};
|
|
674
|
-
this.publish();
|
|
675
|
-
throw diagnostics.DP_CODE_SERVER_0003({
|
|
676
|
-
bin: this.bin,
|
|
677
|
-
reason
|
|
678
|
-
});
|
|
679
|
-
}
|
|
680
|
-
}
|
|
681
|
-
appendLog(text) {
|
|
682
|
-
for (const line of text.split("\n")) {
|
|
683
|
-
if (line.length === 0) continue;
|
|
684
|
-
this.logBuffer.push(line);
|
|
685
|
-
}
|
|
686
|
-
if (this.logBuffer.length > LOG_BUFFER_LINES) this.logBuffer.splice(0, this.logBuffer.length - LOG_BUFFER_LINES);
|
|
687
|
-
}
|
|
688
|
-
lastLog() {
|
|
689
|
-
return this.logBuffer.length ? this.logBuffer[this.logBuffer.length - 1] : void 0;
|
|
690
|
-
}
|
|
691
|
-
publish() {
|
|
692
|
-
this.state?.mutate((draft) => {
|
|
693
|
-
draft.detection = { ...this.detection };
|
|
694
|
-
draft.server = { ...this.server };
|
|
695
|
-
});
|
|
696
|
-
}
|
|
697
|
-
/**
|
|
698
|
-
* Poll the server's readiness path until it responds or the timeout elapses.
|
|
699
|
-
* Returns false if the process exits first.
|
|
700
|
-
*/
|
|
701
|
-
async waitForReady(port, path) {
|
|
702
|
-
const deadline = Date.now() + this.startTimeout;
|
|
703
|
-
while (Date.now() < deadline) {
|
|
704
|
-
if (!this.proc) return false;
|
|
705
|
-
if (await probeHealth(port, path)) return true;
|
|
706
|
-
await delay(PROBE_INTERVAL);
|
|
707
|
-
}
|
|
708
|
-
return false;
|
|
709
|
-
}
|
|
710
|
-
registerCleanup() {
|
|
711
|
-
if (this.exitHandler) return;
|
|
712
|
-
this.exitHandler = () => this.dispose();
|
|
713
|
-
process.once("exit", this.exitHandler);
|
|
714
|
-
}
|
|
715
|
-
};
|
|
716
|
-
function delay(ms) {
|
|
717
|
-
return new Promise((resolve) => {
|
|
718
|
-
setTimeout(resolve, ms).unref?.();
|
|
719
|
-
});
|
|
720
|
-
}
|
|
721
|
-
function probeHealth(port, path) {
|
|
722
|
-
return new Promise((resolve) => {
|
|
723
|
-
const req = request({
|
|
724
|
-
host: "127.0.0.1",
|
|
725
|
-
port,
|
|
726
|
-
path,
|
|
727
|
-
method: "GET",
|
|
728
|
-
timeout: 1500
|
|
729
|
-
}, (res) => {
|
|
730
|
-
res.resume();
|
|
731
|
-
resolve((res.statusCode ?? 0) >= 200 && (res.statusCode ?? 0) < 500);
|
|
732
|
-
});
|
|
733
|
-
req.on("error", () => resolve(false));
|
|
734
|
-
req.on("timeout", () => {
|
|
735
|
-
req.destroy();
|
|
736
|
-
resolve(false);
|
|
737
|
-
});
|
|
738
|
-
req.end();
|
|
739
|
-
});
|
|
740
|
-
}
|
|
741
|
-
//#endregion
|
|
742
|
-
//#region src/node/index.ts
|
|
743
|
-
/**
|
|
744
|
-
* Wire the code-server subsystem onto a devframe node context: create the
|
|
745
|
-
* {@link CodeServerSupervisor}, run the initial binary detection, publish
|
|
746
|
-
* status into shared state, and register the control RPC functions. Returns
|
|
747
|
-
* the supervisor so callers can launch/stop or dispose it on shutdown.
|
|
748
|
-
*
|
|
749
|
-
* Works in any devframe runtime (CLI, Vite, embedded, build) — it only relies
|
|
750
|
-
* on the core `ctx.rpc` shared-state surface, not on the hub.
|
|
751
|
-
*/
|
|
752
|
-
async function setupCodeServer(ctx, options = {}) {
|
|
753
|
-
const supervisor = new CodeServerSupervisor(ctx, options);
|
|
754
|
-
setCodeServerSupervisor(ctx, supervisor);
|
|
755
|
-
await supervisor.init();
|
|
756
|
-
for (const fn of serverFunctions) ctx.rpc.register(fn);
|
|
757
|
-
if (options.startOnBoot) supervisor.start().catch(() => {});
|
|
758
|
-
return supervisor;
|
|
759
|
-
}
|
|
760
|
-
//#endregion
|
|
761
|
-
export { CodeServerSupervisor, detectCodeServer, diagnostics, getCodeServerSupervisor, resolveProfile, setCodeServerSupervisor, setupCodeServer };
|
|
1
|
+
import { t as createCodeServerDevframe } from "../node-C_wU5QYh.mjs";
|
|
2
|
+
import { DEFAULT_CODE_SERVER_PORT, DEFAULT_PORT, PLUGIN_ID, STATE_KEY, getCookieSessionName } from "./constants.mjs";
|
|
3
|
+
export { DEFAULT_CODE_SERVER_PORT, DEFAULT_PORT, PLUGIN_ID, STATE_KEY, createCodeServerDevframe, createCodeServerDevframe as default, getCookieSessionName };
|