dsh-code-server-app 0.1.43 → 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/lib/index.js CHANGED
@@ -1,5 +1,10 @@
1
1
  /**
2
- * dsh-code-server — host 半部:code-server 进程的启动/停止/状态管理 + /api/code-server JSON API。
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, resolveNativeDir, resolveRuntime, runtimePackageName, verifyNatives } from './native.js';
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: 'code-server',
105
+ bin: '',
94
106
  host: '127.0.0.1',
95
107
  port: 8090,
96
108
  auth: 'none',
97
- passwordToken: '',
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
- /** 插件自带 code-server 入口探测(0.1.36 模型:就地运行,不再复制到 profile):
133
- * 1. 平台专属子包 @<scope>/dshcs-code-server-<platform>-<arch>(0.1.37+ 正式布局,
134
- * 由 pnpm 作为 optionalDependency 安装,内含 code-server/ 子树);
135
- * 2. 包内 vendor/code-server(0.1.36 及更早的随包布局 / 开发期);
136
- * 3. 旧版安装位 <profile>\.code-server-app/node_modules/code-server(兼容 0.1.35 及更早);
137
- * 4. 插件目录/node_modules/code-server(开发期 link: 布局)
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
- const entry = codeServerEntry(); // 子包优先,其次包内 vendor
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 "内置扩展"目录 = code-server 程序根的 lib/vscode/extensions。
159
+ /** 扩展安装目标:VS Code「内置扩展」目录 = <树>/lib/vscode/extensions。
158
160
  * (位于程序内置目录的扩展被 VS Code 视为内置——用户视图显示为"内置",不可卸载;
159
161
  * --extensions-dir 的是用户级扩展,可被用户禁用/卸载。)
160
- * 返回 { dst, builtin }——builtin=true 时为核心路径;找不到 code-server 根则回退用户级。 */
162
+ * 返回 { dst, builtin }——builtin=true 时为核心路径;找不到树则回退用户级。 */
161
163
  function extensionTarget(extensionsDir) {
162
164
  try {
163
- const entry = bundledRuntimeEntry();
164
- if (entry !== null) {
165
- const csRoot = path.dirname(path.dirname(path.dirname(entry))); // .../code-server
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
- /** 从 code-server 运行位置解析内部依赖(与运行时同一套向上查找规则)。
217
- * 先试 `<name>/package.json`(code-server 本体包没有入口文件,ESM-only 包也没有 require 入口),
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 = codeServerRoot();
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
- /** 环境检测:code-server 入口 + 原生模块(argon2) + 内部依赖 + 预编译原生包。
241
- * 返回 { ok, entry, native, vscodeInner, innerDeps, codeServer, vendored, upToDate, nativeRuntime, node, platform, arch }。 */
241
+ /** 环境检测:VS Code + server 入口 + 内部依赖 + 预编译原生包。
242
+ * 返回 { ok, tree, entry, productPath, vscodeInner, innerDeps, treeVersion, vendored, upToDate, nativeRuntime, node, platform, arch }。 */
242
243
  function envCheck() {
243
- const entry = bundledRuntimeEntry();
244
- const csRoot = entry ? path.dirname(path.dirname(path.dirname(entry))) : null; // .../code-server
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 = csRoot !== null ? readTreeVersion(csRoot) : null;
249
+ const installed = root !== null ? readTreeVersion(root) : null;
257
250
  const vendored = vendoredVersion();
258
251
  return {
259
- ok: entry !== null && nativeOk && inner.missing.length === 0
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
- native: nativeOk,
256
+ productPath: productPath(root),
263
257
  vscodeInner: inner.missing.length === 0,
264
258
  innerDeps: { declared: inner.declared, resolved: inner.resolved, missing: inner.missing },
265
- codeServer: installed,
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
- /** 解析 code-server 启动方式:返回 { launched, command, script } | { command, args }。
285
- * - `bin` 为可执行文件(裸名/绝对路径 .exe/.cmd):直接 spawn;
286
- * - `bin` JS 入口(依赖安装的 out/node/entry.js):自动用 node 运行;
287
- * - 未配置 `bin` 时:先探测插件依赖安装的 code-server,再回退 PATH 裸名 code-server。 */
288
- function resolveLaunch(config) {
289
- const probe = bundledRuntimeEntry();
290
- let preferred = config.bin || DEFAULT_CONFIG.bin;
291
- // 依赖安装的 code-server 优先,除非用户显式配置了非默认 bin
292
- if (preferred === DEFAULT_CONFIG.bin && probe !== null) preferred = probe;
293
- console.log(`[code-server] resolveLaunch: configuredBin=${config.bin ?? '(none)'} probe=${probe ?? '(none)'} -> ${preferred}`);
294
- if (/\.(js|mjs|cjs)$/i.test(preferred)) {
295
- // JS 入口:node <entry> ...(code-server out/node/entry.js 是官方发布形态)
296
- if (!fs.existsSync(preferred)) {
297
- throw new Error(`code-server 入口不存在: ${preferred}`);
298
- }
299
- return { kind: 'node', script: preferred };
300
- }
301
- if (path.isAbsolute(preferred)) return { kind: 'bin', command: preferred };
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', [preferred], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] });
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', [preferred], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] });
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
- env: envCheck(), // 环境检测(code-server 入口/native/内部依赖/预编译原生包)
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
- port: state.port,
411
- host: cfg.host,
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
- url: running ? `http://${cfg.host}:${state.port}/` : null,
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 healthCheck(cfg.host, state.port);
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 未就绪);code-server 启动日志尾部:\n${state.logTail.slice(-2000)}`;
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 launch = resolveLaunch(cfg); // throws with install guidance when missing
664
+ const serve = requestedServe(); // loopback | dsh(按 DSH 是否提供 webServer 定)
665
+ const launch = resolveLaunch(cfg, serve); // throws with install guidance when missing
575
666
 
576
- // 认证:非回环 host 必须 password;显式 password 必须带 token
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 = `auth=none 仅允许回环绑定(当前 host="${cfg.host}");请改用 auth=password 并配置 passwordToken`;
669
+ state.error = `serve=loopback 仅允许回环绑定(当前 host="${cfg.host}");`
670
+ + '如需对外提供服务,请改用 serve=dsh(挂到 DSH 同源路径,由 DSH 统一防护)';
581
671
  return snapshot();
582
672
  }
583
- if (auth === 'password' && !cfg.passwordToken) {
584
- state.status = 'error';
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
- const probe = await healthCheck(cfg.host, cfg.port, 800);
591
- if (probe.ok) {
592
- const record = readPidFile(cfg);
593
- if (record && isAlive(record.pid)) {
594
- state.status = 'running';
595
- state.pid = record.pid;
596
- state.cwd = cwd ?? record.cwd ?? null;
597
- state.startedAt = record.startedAt ?? null;
598
- state.adopted = true;
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
- '--bind-addr', `${cfg.host}:${cfg.port}`,
618
- '--auth', auth === 'password' ? 'password' : 'none',
619
- '--user-data-dir', userDataDir,
620
- '--extensions-dir', extensionsDir,
621
- '--disable-telemetry',
622
- '--disable-update-check',
623
- ];
624
- if (cwd !== undefined) args.push(cwd);
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 限定的原生包(os 限定/平台专属)嵌套装在平台聚合包下
638
- // (如 <profile>/node_modules/@<scope>/dsh-code-server-runtime-<平台>-<架构>/node_modules/argon2),
639
- // 那些目录不在 code-server 树的向上查找链里 → 用 NODE_PATH 让子进程的 require 找得到。
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 === 'node' ? process.execPath : launch.command;
652
- const spawnArgs = launch.kind === 'node' ? [launch.script, ...args] : args;
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 === 'node' ? launch.script : launch.command,
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.entry === null) parts.push('缺少包内 vendor/code-server/out/node/entry.js');
725
- if (env.native !== true) parts.push('argon2 原生模块缺失(当前平台无预编译产物)');
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
- // 补齐 code-server 树里被"拍平"掉的依赖布局:
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] 找不到 code-server 运行树:平台子包 ${codeServerPackageName()} 未安装,`
897
- + '且包内 vendor/code-server 不存在(开发期请先 `npm run vendor:code-server`)');
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] code-server 运行树: ${codeServerRoot()}`);
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
- if (liveInstance) {
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} auth=${cfg.auth})`);
1029
+ console.log(`[code-server] static plugin loaded (serve=${requestedServe()} host=${cfg.host} port=${cfg.port})`);
927
1030
  }