dsh-code-server-app 0.1.42 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.en.md +113 -106
- package/README.md +125 -92
- package/cordis.patch.yml +19 -15
- package/lib/client.js +1 -3
- package/lib/index.js +247 -144
- package/lib/launcher.mjs +313 -0
- package/lib/native.js +44 -12
- package/lib/serve-dsh.mjs +162 -0
- package/lib/vendor.js +94 -33
- package/package.json +11 -7
- package/scripts/vendor-repacks.mjs +72 -99
- package/scripts/vendor-vscode-server.mjs +278 -0
- package/vendor/VENDOR.json +5 -2
package/lib/index.js
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* dsh-code-server — host 半部:
|
|
2
|
+
* dsh-code-server — host 半部:VS Code server 子进程(lib/launcher.mjs)的启动/停止/状态管理
|
|
3
|
+
* + /api/code-server JSON API。
|
|
4
|
+
*
|
|
5
|
+
* 模型(0.2.0 起):不再运行 code-server 的 Node 服务层 —— 直接由 lib/launcher.mjs 驱动
|
|
6
|
+
* <树>/lib/vscode/out/server-main.js(树 = @<scope>/dshcs-vscode-server 的 vscode/ 子目录)。
|
|
7
|
+
* 依据与实测见 docs/analysis-code-server-as-dsh-plugin.md。
|
|
3
8
|
*
|
|
4
9
|
* 零外部依赖:只用 Node 内置模块(child_process / http / fs / path / os)。
|
|
5
10
|
* 静态 profile 插件,与 dsh-webproxy-router-plugin 同形态:
|
|
@@ -42,13 +47,16 @@ import {
|
|
|
42
47
|
PACKAGE_ROOT,
|
|
43
48
|
codeServerEntry,
|
|
44
49
|
codeServerPackageName,
|
|
45
|
-
codeServerRoot,
|
|
46
50
|
legacyInstallRoot,
|
|
51
|
+
productPath,
|
|
47
52
|
readTreeVersion,
|
|
48
53
|
vendoredVersion,
|
|
49
54
|
vendorReady,
|
|
55
|
+
vsRoot,
|
|
56
|
+
vsServerEntry,
|
|
50
57
|
} from './vendor.js';
|
|
51
|
-
import { aliasNodePathDirs, ensureRuntimeLayout,
|
|
58
|
+
import { aliasNodePathDirs, ensureRuntimeLayout, resolveRuntime, runtimePackageName, verifyNatives } from './native.js';
|
|
59
|
+
import { MOUNT_PATH, mountOnWebServer } from './serve-dsh.mjs';
|
|
52
60
|
|
|
53
61
|
// schemastery 由 DSH 部署自带(官方核心依赖),仿 auto-open-web 的解析策略:
|
|
54
62
|
// 常规 import 优先,不可用时回退到全局 npm 布局的 DSH 部署副本。
|
|
@@ -87,16 +95,21 @@ export const Config = z.object({
|
|
|
87
95
|
/** windowedOpen=false(默认)点击悬浮球打开内部浮动窗口;
|
|
88
96
|
* true 时改为在浏览器新标签页打开 code-server(自动启动并跟随工作区)。 */
|
|
89
97
|
windowedOpen: z.boolean().default(false),
|
|
98
|
+
/** 服务方式:loopback(默认)独立回环端口;dsh 挂到 DSH webServer 的 /code-server
|
|
99
|
+
* (同源、无额外端口、复用 DSH 的 Host/Origin + cookie 防护;需要 DSH 提供 webServer 服务,
|
|
100
|
+
* 缺失或注册失败时自动回退 loopback)。 */
|
|
101
|
+
serve: z.union([z.const('loopback'), z.const('dsh')]).default('loopback'),
|
|
90
102
|
});
|
|
91
103
|
|
|
92
104
|
const DEFAULT_CONFIG = {
|
|
93
|
-
bin: '
|
|
105
|
+
bin: '',
|
|
94
106
|
host: '127.0.0.1',
|
|
95
107
|
port: 8090,
|
|
96
108
|
auth: 'none',
|
|
97
|
-
|
|
109
|
+
serve: 'loopback',
|
|
98
110
|
userDataDir: '',
|
|
99
111
|
extensionsDir: '',
|
|
112
|
+
locale: '',
|
|
100
113
|
readyTimeoutMs: 60000,
|
|
101
114
|
};
|
|
102
115
|
|
|
@@ -129,41 +142,29 @@ function win32() {
|
|
|
129
142
|
return process.platform === 'win32';
|
|
130
143
|
}
|
|
131
144
|
|
|
132
|
-
/** 插件自带
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
* 不存在 → 回退配置/PATH。 */
|
|
145
|
+
/** 插件自带 launcher(<pkg>/lib/launcher.mjs);它直接驱动 VS Code server(0.2.0 模型)。 */
|
|
146
|
+
function launcherPath() {
|
|
147
|
+
return path.join(path.dirname(fileURLToPath(import.meta.url)), 'launcher.mjs');
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** code-server 兼容入口(旧全量树才有 out/node/entry.js);新树返回 null。 */
|
|
139
151
|
function bundledRuntimeEntry() {
|
|
140
152
|
try {
|
|
141
|
-
|
|
142
|
-
if (entry !== null) return entry;
|
|
143
|
-
const here = path.dirname(fileURLToPath(import.meta.url)); // .../dsh-code-server-app/lib
|
|
144
|
-
const candidates = [
|
|
145
|
-
path.join(here, '..', '..', '..', '.code-server-app', 'node_modules', 'code-server', 'out', 'node', 'entry.js'),
|
|
146
|
-
path.join(here, '..', 'node_modules', 'code-server', 'out', 'node', 'entry.js'),
|
|
147
|
-
];
|
|
148
|
-
for (const candidate of candidates) {
|
|
149
|
-
if (fs.existsSync(candidate)) return candidate;
|
|
150
|
-
}
|
|
151
|
-
return null;
|
|
153
|
+
return codeServerEntry();
|
|
152
154
|
} catch {
|
|
153
155
|
return null;
|
|
154
156
|
}
|
|
155
157
|
}
|
|
156
158
|
|
|
157
|
-
/** 扩展安装目标:VS Code
|
|
159
|
+
/** 扩展安装目标:VS Code「内置扩展」目录 = <树>/lib/vscode/extensions。
|
|
158
160
|
* (位于程序内置目录的扩展被 VS Code 视为内置——用户视图显示为"内置",不可卸载;
|
|
159
161
|
* --extensions-dir 的是用户级扩展,可被用户禁用/卸载。)
|
|
160
|
-
* 返回 { dst, builtin }——builtin=true
|
|
162
|
+
* 返回 { dst, builtin }——builtin=true 时为核心路径;找不到树则回退用户级。 */
|
|
161
163
|
function extensionTarget(extensionsDir) {
|
|
162
164
|
try {
|
|
163
|
-
const
|
|
164
|
-
if (
|
|
165
|
-
const
|
|
166
|
-
const vscodeExt = path.join(csRoot, 'lib', 'vscode', 'extensions');
|
|
165
|
+
const root = vsRoot();
|
|
166
|
+
if (root !== null) {
|
|
167
|
+
const vscodeExt = path.join(root, 'lib', 'vscode', 'extensions');
|
|
167
168
|
if (fs.existsSync(vscodeExt)) {
|
|
168
169
|
return { dst: path.join(vscodeExt, 'dshcs-open-file'), builtin: true };
|
|
169
170
|
}
|
|
@@ -203,22 +204,22 @@ function openFileSignalPath(userDataDir) {
|
|
|
203
204
|
return path.join(userDataDir, 'User', 'dshcs-open.json');
|
|
204
205
|
}
|
|
205
206
|
|
|
206
|
-
/** 插件 package.json 里声明的内部依赖(纯 JS
|
|
207
|
+
/** 插件 package.json 里声明的内部依赖(纯 JS 直装集;排除 VS Code 树包本身)。 */
|
|
207
208
|
function declaredInnerDeps() {
|
|
208
209
|
try {
|
|
209
210
|
const manifest = JSON.parse(fs.readFileSync(path.join(PACKAGE_ROOT, 'package.json'), 'utf8'));
|
|
210
|
-
return Object.keys(manifest.dependencies ?? {})
|
|
211
|
+
return Object.keys(manifest.dependencies ?? {})
|
|
212
|
+
.filter((name) => !/^@[^/]+\/dshcs-(vscode-server|code-server)$/.test(name));
|
|
211
213
|
} catch {
|
|
212
214
|
return [];
|
|
213
215
|
}
|
|
214
216
|
}
|
|
215
217
|
|
|
216
|
-
/** 从
|
|
217
|
-
* 先试 `<name>/package.json`(
|
|
218
|
-
* 再退回 `<name>` 本身。 */
|
|
218
|
+
/** 从 VS Code 树位置解析内部依赖(与运行时同一套向上查找规则)。
|
|
219
|
+
* 先试 `<name>/package.json`(ESM-only 包没有 require 入口),再退回 `<name>` 本身。 */
|
|
219
220
|
function checkInnerDeps() {
|
|
220
221
|
const declared = declaredInnerDeps();
|
|
221
|
-
const root =
|
|
222
|
+
const root = vsRoot();
|
|
222
223
|
const from = createRequire(path.join(root ?? PACKAGE_ROOT, 'package.json'));
|
|
223
224
|
const missing = [];
|
|
224
225
|
let resolved = 0;
|
|
@@ -237,32 +238,25 @@ function checkInnerDeps() {
|
|
|
237
238
|
return { declared: declared.length, resolved, missing };
|
|
238
239
|
}
|
|
239
240
|
|
|
240
|
-
/** 环境检测:
|
|
241
|
-
* 返回 { ok, entry,
|
|
241
|
+
/** 环境检测:VS Code 树 + server 入口 + 内部依赖 + 预编译原生包。
|
|
242
|
+
* 返回 { ok, tree, entry, productPath, vscodeInner, innerDeps, treeVersion, vendored, upToDate, nativeRuntime, node, platform, arch }。 */
|
|
242
243
|
function envCheck() {
|
|
243
|
-
const
|
|
244
|
-
const
|
|
245
|
-
// argon2 不在 code-server 本体包里(平台包 @<scope>/dshcs-argon2-<平台>-<架构> 提供,经聚合包用
|
|
246
|
-
// npm: 别名装回原名);pnpm 会把带 os/cpu 限定的包嵌套装在聚合包下,所以按多锚点解析。
|
|
247
|
-
const argon2 = resolveNativeDir('argon2');
|
|
248
|
-
// 必须按当前平台判定:argon2 的 prebuilds 目录存在不代表本平台有二进制(win32-arm64 就缺)。
|
|
249
|
-
const nativeOk = argon2 !== null && (
|
|
250
|
-
fs.existsSync(path.join(argon2, 'build', 'Release', 'argon2.node')) ||
|
|
251
|
-
fs.existsSync(path.join(argon2, 'prebuilds', `${process.platform}-${process.arch}`))
|
|
252
|
-
);
|
|
244
|
+
const root = vsRoot();
|
|
245
|
+
const entry = vsServerEntry();
|
|
253
246
|
const inner = checkInnerDeps();
|
|
254
247
|
const runtime = resolveRuntime();
|
|
255
248
|
const natives = verifyNatives(runtime !== null ? runtime.modules : []);
|
|
256
|
-
const installed =
|
|
249
|
+
const installed = root !== null ? readTreeVersion(root) : null;
|
|
257
250
|
const vendored = vendoredVersion();
|
|
258
251
|
return {
|
|
259
|
-
ok: entry !== null &&
|
|
252
|
+
ok: entry !== null && inner.missing.length === 0
|
|
260
253
|
&& runtime !== null && natives.missing.length === 0,
|
|
254
|
+
tree: root !== null ? root.replace(/\\/g, '/') : null,
|
|
261
255
|
entry: entry !== null ? entry.replace(/\\/g, '/') : null,
|
|
262
|
-
|
|
256
|
+
productPath: productPath(root),
|
|
263
257
|
vscodeInner: inner.missing.length === 0,
|
|
264
258
|
innerDeps: { declared: inner.declared, resolved: inner.resolved, missing: inner.missing },
|
|
265
|
-
|
|
259
|
+
treeVersion: installed,
|
|
266
260
|
vendored,
|
|
267
261
|
upToDate: installed !== null && vendored !== null && installed === vendored,
|
|
268
262
|
// 本平台预编译原生产物(平台聚合包 + 它带回原始名字的原生模块)
|
|
@@ -281,41 +275,49 @@ function envCheck() {
|
|
|
281
275
|
};
|
|
282
276
|
}
|
|
283
277
|
|
|
284
|
-
/**
|
|
285
|
-
* -
|
|
286
|
-
* - `bin`
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
278
|
+
/** 解析启动方式:
|
|
279
|
+
* - 默认:插件自带 launcher(node lib/launcher.mjs)+ 内置 VS Code 树;
|
|
280
|
+
* - `bin` 显式配置时按旧模型当逃生舱:可执行文件 / code-server 的 out/node/entry.js。 */
|
|
281
|
+
function resolveLaunch(config, serve) {
|
|
282
|
+
const configured = typeof config.bin === 'string' && config.bin !== '' && config.bin !== DEFAULT_CONFIG.bin
|
|
283
|
+
? config.bin
|
|
284
|
+
: null;
|
|
285
|
+
if (configured === null) {
|
|
286
|
+
const root = vsRoot();
|
|
287
|
+
if (root === null || vsServerEntry() === null) {
|
|
288
|
+
throw new Error(
|
|
289
|
+
'找不到 VS Code 树(缺 lib/vscode/out/server-main.js)。请重新安装插件'
|
|
290
|
+
+ '(`dsh plugin --profile web add dsh-code-server-app`),'
|
|
291
|
+
+ '开发期先用 `node scripts/vendor-vscode-server.mjs --dev-links` 生成 vendor/vscode。',
|
|
292
|
+
);
|
|
293
|
+
}
|
|
294
|
+
return { kind: 'launcher', script: launcherPath(), tree: root };
|
|
295
|
+
}
|
|
296
|
+
console.log(`[code-server] resolveLaunch: 使用显式配置的 bin=${configured}(launcher 旁路)`);
|
|
297
|
+
if (/\.(js|mjs|cjs)$/i.test(configured)) {
|
|
298
|
+
if (!fs.existsSync(configured)) throw new Error(`配置的入口不存在: ${configured}`);
|
|
299
|
+
return { kind: 'node', script: configured };
|
|
300
|
+
}
|
|
301
|
+
if (path.isAbsolute(configured)) return { kind: 'bin', command: configured };
|
|
302
302
|
try {
|
|
303
303
|
if (win32()) {
|
|
304
|
-
const out = execFileSync('where.exe', [
|
|
304
|
+
const out = execFileSync('where.exe', [configured], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] });
|
|
305
305
|
const lines = out.split(/\r?\n/).map((s) => s.trim()).filter(Boolean);
|
|
306
306
|
return { kind: 'bin', command: lines.find((l) => /\.cmd$/i.test(l)) ?? lines.find((l) => /\.exe$/i.test(l)) ?? lines[0] };
|
|
307
307
|
}
|
|
308
|
-
const out = execFileSync('which', [
|
|
308
|
+
const out = execFileSync('which', [configured], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] });
|
|
309
309
|
return { kind: 'bin', command: out.split('\n')[0].trim() };
|
|
310
310
|
} catch {
|
|
311
|
-
throw new Error(
|
|
312
|
-
`code-server 未找到("${preferred}" 不在 PATH)。插件包内应自带 vendor/code-server/out/node/entry.js;` +
|
|
313
|
-
`若缺失请重新安装插件(dsh plugin --profile web add <tgz>),` +
|
|
314
|
-
`也可在 cordis.patch.yml 的 code-server config 中把 bin 指向已安装的 code-server 可执行文件 / out/node/entry.js。`,
|
|
315
|
-
);
|
|
311
|
+
throw new Error(`配置的 bin 未找到("${configured}" 不在 PATH)`);
|
|
316
312
|
}
|
|
317
313
|
}
|
|
318
314
|
|
|
315
|
+
/** 命名管道名(仅 dsh 模式使用):按 profile/pid 稳定,避免撞名。 */
|
|
316
|
+
function pipeName() {
|
|
317
|
+
const key = `${process.pid}`;
|
|
318
|
+
return win32() ? `\\\\.\\pipe\\dshcs-vscode-${key}` : path.join(os.tmpdir(), `dshcs-vscode-${key}.sock`);
|
|
319
|
+
}
|
|
320
|
+
|
|
319
321
|
function healthCheck(host, port, timeoutMs = 1500) {
|
|
320
322
|
return new Promise((resolve) => {
|
|
321
323
|
const req = http.get({ host, port, path: '/healthz', timeout: timeoutMs, method: 'GET' }, (res) => {
|
|
@@ -330,6 +332,22 @@ function healthCheck(host, port, timeoutMs = 1500) {
|
|
|
330
332
|
});
|
|
331
333
|
}
|
|
332
334
|
|
|
335
|
+
/** 命名管道上的 /healthz 探针(dsh 模式;管道由 launcher 监听)。 */
|
|
336
|
+
function healthCheckPipe(pipe, timeoutMs = 1500) {
|
|
337
|
+
return new Promise((resolve) => {
|
|
338
|
+
const req = http.request({ socketPath: pipe, path: '/healthz', method: 'GET', timeout: timeoutMs }, (res) => {
|
|
339
|
+
res.resume();
|
|
340
|
+
resolve({ ok: res.statusCode >= 200 && res.statusCode < 500, statusCode: res.statusCode });
|
|
341
|
+
});
|
|
342
|
+
req.on('error', () => resolve({ ok: false }));
|
|
343
|
+
req.on('timeout', () => {
|
|
344
|
+
req.destroy();
|
|
345
|
+
resolve({ ok: false });
|
|
346
|
+
});
|
|
347
|
+
req.end();
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
|
|
333
351
|
function readPidFile(config) {
|
|
334
352
|
try {
|
|
335
353
|
const raw = JSON.parse(fs.readFileSync(pidFile(config), 'utf8'));
|
|
@@ -357,8 +375,15 @@ export async function apply(ctx, config) {
|
|
|
357
375
|
|
|
358
376
|
// ---- 设置:行配置为种子;settings 命名空间持久化(设置卡片写入) ----
|
|
359
377
|
const settingsSvc = ctx.get('settings');
|
|
378
|
+
// connection 是 host↔client 通道(必需);提前取出,供 DSH 同源挂载的 fence 使用。
|
|
379
|
+
const connection = ctx.get('connection');
|
|
380
|
+
if (connection === undefined || connection.fetch === undefined) {
|
|
381
|
+
console.error('[code-server] connection service unavailable; plugin registered but idle');
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
360
384
|
let reserveComposer = true;
|
|
361
385
|
let windowedOpen = false;
|
|
386
|
+
let serveSetting = 'loopback';
|
|
362
387
|
if (settingsSvc !== undefined && typeof settingsSvc.register === 'function') {
|
|
363
388
|
try {
|
|
364
389
|
const scope = settingsSvc.register(SETTINGS_NS, Config);
|
|
@@ -367,6 +392,7 @@ export async function apply(ctx, config) {
|
|
|
367
392
|
const resolved = scope.get();
|
|
368
393
|
reserveComposer = resolved && typeof resolved.reserveComposer === 'boolean' ? resolved.reserveComposer : true;
|
|
369
394
|
windowedOpen = resolved && typeof resolved.windowedOpen === 'boolean' ? resolved.windowedOpen : false;
|
|
395
|
+
serveSetting = resolved && resolved.serve === 'dsh' ? 'dsh' : 'loopback';
|
|
370
396
|
}
|
|
371
397
|
scope.watch((next) => {
|
|
372
398
|
if (next != null && typeof next.reserveComposer === 'boolean') {
|
|
@@ -377,12 +403,37 @@ export async function apply(ctx, config) {
|
|
|
377
403
|
windowedOpen = next.windowedOpen;
|
|
378
404
|
console.log(`[code-server] windowedOpen updated: ${windowedOpen}`);
|
|
379
405
|
}
|
|
406
|
+
if (next != null && (next.serve === 'dsh' || next.serve === 'loopback')) {
|
|
407
|
+
if (next.serve !== serveSetting) {
|
|
408
|
+
serveSetting = next.serve;
|
|
409
|
+
console.log(`[code-server] serve updated: ${serveSetting}(下次启动生效)`);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
380
412
|
});
|
|
381
413
|
} catch (error) {
|
|
382
414
|
console.error(`[code-server] settings unavailable; using defaults (reserveComposer=true, windowedOpen=false): ${error.message}`);
|
|
383
415
|
}
|
|
384
416
|
}
|
|
385
417
|
|
|
418
|
+
// 依赖布局自愈必须发生在任何解析之前:精简树不含 lib/vscode/node_modules,
|
|
419
|
+
// 由这里按插件依赖图补 junction(幂等;重装后自愈)。
|
|
420
|
+
const layout = ensureRuntimeLayout();
|
|
421
|
+
|
|
422
|
+
// ---- 服务方式:settings/行配置请求 dsh,但只有本 deployment 真的提供 webServer 时才用 ----
|
|
423
|
+
let webServerSvc = ctx.get('webServer');
|
|
424
|
+
let dshMount = null; // { disposers, upgradePath } —— 由下面的 ctx.inject 填充
|
|
425
|
+
|
|
426
|
+
/** 生效的服务方式:loopback(默认,独立回环端口)| dsh(DSH 同源挂载)。 */
|
|
427
|
+
function requestedServe() {
|
|
428
|
+
const want = serveSetting === 'dsh' || cfg.serve === 'dsh' ? 'dsh' : 'loopback';
|
|
429
|
+
if (want === 'loopback') return 'loopback';
|
|
430
|
+
if (webServerSvc === undefined || dshMount === null) {
|
|
431
|
+
console.warn('[code-server] serve=dsh 但 webServer 不可用/挂载失败 → 回退 loopback');
|
|
432
|
+
return 'loopback';
|
|
433
|
+
}
|
|
434
|
+
return 'dsh';
|
|
435
|
+
}
|
|
436
|
+
|
|
386
437
|
const state = {
|
|
387
438
|
status: 'stopped', // stopped | starting | running | stopping | error
|
|
388
439
|
pid: null,
|
|
@@ -393,7 +444,9 @@ export async function apply(ctx, config) {
|
|
|
393
444
|
logTail: '',
|
|
394
445
|
startedAt: null,
|
|
395
446
|
adopted: false,
|
|
396
|
-
|
|
447
|
+
serve: 'loopback', // 实际生效的服务方式(loopback | dsh)
|
|
448
|
+
pipe: null, // dsh 模式下的命名管道
|
|
449
|
+
env: envCheck(), // 环境检测(VS Code 树 / server 入口 / 内部依赖 / 预编译原生包)
|
|
397
450
|
setup: { running: false, done: true, ok: true, logTail: '0.1.36 起由包管理器安装依赖,无需「安装环境」步骤', startedAt: null, finishedAt: null }, // 兼容旧客户端
|
|
398
451
|
};
|
|
399
452
|
|
|
@@ -401,17 +454,48 @@ export async function apply(ctx, config) {
|
|
|
401
454
|
let pollTimer = null;
|
|
402
455
|
let disposeKilled = false;
|
|
403
456
|
|
|
457
|
+
// ---- DSH 同源挂载(serve=dsh):把 /code-server 注册到 DSH 自己的 webServer ----
|
|
458
|
+
// webServer 只在 web profile 存在(desktop 显式禁用该行)→ 用 ctx.inject 特性检测,
|
|
459
|
+
// 缺失时 requestedServe() 自动回退 loopback。
|
|
460
|
+
ctx.inject(['webServer'], (wsCtx) => {
|
|
461
|
+
webServerSvc = wsCtx.webServer;
|
|
462
|
+
try {
|
|
463
|
+
dshMount = mountOnWebServer({
|
|
464
|
+
webServer: wsCtx.webServer,
|
|
465
|
+
connection,
|
|
466
|
+
// 目标随时可变:launcher 重启会换管道/端口,未运行时返回 null(路由回 503)
|
|
467
|
+
getTarget: () => (state.serve === 'dsh' && state.pipe !== null ? { kind: 'pipe', pipe: state.pipe } : null),
|
|
468
|
+
productPath: state.env?.productPath ?? productPath() ?? 'stable',
|
|
469
|
+
});
|
|
470
|
+
console.log(`[code-server] serve=dsh 挂载就绪:${MOUNT_PATH}/ (HTTP)+ ${dshMount.upgradePath} (WS)`);
|
|
471
|
+
} catch (error) {
|
|
472
|
+
dshMount = null;
|
|
473
|
+
console.error(`[code-server] serve=dsh 挂载失败(将回退 loopback):${error && error.message ? error.message : error}`);
|
|
474
|
+
}
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
ctx.effect(() => () => {
|
|
478
|
+
if (dshMount === null) return;
|
|
479
|
+
for (const dispose of dshMount.disposers) {
|
|
480
|
+
try { dispose(); } catch { /* ignore */ }
|
|
481
|
+
}
|
|
482
|
+
}, 'code-server: dsh mount');
|
|
483
|
+
|
|
404
484
|
function snapshot() {
|
|
405
485
|
const running = state.status === 'running' && state.pid !== null;
|
|
486
|
+
const dshMode = state.serve === 'dsh';
|
|
406
487
|
return {
|
|
407
488
|
ok: state.status !== 'error' || running,
|
|
408
489
|
running,
|
|
409
490
|
status: state.status,
|
|
410
|
-
|
|
411
|
-
|
|
491
|
+
serve: state.serve,
|
|
492
|
+
port: dshMode ? null : state.port,
|
|
493
|
+
host: dshMode ? null : cfg.host,
|
|
412
494
|
pid: state.pid,
|
|
413
495
|
cwd: state.cwd,
|
|
414
|
-
|
|
496
|
+
// dsh 模式:同源相对地址(由 DSH webServer 的 prefix 路由提供服务)
|
|
497
|
+
url: running ? (dshMode ? '/code-server/' : `http://${cfg.host}:${state.port}/`) : null,
|
|
498
|
+
productPath: state.env?.productPath ?? null,
|
|
415
499
|
version: state.version,
|
|
416
500
|
error: state.error,
|
|
417
501
|
logTail: state.logTail.slice(-LOG_TAIL_MAX),
|
|
@@ -523,6 +607,12 @@ export async function apply(ctx, config) {
|
|
|
523
607
|
if (reason) state.error = null;
|
|
524
608
|
}
|
|
525
609
|
|
|
610
|
+
/** 就绪探针:loopback 走 TCP,dsh 模式走命名管道。 */
|
|
611
|
+
function probeReady(timeoutMs = 1500) {
|
|
612
|
+
if (state.pipe !== null) return healthCheckPipe(state.pipe, timeoutMs);
|
|
613
|
+
return healthCheck(cfg.host, state.port, timeoutMs);
|
|
614
|
+
}
|
|
615
|
+
|
|
526
616
|
function beginPollingReady() {
|
|
527
617
|
stopPolling();
|
|
528
618
|
const deadline = Date.now() + (Number(cfg.readyTimeoutMs) || DEFAULT_CONFIG.readyTimeoutMs);
|
|
@@ -533,7 +623,7 @@ export async function apply(ctx, config) {
|
|
|
533
623
|
first = false;
|
|
534
624
|
return; // 等 800ms 才首次探测(给 Node 启动留时间)
|
|
535
625
|
}
|
|
536
|
-
const probe = await
|
|
626
|
+
const probe = await probeReady();
|
|
537
627
|
if (probe.ok) {
|
|
538
628
|
stopPolling();
|
|
539
629
|
state.status = 'running';
|
|
@@ -542,7 +632,7 @@ export async function apply(ctx, config) {
|
|
|
542
632
|
if (Date.now() > deadline) {
|
|
543
633
|
stopPolling();
|
|
544
634
|
state.status = 'error';
|
|
545
|
-
state.error = `启动超时(${cfg.readyTimeoutMs}ms 内 /healthz 未就绪)
|
|
635
|
+
state.error = `启动超时(${cfg.readyTimeoutMs}ms 内 /healthz 未就绪);启动日志尾部:\n${state.logTail.slice(-2000)}`;
|
|
546
636
|
}
|
|
547
637
|
}, 500);
|
|
548
638
|
}
|
|
@@ -571,36 +661,37 @@ export async function apply(ctx, config) {
|
|
|
571
661
|
}
|
|
572
662
|
}
|
|
573
663
|
|
|
574
|
-
const
|
|
664
|
+
const serve = requestedServe(); // loopback | dsh(按 DSH 是否提供 webServer 定)
|
|
665
|
+
const launch = resolveLaunch(cfg, serve); // throws with install guidance when missing
|
|
575
666
|
|
|
576
|
-
|
|
577
|
-
const auth = cfg.auth || 'none';
|
|
578
|
-
if (auth === 'none' && !LOOPBACK_HOSTS.has(cfg.host)) {
|
|
667
|
+
if (serve === 'loopback' && !LOOPBACK_HOSTS.has(cfg.host)) {
|
|
579
668
|
state.status = 'error';
|
|
580
|
-
state.error = `
|
|
669
|
+
state.error = `serve=loopback 仅允许回环绑定(当前 host="${cfg.host}");`
|
|
670
|
+
+ '如需对外提供服务,请改用 serve=dsh(挂到 DSH 同源路径,由 DSH 统一防护)';
|
|
581
671
|
return snapshot();
|
|
582
672
|
}
|
|
583
|
-
if (auth === 'password'
|
|
584
|
-
|
|
585
|
-
state.error = 'auth=password 需要配置 passwordToken(cordis.patch.yml 的 config.passwordToken)';
|
|
586
|
-
return snapshot();
|
|
673
|
+
if (cfg.auth === 'password') {
|
|
674
|
+
console.warn('[code-server] 0.2.0 起不再支持口令认证(argon2 已移除),按 auth=none 运行');
|
|
587
675
|
}
|
|
588
676
|
|
|
589
|
-
// 端口占用则尝试 adopt(pid.json 有效 + /healthz 响应),否则报错
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
677
|
+
// 端口占用则尝试 adopt(仅 loopback 模式;pid.json 有效 + /healthz 响应),否则报错
|
|
678
|
+
if (serve === 'loopback') {
|
|
679
|
+
const probe = await healthCheck(cfg.host, cfg.port, 800);
|
|
680
|
+
if (probe.ok) {
|
|
681
|
+
const record = readPidFile(cfg);
|
|
682
|
+
if (record && isAlive(record.pid)) {
|
|
683
|
+
state.serve = 'loopback';
|
|
684
|
+
state.status = 'running';
|
|
685
|
+
state.pid = record.pid;
|
|
686
|
+
state.cwd = cwd ?? record.cwd ?? null;
|
|
687
|
+
state.startedAt = record.startedAt ?? null;
|
|
688
|
+
state.adopted = true;
|
|
689
|
+
return snapshot();
|
|
690
|
+
}
|
|
691
|
+
state.status = 'error';
|
|
692
|
+
state.error = `端口 ${cfg.port} 已被占用且没有有效的 pid.json 记录(拒绝误杀);请释放端口或修改 port 配置`;
|
|
599
693
|
return snapshot();
|
|
600
694
|
}
|
|
601
|
-
state.status = 'error';
|
|
602
|
-
state.error = `端口 ${cfg.port} 已被占用且没有有效的 pid.json 记录(拒绝误杀);请释放端口或修改 port 配置`;
|
|
603
|
-
return snapshot();
|
|
604
695
|
}
|
|
605
696
|
|
|
606
697
|
// 重建数据目录
|
|
@@ -613,16 +704,35 @@ export async function apply(ctx, config) {
|
|
|
613
704
|
// 安装内置扩展(dshcs-open-file:host 信号文件 → VS Code 打开文件)
|
|
614
705
|
installBundledExtension(extensionsDir, userDataDir);
|
|
615
706
|
|
|
616
|
-
const args =
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
707
|
+
const args = launch.kind === 'launcher'
|
|
708
|
+
? [
|
|
709
|
+
launch.script,
|
|
710
|
+
'--tree', launch.tree,
|
|
711
|
+
'--user-data-dir', userDataDir,
|
|
712
|
+
'--extensions-dir', extensionsDir,
|
|
713
|
+
'--parent-pid', String(process.pid),
|
|
714
|
+
]
|
|
715
|
+
: [
|
|
716
|
+
'--bind-addr', `${cfg.host}:${cfg.port}`,
|
|
717
|
+
'--auth', 'none',
|
|
718
|
+
'--user-data-dir', userDataDir,
|
|
719
|
+
'--extensions-dir', extensionsDir,
|
|
720
|
+
'--disable-telemetry',
|
|
721
|
+
'--disable-update-check',
|
|
722
|
+
];
|
|
723
|
+
if (launch.kind === 'launcher') {
|
|
724
|
+
if (serve === 'dsh') {
|
|
725
|
+
state.pipe = pipeName();
|
|
726
|
+
args.push('--pipe', state.pipe);
|
|
727
|
+
} else {
|
|
728
|
+
state.pipe = null;
|
|
729
|
+
args.push('--port', String(cfg.port), '--host', cfg.host);
|
|
730
|
+
}
|
|
731
|
+
if (typeof cfg.locale === 'string' && cfg.locale !== '') args.push('--locale', cfg.locale);
|
|
732
|
+
}
|
|
733
|
+
if (cwd !== undefined && launch.kind !== 'launcher') args.push(cwd);
|
|
625
734
|
|
|
735
|
+
state.serve = serve;
|
|
626
736
|
state.error = null;
|
|
627
737
|
state.logTail = '';
|
|
628
738
|
state.status = 'starting';
|
|
@@ -631,12 +741,11 @@ export async function apply(ctx, config) {
|
|
|
631
741
|
state.adopted = false;
|
|
632
742
|
|
|
633
743
|
const env = { ...process.env };
|
|
634
|
-
if (auth === 'password') env.PASSWORD = cfg.passwordToken;
|
|
635
744
|
// 内置扩展信号文件路径(host → 扩展 打开文件)
|
|
636
745
|
env.DSHCS_OPEN_FILE_SIGNAL = openFileSignalPath(userDataDir);
|
|
637
|
-
// pnpm 会把带 os/cpu
|
|
638
|
-
//
|
|
639
|
-
//
|
|
746
|
+
// pnpm 会把带 os/cpu 限定的原生包嵌套装在平台聚合包下(如 <profile>/node_modules/@<scope>/
|
|
747
|
+
// dsh-code-server-runtime-<平台>-<架构>/node_modules/@vscode/spdlog),那些目录不在 VS Code
|
|
748
|
+
// 树的向上查找链里 → 用 NODE_PATH 让子进程的 require 找得到(CJS;ESM 由 ensureAliasLinks 的 junction 负责)。
|
|
640
749
|
const nodePathDirs = aliasNodePathDirs();
|
|
641
750
|
if (nodePathDirs.length > 0) {
|
|
642
751
|
const existing = typeof env.NODE_PATH === 'string' && env.NODE_PATH !== '' ? env.NODE_PATH.split(path.delimiter) : [];
|
|
@@ -648,8 +757,8 @@ export async function apply(ctx, config) {
|
|
|
648
757
|
// 启动前再自愈一次依赖布局(插件重装/树被替换后可能丢失;幂等且只做存在性检查)
|
|
649
758
|
ensureRuntimeLayout();
|
|
650
759
|
const isCmd = launch.kind === 'bin' && win32() && /\.cmd$/i.test(launch.command);
|
|
651
|
-
const command = launch.kind === '
|
|
652
|
-
const spawnArgs = launch.kind === '
|
|
760
|
+
const command = launch.kind === 'bin' ? launch.command : process.execPath;
|
|
761
|
+
const spawnArgs = launch.kind === 'bin' ? args : [launch.script, ...args];
|
|
653
762
|
// shell 仅对 .cmd shim(Windows npm 全局包)必要:它必须经 cmd.exe 解析。
|
|
654
763
|
// 含空格路径由 spawn 数组传参,不再经 shell 拼接,避免 'C:\Program' 拆分。
|
|
655
764
|
proc = spawn(isCmd ? `"${command}"` : command, spawnArgs, {
|
|
@@ -671,10 +780,12 @@ export async function apply(ctx, config) {
|
|
|
671
780
|
pid: proc.pid,
|
|
672
781
|
startedAt: state.startedAt,
|
|
673
782
|
cwd: cwd ?? null,
|
|
674
|
-
host: cfg.host,
|
|
675
|
-
port: cfg.port,
|
|
783
|
+
host: serve === 'dsh' ? null : cfg.host,
|
|
784
|
+
port: serve === 'dsh' ? null : cfg.port,
|
|
785
|
+
serve,
|
|
786
|
+
pipe: state.pipe,
|
|
676
787
|
launchKind: launch.kind,
|
|
677
|
-
launchCommand: launch.kind === '
|
|
788
|
+
launchCommand: launch.kind === 'bin' ? launch.command : launch.script,
|
|
678
789
|
});
|
|
679
790
|
|
|
680
791
|
proc.stdout?.on?.('data', appendLog);
|
|
@@ -721,8 +832,8 @@ export async function apply(ctx, config) {
|
|
|
721
832
|
/** 环境问题的一句话描述(状态卡/日志共用)。 */
|
|
722
833
|
function describeEnvProblem(env) {
|
|
723
834
|
const parts = [];
|
|
724
|
-
if (env.
|
|
725
|
-
if (env.
|
|
835
|
+
if (env.tree === null) parts.push(`缺少 VS Code 树(包内 vendor/vscode 或 dshcs-vscode-server 未安装)`);
|
|
836
|
+
else if (env.entry === null) parts.push('树里缺少 lib/vscode/out/server-main.js');
|
|
726
837
|
if (env.vscodeInner !== true) {
|
|
727
838
|
const missing = Array.isArray(env.innerDeps?.missing) ? env.innerDeps.missing : [];
|
|
728
839
|
parts.push(`内部依赖未装全(缺 ${missing.length} 个:${missing.slice(0, 5).join(', ')}${missing.length > 5 ? ' …' : ''})`);
|
|
@@ -822,16 +933,9 @@ export async function apply(ctx, config) {
|
|
|
822
933
|
}
|
|
823
934
|
}
|
|
824
935
|
|
|
825
|
-
const connection = ctx.get('connection');
|
|
826
|
-
if (connection === undefined || connection.fetch === undefined) {
|
|
827
|
-
console.error('[code-server] connection service unavailable; plugin registered but idle');
|
|
828
|
-
return;
|
|
829
|
-
}
|
|
830
|
-
|
|
831
936
|
// 每条操作一条 exact Fetch 路由。desktop 的 assetHandler 只把 /api/* 交给
|
|
832
937
|
// createSharedFetchHandler('/api'),所以路径必须落在 /api 下。
|
|
833
|
-
const disposers = [
|
|
834
|
-
{ path: `${API_BASE}/status`, methods: ['GET'], fetch: handleStatus },
|
|
938
|
+
const disposers = [ { path: `${API_BASE}/status`, methods: ['GET'], fetch: handleStatus },
|
|
835
939
|
{ path: `${API_BASE}/start`, methods: ['POST'], fetch: handleStart },
|
|
836
940
|
{ path: `${API_BASE}/stop`, methods: ['POST'], fetch: handleStop },
|
|
837
941
|
{ path: `${API_BASE}/setup`, methods: ['POST'], fetch: handleSetup },
|
|
@@ -880,12 +984,7 @@ export async function apply(ctx, config) {
|
|
|
880
984
|
}
|
|
881
985
|
|
|
882
986
|
state.env = envCheck();
|
|
883
|
-
//
|
|
884
|
-
// ① VS Code 内部依赖目录(lib/vscode/node_modules、lib/vscode/extensions/node_modules)
|
|
885
|
-
// —— 老模型是 npm 装在树里的真实目录,新模型拍平在 profile 根;用显式路径找依赖的代码
|
|
886
|
-
// (如内置 TS 扩展找 tsserver.js)否则会报 "tsserver was deleted …";
|
|
887
|
-
// ② 聚合包带回的原生别名(ESM import 不认 NODE_PATH,只能靠目录链)。
|
|
888
|
-
const layout = ensureRuntimeLayout();
|
|
987
|
+
// layout 已在本函数开头算过(envCheck 之前):这里只报告结果。
|
|
889
988
|
if (layout.created.length > 0) {
|
|
890
989
|
console.log(`[code-server] 已补齐 ${layout.created.length} 个依赖链接: ${layout.created.join(', ')}`);
|
|
891
990
|
}
|
|
@@ -893,10 +992,10 @@ export async function apply(ctx, config) {
|
|
|
893
992
|
console.warn(`[code-server] 依赖链接创建失败: ${layout.failed.join('; ')}`);
|
|
894
993
|
}
|
|
895
994
|
if (!vendorReady()) {
|
|
896
|
-
console.warn(`[code-server] 找不到
|
|
897
|
-
+ '且包内 vendor/
|
|
995
|
+
console.warn(`[code-server] 找不到 VS Code 树:平台无关包 ${codeServerPackageName()} 未安装,`
|
|
996
|
+
+ '且包内 vendor/vscode 不存在(开发期请先 `node scripts/vendor-vscode-server.mjs --dev-links`)');
|
|
898
997
|
} else {
|
|
899
|
-
console.log(`[code-server]
|
|
998
|
+
console.log(`[code-server] VS Code 树: ${vsRoot()}(productPath=${state.env.productPath ?? '?'})`);
|
|
900
999
|
}
|
|
901
1000
|
if (!state.env.ok) {
|
|
902
1001
|
console.warn(`[code-server] 环境未就绪: ${describeEnvProblem(state.env)}`);
|
|
@@ -906,10 +1005,12 @@ export async function apply(ctx, config) {
|
|
|
906
1005
|
console.log(`[code-server] 检测到旧版安装根 ${legacyRoot}(0.1.35 及更早遗留,已不再使用,可安全删除)`);
|
|
907
1006
|
}
|
|
908
1007
|
|
|
909
|
-
// DSH host 重启后 adopt:pid.json 有效且进程存活且 /healthz 响应 →
|
|
910
|
-
|
|
1008
|
+
// DSH host 重启后 adopt(仅 loopback 模式):pid.json 有效且进程存活且 /healthz 响应 → 接管。
|
|
1009
|
+
// dsh 模式的 launcher 带 --parent-pid 看门狗:host 一退出它就自行退出,不存在可接管的孤儿。
|
|
1010
|
+
if (liveInstance && (record.serve !== 'dsh')) {
|
|
911
1011
|
const probe = await healthCheck(cfg.host, cfg.port, 800);
|
|
912
1012
|
if (probe.ok) {
|
|
1013
|
+
state.serve = 'loopback';
|
|
913
1014
|
state.status = 'running';
|
|
914
1015
|
state.pid = record.pid;
|
|
915
1016
|
state.cwd = record.cwd ?? null;
|
|
@@ -919,9 +1020,11 @@ export async function apply(ctx, config) {
|
|
|
919
1020
|
} else {
|
|
920
1021
|
removePidFile(cfg);
|
|
921
1022
|
}
|
|
1023
|
+
} else if (liveInstance) {
|
|
1024
|
+
removePidFile(cfg); // 上次是 dsh 模式(管道随进程消失)→ 清理陈旧记录
|
|
922
1025
|
}
|
|
923
1026
|
|
|
924
1027
|
maybePrestart();
|
|
925
1028
|
|
|
926
|
-
console.log(`[code-server] static plugin loaded (host=${cfg.host} port=${cfg.port}
|
|
1029
|
+
console.log(`[code-server] static plugin loaded (serve=${requestedServe()} host=${cfg.host} port=${cfg.port})`);
|
|
927
1030
|
}
|