dsh-code-server-app 0.2.7 → 0.2.8

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/launcher.mjs CHANGED
@@ -1,362 +1,433 @@
1
- /**
2
- * lib/launcher.mjs — VS Code server 的最小启动器(取代 code-server 的 out/node/** 服务层)。
3
- *
4
- * 在**子进程**里加载 <tree>/lib/vscode/out/server-main.js,自建 node:http 把请求交给 VS Code 的
5
- * handleRequest/handleUpgrade,并补齐 code-server 原先负责的少量 HTTP 面:/healthz(就绪探针)、
6
- * /manifest.json(PWA)、/_static/*(favicon/PWA/serviceWorker)、/proxy/:port/…(端口转发)。
7
- *
8
- * 为什么独立进程:VS Code server 会改进程全局(win32 下 import 即 chdir、注册 SIGPIPE、patch
9
- * Module 解析、多处 process.exit),且 node-pty/sqlite 崩溃时必须只带走 IDE、不能带走 DSH host。
10
- *
11
- * 用法(由 lib/index.js 调用):node lib/launcher.mjs --tree <树根> --user-data-dir <dir>
12
- * --extensions-dir <dir> (--port <n> [--host 127.0.0.1] | --pipe <name>) [--parent-pid <pid>] [--locale zh-cn]
13
- * 就绪信号:stdout 打印 `dshcs-ready <productPath> <mode> <addr>`。
14
- */
15
-
16
- import { createServer as createHttpServer, request as httpRequest } from 'node:http';
17
- import { createReadStream, existsSync, mkdirSync, readFileSync, statSync } from 'node:fs';
18
- import { connect } from 'node:net';
19
- import { dirname, extname, join, normalize, resolve, sep } from 'node:path';
20
- import { fileURLToPath, pathToFileURL } from 'node:url';
21
-
22
- const HERE = dirname(fileURLToPath(import.meta.url));
23
- const PACKAGE_ROOT = resolve(HERE, '..');
24
-
25
- // ---------------------------------------------------------------- 参数
26
-
27
- function parseArgs(argv) {
28
- const out = {
29
- tree: process.env.DSHCS_VS_ROOT ?? null,
30
- userDataDir: null,
31
- extensionsDir: null,
32
- host: '127.0.0.1',
33
- port: null,
34
- pipe: null,
35
- parentPid: null,
36
- locale: null,
37
- disableProxy: false,
38
- };
39
- for (let i = 0; i < argv.length; i += 1) {
40
- const a = argv[i];
41
- if (a === '--tree') out.tree = argv[++i];
42
- else if (a === '--user-data-dir') out.userDataDir = argv[++i];
43
- else if (a === '--extensions-dir') out.extensionsDir = argv[++i];
44
- else if (a === '--host') out.host = argv[++i];
45
- else if (a === '--port') out.port = Number(argv[++i]);
46
- else if (a === '--pipe') out.pipe = argv[++i];
47
- else if (a === '--parent-pid') out.parentPid = Number(argv[++i]);
48
- else if (a === '--locale') out.locale = argv[++i];
49
- else if (a === '--disable-proxy') out.disableProxy = true;
50
- }
51
- return out;
52
- }
53
-
54
- const args = parseArgs(process.argv.slice(2));
55
- const log = (...parts) => console.log('[dshcs-launcher]', ...parts);
56
- const fail = (message) => { console.error('[dshcs-launcher] FATAL', message); process.exit(2); };
57
-
58
- if (args.tree === null) fail('缺少 --tree(或环境变量 DSHCS_VS_ROOT)');
59
- const tree = resolve(args.tree);
60
- const serverMain = join(tree, 'lib', 'vscode', 'out', 'server-main.js');
61
- if (!existsSync(serverMain)) fail(`不是一棵 VS Code 树(缺少 ${serverMain})`);
62
- if (args.pipe === null && !Number.isFinite(args.port)) fail('必须给 --port 或 --pipe');
63
-
64
- const userDataDir = resolve(args.userDataDir ?? join(PACKAGE_ROOT, '.dshcs-data', 'user-data'));
65
- const extensionsDir = resolve(args.extensionsDir ?? join(userDataDir, '..', 'extensions'));
66
- mkdirSync(userDataDir, { recursive: true });
67
- mkdirSync(extensionsDir, { recursive: true });
68
-
69
- const product = (() => {
70
- try { return JSON.parse(readFileSync(join(tree, 'lib', 'vscode', 'product.json'), 'utf8')); } catch { return {}; }
71
- })();
72
- const productPath = `${product.quality ?? 'oss'}-${product.commit ?? 'dev'}`;
73
-
74
- // ---------------------------------------------------------------- 进程护栏(必须 import 前)
75
-
76
- process.env.CODE_SERVER_PARENT_PID ??= String(process.pid);
77
- process.env.VSCODE_HANDLES_SIGPIPE ??= '1';
78
- process.env.VSCODE_CWD ??= process.cwd();
79
-
80
- let vsServer = null;
81
- const recentUpgrades = [];
82
-
83
- async function loadVscodeServer() {
84
- const cwdBefore = process.cwd();
85
- const mod = await import(pathToFileURL(serverMain).href);
86
- // win32 下 server-main 顶层 DB() 会 chdir 到 dirname(process.execPath),立即复位
87
- try { if (process.cwd() !== cwdBefore) process.chdir(cwdBefore); } catch { /* ignore */ }
88
- const serverModule = await mod.loadCodeWithNls();
89
- const codeArgs = {
90
- auth: 'none',
91
- 'user-data-dir': userDataDir,
92
- 'extensions-dir': extensionsDir,
93
- 'accept-server-license-terms': true,
94
- compatibility: '1.64',
95
- 'without-connection-token': true,
96
- 'disable-telemetry': true,
97
- 'disable-update-check': true,
98
- _: [],
99
- };
100
- if (args.locale !== null) codeArgs.locale = args.locale;
101
- return serverModule.createServer(null, codeArgs);
102
- }
103
-
104
- // ---------------------------------------------------------------- 静态资源(/_static/*)
105
-
106
- const MIME = {
107
- '.svg': 'image/svg+xml; charset=utf-8',
108
- '.ico': 'image/x-icon',
109
- '.png': 'image/png',
110
- '.jpg': 'image/jpeg',
111
- '.js': 'text/javascript; charset=utf-8',
112
- '.css': 'text/css; charset=utf-8',
113
- '.json': 'application/json; charset=utf-8',
114
- '.txt': 'text/plain; charset=utf-8',
115
- '.webmanifest': 'application/manifest+json',
116
- };
117
-
118
- function serveTreeStatic(urlPath, res) {
119
- const rel = decodeURIComponent(urlPath.replace(/^\/_static\/?/, ''));
120
- const full = normalize(join(tree, rel));
121
- if (full !== tree && !full.startsWith(tree + sep)) { res.writeHead(403); res.end('forbidden'); return; }
122
- let stat;
123
- try { stat = statSync(full); } catch { res.writeHead(404); res.end(); return; }
124
- if (!stat.isFile()) { res.writeHead(404); res.end(); return; }
125
- const headers = {
126
- 'content-type': MIME[extname(full).toLowerCase()] ?? 'application/octet-stream',
127
- 'cache-control': 'public, max-age=3600',
128
- };
129
- if (full.endsWith('serviceWorker.js')) headers['service-worker-allowed'] = '/';
130
- res.writeHead(200, headers);
131
- createReadStream(full).pipe(res);
132
- }
133
-
134
- function manifestBody() {
135
- return JSON.stringify({
136
- name: product.nameShort ?? 'code-server',
137
- short_name: product.nameShort ?? 'code-server',
138
- start_url: '.',
139
- display: 'fullscreen',
140
- display_override: ['window-controls-overlay'],
141
- description: 'Run Code on a remote server.',
142
- icons: [192, 512].flatMap((size) => ([
143
- { src: `./_static/src/browser/media/pwa-icon-${size}.png`, type: 'image/png', sizes: `${size}x${size}`, purpose: 'any' },
144
- { src: `./_static/src/browser/media/pwa-icon-maskable-${size}.png`, type: 'image/png', sizes: `${size}x${size}`, purpose: 'maskable' },
145
- ])),
146
- }, null, 2);
147
- }
148
-
149
- // ---------------------------------------------------------------- 转发端口代理(/proxy/:port、/absproxy/:port)
150
-
151
- const PROXY_RE = /^\/(abs)?proxy\/(\d{1,5})(\/.*)?$/;
152
-
153
- function proxyTarget(url) {
154
- const m = PROXY_RE.exec(new URL(url, 'http://x').pathname);
155
- if (m === null) return null;
156
- const port = Number(m[2]);
157
- if (!Number.isInteger(port) || port <= 0 || port > 65535) return null;
158
- const rest = m[3] ?? '/';
159
- const search = new URL(url, 'http://x').search;
160
- return { port, path: m[1] === 'abs' ? `${rest}${search}` : `${rest}${search}` };
161
- }
162
-
163
- function handleProxy(req, res, target) {
164
- const upstream = httpRequest({
165
- host: '127.0.0.1',
166
- port: target.port,
167
- method: req.method,
168
- path: target.path,
169
- headers: stripHopHeaders(req.headers),
170
- }, (upstreamRes) => {
171
- res.writeHead(upstreamRes.statusCode ?? 502, upstreamRes.headers);
172
- upstreamRes.pipe(res);
173
- });
174
- upstream.on('error', (error) => {
175
- log(`proxy ${target.port} failed: ${error.message}`);
176
- if (!res.headersSent) res.writeHead(502, { 'content-type': 'text/plain; charset=utf-8' });
177
- res.end(`proxy error: ${error.message}`);
178
- });
179
- req.pipe(upstream);
180
- }
181
-
182
- function stripHopHeaders(headers) {
183
- const out = { ...headers };
184
- delete out.connection;
185
- delete out['proxy-connection'];
186
- return out;
187
- }
188
-
189
- function handleProxyUpgrade(req, socket, head, target) {
190
- const upstream = connect({ host: '127.0.0.1', port: target.port }, () => {
191
- const lines = [`GET ${target.path} HTTP/1.1`];
192
- for (let i = 0; i < req.rawHeaders.length; i += 2) lines.push(`${req.rawHeaders[i]}: ${req.rawHeaders[i + 1]}`);
193
- upstream.write(`${lines.join('\r\n')}\r\n\r\n`);
194
- if (head.length > 0) upstream.write(head);
195
- upstream.pipe(socket);
196
- socket.pipe(upstream);
197
- });
198
- upstream.on('error', (error) => { log(`proxy ws ${target.port} failed: ${error.message}`); socket.destroy(); });
199
- socket.on('error', () => upstream.destroy());
200
- socket.on('close', () => upstream.destroy());
201
- }
202
-
203
- // ---------------------------------------------------------------- 跨源防护(等价 code-server 的 ensureOrigin)
204
-
205
- /** 取请求 Host:优先 `Forwarded: host=`、其次 `X-Forwarded-Host`(取第一个)、最后 `Host`。
206
- * trim + 小写,与 code-server 的 getHost() 同语义(反向代理未透传 Host 时返回 undefined)。 */
207
- function getHostHeader(req) {
208
- const first = (name) => {
209
- const value = req.headers[name];
210
- return Array.isArray(value) ? value[0] : value;
211
- };
212
- const forwarded = first('forwarded');
213
- if (forwarded !== undefined && forwarded !== '') {
214
- for (const part of forwarded.split(/[;,]/)) {
215
- const eq = part.indexOf('=');
216
- if (eq < 0) continue;
217
- const key = part.slice(0, eq).trim().toLowerCase();
218
- const value = part.slice(eq + 1).trim();
219
- if (key === 'host' && value !== '') return value.toLowerCase();
220
- }
221
- }
222
- const xHost = first('x-forwarded-host');
223
- if (xHost !== undefined && xHost !== '') {
224
- const head = xHost.split(',')[0];
225
- if (head !== undefined && head.trim() !== '') return head.trim().toLowerCase();
226
- }
227
- const host = first('host');
228
- return host !== undefined && host !== '' ? host.trim().toLowerCase() : undefined;
229
- }
230
-
231
- /** code-server `authenticateOrigin()` 的等价物:带 Origin 的请求(浏览器) host 必须等于 Host;
232
- * 缺 Origin(非浏览器,如本地工具/测试)放行。code-server 另外支持 `--trusted-origins` /
233
- * `--proxy-domain` 通配,本插件没有这两个概念,故不实现。
234
- * 为什么必须有:VS Code handleUpgrade 不校验来源,而 IDE auth=none —— 缺了这道检查,
235
- * 本机任意浏览器页面都能开 ws://127.0.0.1:<port>/stable-<commit> 直接驱动 IDE。
236
- * (dsh 模式下请求来自 DSH 自己的路由并已过 requestRejection,Origin/Host 天然一致。) */
237
- function originAllowed(req) {
238
- const raw = req.headers.origin;
239
- const originRaw = Array.isArray(raw) ? raw[0] : raw;
240
- if (originRaw === undefined || originRaw === '') return true;
241
- let origin;
242
- try {
243
- origin = new URL(originRaw).host.trim().toLowerCase();
244
- } catch {
245
- return false;
246
- }
247
- if (origin === '') return false;
248
- const host = getHostHeader(req);
249
- if (host === undefined) return false;
250
- return host === origin;
251
- }
252
-
253
- // ---------------------------------------------------------------- HTTP / WS 分发
254
-
255
- const server = createHttpServer((req, res) => {
256
- const url = req.url ?? '/';
257
- if (url === '/healthz') {
258
- res.writeHead(200, { 'content-type': 'application/json; charset=utf-8', 'cache-control': 'no-store' });
259
- res.end(JSON.stringify({
260
- ok: true,
261
- productPath,
262
- pid: process.pid,
263
- mode: args.pipe === null ? 'tcp' : 'pipe',
264
- recentUpgrades: recentUpgrades.slice(-3),
265
- }));
266
- return;
267
- }
268
- if (url === '/manifest.json') {
269
- res.writeHead(200, { 'content-type': 'application/manifest+json; charset=utf-8' });
270
- res.end(manifestBody());
271
- return;
272
- }
273
- if (url.startsWith('/_static/')) { serveTreeStatic(url, res); return; }
274
- if (!args.disableProxy) {
275
- const target = proxyTarget(url);
276
- if (target !== null) { handleProxy(req, res, target); return; }
277
- }
278
- Promise.resolve(vsServer.handleRequest(req, res)).catch((error) => {
279
- log(`handleRequest failed: ${error && error.stack ? error.stack : error}`);
280
- if (!res.headersSent) res.writeHead(500, { 'content-type': 'text/plain; charset=utf-8' });
281
- if (!res.writableEnded) res.end('internal error');
282
- });
283
- });
284
-
285
- server.on('upgrade', (req, socket, head) => {
286
- const url = req.url ?? '/';
287
- recentUpgrades.push(url);
288
- if (recentUpgrades.length > 10) recentUpgrades.shift();
289
- // 跨源防护(与 code-server 一致:只卡 upgrade,HTTP 侧 VS Code 仅接受 GET 且状态变更都走 WS)
290
- if (!originAllowed(req)) {
291
- log(`拒绝跨源 WebSocket:origin=${req.headers.origin} host=${req.headers.host ?? '-'} url=${url}`);
292
- socket.end('HTTP/1.1 403 Forbidden\r\nConnection: close\r\nContent-Length: 0\r\n\r\n');
293
- return;
294
- }
295
- if (!args.disableProxy) {
296
- const target = proxyTarget(url);
297
- if (target !== null) { handleProxyUpgrade(req, socket, head, target); return; }
298
- }
299
- socket.pause();
300
- req.ws = socket;
301
- req.head = head;
302
- try {
303
- vsServer.handleUpgrade(req, socket);
304
- } catch (error) {
305
- log(`handleUpgrade threw: ${error && error.message ? error.message : error}`);
306
- socket.destroy();
307
- }
308
- socket.resume();
309
- });
310
-
311
- // ---------------------------------------------------------------- 生命周期
312
-
313
- let shuttingDown = false;
314
- async function shutdown(reason, code = 0) {
315
- if (shuttingDown) return;
316
- shuttingDown = true;
317
- log(`shutting down (${reason})`);
318
- try { await vsServer?.dispose?.(); } catch (error) { log(`dispose failed: ${error && error.message}`); }
319
- const done = () => process.exit(code);
320
- try { server.close(done); } catch { done(); }
321
- setTimeout(() => process.exit(code), 3000).unref();
322
- }
323
-
324
- for (const sig of ['SIGINT', 'SIGTERM']) process.on(sig, () => { void shutdown(sig); });
325
-
326
- // 父进程(DSH host)消失 → 自行退出,避免留下孤儿 IDE 进程
327
- if (Number.isFinite(args.parentPid) && args.parentPid > 0) {
328
- const parentPid = args.parentPid;
329
- const timer = setInterval(() => {
330
- try {
331
- process.kill(parentPid, 0);
332
- } catch (error) {
333
- if (error && error.code === 'ESRCH') void shutdown(`parent ${parentPid} gone`);
334
- }
335
- }, 5000);
336
- timer.unref();
337
- }
338
-
339
- // ---------------------------------------------------------------- 启动
340
-
341
- try {
342
- vsServer = await loadVscodeServer();
343
- } catch (error) {
344
- fail(`加载 VS Code server 失败: ${error && error.stack ? error.stack : error}`);
345
- }
346
-
347
- const listenTarget = args.pipe === null ? { host: args.host, port: args.port } : args.pipe;
348
- server.on('error', (error) => { fail(`监听失败(${JSON.stringify(listenTarget)}): ${error.message}`); });
349
-
350
- server.listen(listenTarget, () => {
351
- const addr = server.address();
352
- const shown = typeof addr === 'string' ? addr : `${addr.address}:${addr.port}`;
353
- log(`listening on ${shown} (tree=${tree})`);
354
- log(`user-data-dir=${userDataDir} extensions-dir=${extensionsDir}`);
355
- console.log(`dshcs-ready ${productPath} ${args.pipe === null ? 'tcp' : 'pipe'} ${shown}`);
356
- });
357
-
358
- // 兜底:未捕获异常不应静默留下半死进程
359
- process.on('uncaughtException', (error) => {
360
- console.error('[dshcs-launcher] uncaughtException', error && error.stack ? error.stack : error);
361
- void shutdown('uncaughtException', 1);
362
- });
1
+ /**
2
+ * lib/launcher.mjs — VS Code server 的最小启动器(取代 code-server 的 out/node/** 服务层)。
3
+ *
4
+ * 在**子进程**里加载 <tree>/lib/vscode/out/server-main.js,自建 node:http 把请求交给 VS Code 的
5
+ * handleRequest/handleUpgrade,并补齐 code-server 原先负责的少量 HTTP 面:/healthz(就绪探针)、
6
+ * /manifest.json(PWA)、/_static/*(favicon/PWA/serviceWorker)、/proxy/:port/…(端口转发)。
7
+ *
8
+ * 为什么独立进程:VS Code server 会改进程全局(win32 下 import 即 chdir、注册 SIGPIPE、patch
9
+ * Module 解析、多处 process.exit),且 node-pty/sqlite 崩溃时必须只带走 IDE、不能带走 DSH host。
10
+ *
11
+ * 用法(由 lib/index.js 调用):node lib/launcher.mjs --tree <树根> --user-data-dir <dir>
12
+ * --extensions-dir <dir> (--port <n> [--host 127.0.0.1] | --pipe <name>) [--parent-pid <pid>] [--locale zh-cn]
13
+ * 就绪信号:stdout 打印 `dshcs-ready <productPath> <mode> <addr>`。
14
+ */
15
+
16
+ import { createServer as createHttpServer, request as httpRequest } from 'node:http';
17
+ import { createReadStream, existsSync, mkdirSync, readFileSync, statSync } from 'node:fs';
18
+ import { connect } from 'node:net';
19
+ import { dirname, extname, join, normalize, resolve, sep } from 'node:path';
20
+ import { fileURLToPath, pathToFileURL } from 'node:url';
21
+ import { gunzipSync } from 'node:zlib';
22
+
23
+ const HERE = dirname(fileURLToPath(import.meta.url));
24
+ const PACKAGE_ROOT = resolve(HERE, '..');
25
+
26
+ // ---------------------------------------------------------------- 参数
27
+
28
+ function parseArgs(argv) {
29
+ const out = {
30
+ tree: process.env.DSHCS_VS_ROOT ?? null,
31
+ userDataDir: null,
32
+ extensionsDir: null,
33
+ host: '127.0.0.1',
34
+ port: null,
35
+ pipe: null,
36
+ parentPid: null,
37
+ locale: null,
38
+ disableProxy: false,
39
+ /** ?v= 缓存击穿标记(host DSHCS_HTML_TAG;为空则每次启动生成一个) */
40
+ htmlTag: process.env.DSHCS_HTML_TAG ?? null,
41
+ };
42
+ for (let i = 0; i < argv.length; i += 1) {
43
+ const a = argv[i];
44
+ if (a === '--tree') out.tree = argv[++i];
45
+ else if (a === '--user-data-dir') out.userDataDir = argv[++i];
46
+ else if (a === '--extensions-dir') out.extensionsDir = argv[++i];
47
+ else if (a === '--host') out.host = argv[++i];
48
+ else if (a === '--port') out.port = Number(argv[++i]);
49
+ else if (a === '--pipe') out.pipe = argv[++i];
50
+ else if (a === '--parent-pid') out.parentPid = Number(argv[++i]);
51
+ else if (a === '--locale') out.locale = argv[++i];
52
+ else if (a === '--disable-proxy') out.disableProxy = true;
53
+ }
54
+ return out;
55
+ }
56
+
57
+ const args = parseArgs(process.argv.slice(2));
58
+ const log = (...parts) => console.log('[dshcs-launcher]', ...parts);
59
+ const fail = (message) => { console.error('[dshcs-launcher] FATAL', message); process.exit(2); };
60
+
61
+ if (args.tree === null) fail('缺少 --tree(或环境变量 DSHCS_VS_ROOT)');
62
+ const tree = resolve(args.tree);
63
+ const serverMain = join(tree, 'lib', 'vscode', 'out', 'server-main.js');
64
+ if (!existsSync(serverMain)) fail(`不是一棵 VS Code 树(缺少 ${serverMain})`);
65
+ if (args.pipe === null && !Number.isFinite(args.port)) fail('必须给 --port 或 --pipe');
66
+
67
+ const userDataDir = resolve(args.userDataDir ?? join(PACKAGE_ROOT, '.dshcs-data', 'user-data'));
68
+ const extensionsDir = resolve(args.extensionsDir ?? join(userDataDir, '..', 'extensions'));
69
+ mkdirSync(userDataDir, { recursive: true });
70
+ mkdirSync(extensionsDir, { recursive: true });
71
+
72
+ const product = (() => {
73
+ try { return JSON.parse(readFileSync(join(tree, 'lib', 'vscode', 'product.json'), 'utf8')); } catch { return {}; }
74
+ })();
75
+ const productPath = `${product.quality ?? 'oss'}-${product.commit ?? 'dev'}`;
76
+
77
+ // ---------------------------------------------------------------- 进程护栏(必须 import 前)
78
+
79
+ process.env.CODE_SERVER_PARENT_PID ??= String(process.pid);
80
+ process.env.VSCODE_HANDLES_SIGPIPE ??= '1';
81
+ process.env.VSCODE_CWD ??= process.cwd();
82
+
83
+ let vsServer = null;
84
+ const recentUpgrades = [];
85
+
86
+ async function loadVscodeServer() {
87
+ const cwdBefore = process.cwd();
88
+ const mod = await import(pathToFileURL(serverMain).href);
89
+ // win32 server-main 顶层 DB() 会 chdir 到 dirname(process.execPath),立即复位
90
+ try { if (process.cwd() !== cwdBefore) process.chdir(cwdBefore); } catch { /* ignore */ }
91
+ const serverModule = await mod.loadCodeWithNls();
92
+ const codeArgs = {
93
+ auth: 'none',
94
+ 'user-data-dir': userDataDir,
95
+ 'extensions-dir': extensionsDir,
96
+ 'accept-server-license-terms': true,
97
+ compatibility: '1.64',
98
+ 'without-connection-token': true,
99
+ 'disable-telemetry': true,
100
+ 'disable-update-check': true,
101
+ _: [],
102
+ };
103
+ if (args.locale !== null) codeArgs.locale = args.locale;
104
+ return serverModule.createServer(null, codeArgs);
105
+ }
106
+
107
+ // ---------------------------------------------------------------- 静态资源(/_static/*)
108
+
109
+ const MIME = {
110
+ '.svg': 'image/svg+xml; charset=utf-8',
111
+ '.ico': 'image/x-icon',
112
+ '.png': 'image/png',
113
+ '.jpg': 'image/jpeg',
114
+ '.js': 'text/javascript; charset=utf-8',
115
+ '.css': 'text/css; charset=utf-8',
116
+ '.json': 'application/json; charset=utf-8',
117
+ '.txt': 'text/plain; charset=utf-8',
118
+ '.webmanifest': 'application/manifest+json',
119
+ };
120
+
121
+ function serveTreeStatic(urlPath, res) {
122
+ const rel = decodeURIComponent(urlPath.replace(/^\/_static\/?/, ''));
123
+ const full = normalize(join(tree, rel));
124
+ if (full !== tree && !full.startsWith(tree + sep)) { res.writeHead(403); res.end('forbidden'); return; }
125
+ let stat;
126
+ try { stat = statSync(full); } catch { res.writeHead(404); res.end(); return; }
127
+ if (!stat.isFile()) { res.writeHead(404); res.end(); return; }
128
+ const headers = {
129
+ 'content-type': MIME[extname(full).toLowerCase()] ?? 'application/octet-stream',
130
+ 'cache-control': 'public, max-age=3600',
131
+ };
132
+ if (full.endsWith('serviceWorker.js')) headers['service-worker-allowed'] = '/';
133
+ res.writeHead(200, headers);
134
+ createReadStream(full).pipe(res);
135
+ }
136
+
137
+ function manifestBody() {
138
+ return JSON.stringify({
139
+ name: product.nameShort ?? 'code-server',
140
+ short_name: product.nameShort ?? 'code-server',
141
+ start_url: '.',
142
+ display: 'fullscreen',
143
+ display_override: ['window-controls-overlay'],
144
+ description: 'Run Code on a remote server.',
145
+ icons: [192, 512].flatMap((size) => ([
146
+ { src: `./_static/src/browser/media/pwa-icon-${size}.png`, type: 'image/png', sizes: `${size}x${size}`, purpose: 'any' },
147
+ { src: `./_static/src/browser/media/pwa-icon-maskable-${size}.png`, type: 'image/png', sizes: `${size}x${size}`, purpose: 'maskable' },
148
+ ])),
149
+ }, null, 2);
150
+ }
151
+
152
+ // ---------------------------------------------------------------- 转发端口代理(/proxy/:port、/absproxy/:port)
153
+
154
+ const PROXY_RE = /^\/(abs)?proxy\/(\d{1,5})(\/.*)?$/;
155
+
156
+ function proxyTarget(url) {
157
+ const m = PROXY_RE.exec(new URL(url, 'http://x').pathname);
158
+ if (m === null) return null;
159
+ const port = Number(m[2]);
160
+ if (!Number.isInteger(port) || port <= 0 || port > 65535) return null;
161
+ const rest = m[3] ?? '/';
162
+ const search = new URL(url, 'http://x').search;
163
+ return { port, path: m[1] === 'abs' ? `${rest}${search}` : `${rest}${search}` };
164
+ }
165
+
166
+ function handleProxy(req, res, target) {
167
+ const upstream = httpRequest({
168
+ host: '127.0.0.1',
169
+ port: target.port,
170
+ method: req.method,
171
+ path: target.path,
172
+ headers: stripHopHeaders(req.headers),
173
+ }, (upstreamRes) => {
174
+ res.writeHead(upstreamRes.statusCode ?? 502, upstreamRes.headers);
175
+ upstreamRes.pipe(res);
176
+ });
177
+ upstream.on('error', (error) => {
178
+ log(`proxy ${target.port} failed: ${error.message}`);
179
+ if (!res.headersSent) res.writeHead(502, { 'content-type': 'text/plain; charset=utf-8' });
180
+ res.end(`proxy error: ${error.message}`);
181
+ });
182
+ req.pipe(upstream);
183
+ }
184
+
185
+ function stripHopHeaders(headers) {
186
+ const out = { ...headers };
187
+ delete out.connection;
188
+ delete out['proxy-connection'];
189
+ return out;
190
+ }
191
+
192
+ function handleProxyUpgrade(req, socket, head, target) {
193
+ const upstream = connect({ host: '127.0.0.1', port: target.port }, () => {
194
+ const lines = [`GET ${target.path} HTTP/1.1`];
195
+ for (let i = 0; i < req.rawHeaders.length; i += 2) lines.push(`${req.rawHeaders[i]}: ${req.rawHeaders[i + 1]}`);
196
+ upstream.write(`${lines.join('\r\n')}\r\n\r\n`);
197
+ if (head.length > 0) upstream.write(head);
198
+ upstream.pipe(socket);
199
+ socket.pipe(upstream);
200
+ });
201
+ upstream.on('error', (error) => { log(`proxy ws ${target.port} failed: ${error.message}`); socket.destroy(); });
202
+ socket.on('error', () => upstream.destroy());
203
+ socket.on('close', () => upstream.destroy());
204
+ }
205
+
206
+ // ---------------------------------------------------------------- 跨源防护(等价 code-server 的 ensureOrigin)
207
+
208
+ /** 取请求 Host:优先 `Forwarded: host=`、其次 `X-Forwarded-Host`(取第一个)、最后 `Host`。
209
+ * trim + 小写,与 code-server 的 getHost() 同语义(反向代理未透传 Host 时返回 undefined)。 */
210
+ function getHostHeader(req) {
211
+ const first = (name) => {
212
+ const value = req.headers[name];
213
+ return Array.isArray(value) ? value[0] : value;
214
+ };
215
+ const forwarded = first('forwarded');
216
+ if (forwarded !== undefined && forwarded !== '') {
217
+ for (const part of forwarded.split(/[;,]/)) {
218
+ const eq = part.indexOf('=');
219
+ if (eq < 0) continue;
220
+ const key = part.slice(0, eq).trim().toLowerCase();
221
+ const value = part.slice(eq + 1).trim();
222
+ if (key === 'host' && value !== '') return value.toLowerCase();
223
+ }
224
+ }
225
+ const xHost = first('x-forwarded-host');
226
+ if (xHost !== undefined && xHost !== '') {
227
+ const head = xHost.split(',')[0];
228
+ if (head !== undefined && head.trim() !== '') return head.trim().toLowerCase();
229
+ }
230
+ const host = first('host');
231
+ return host !== undefined && host !== '' ? host.trim().toLowerCase() : undefined;
232
+ }
233
+
234
+ /** code-server `authenticateOrigin()` 的等价物:带 Origin 的请求(浏览器)其 host 必须等于 Host;
235
+ * Origin(非浏览器,如本地工具/测试)放行。code-server 另外支持 `--trusted-origins` /
236
+ * `--proxy-domain` 通配,本插件没有这两个概念,故不实现。
237
+ * 为什么必须有:VS Code 的 handleUpgrade 不校验来源,而 IDE 是 auth=none —— 缺了这道检查,
238
+ * 本机任意浏览器页面都能开 ws://127.0.0.1:<port>/stable-<commit> 直接驱动 IDE。
239
+ * (dsh 模式下请求来自 DSH 自己的路由并已过 requestRejection,Origin/Host 天然一致。) */
240
+ function originAllowed(req) {
241
+ const raw = req.headers.origin;
242
+ const originRaw = Array.isArray(raw) ? raw[0] : raw;
243
+ if (originRaw === undefined || originRaw === '') return true;
244
+ let origin;
245
+ try {
246
+ origin = new URL(originRaw).host.trim().toLowerCase();
247
+ } catch {
248
+ return false;
249
+ }
250
+ if (origin === '') return false;
251
+ const host = getHostHeader(req);
252
+ if (host === undefined) return false;
253
+ return host === origin;
254
+ }
255
+
256
+ // ---------------------------------------------------------------- HTTP / WS 分发
257
+
258
+ /** 工作台 HTML 的响应改写:给 workbench.js / workbench.css / nls 资源加 `?v=<tag>` 缓存击穿。
259
+ *
260
+ * 为什么必须有:这些资源带 `cache-control: public, max-age=31536000`,而**渲染器的缓存按 URL 走** ——
261
+ * 插件或 VS Code 树升级后沿用同一个 URL,渲染器会继续跑**旧 bundle**(表现为界面/配色行为诡异,
262
+ * 与服务器不一致)。README 里那条"清 Cache/Code Cache/GPUCache"就是它的手工绕法。 */
263
+ function rewriteWorkbenchHtml(html) {
264
+ const tag = args.htmlTag !== null && args.htmlTag !== '' ? args.htmlTag : `t${Date.now()}`;
265
+ let out = html.replace(/([^"'=]*\/workbench\.(?:js|css))"/g, `$1?v=${tag}"`);
266
+ out = out.replace(/([^"'=]*\/nls\.messages\.js)"/g, `$1?v=${tag}"`);
267
+ return out;
268
+ }
269
+
270
+ /** 缓冲响应体后改写(GET / 用):去掉 content-length/content-encoding,并改成 no-store。 */
271
+ function interceptHtmlResponse(res) {
272
+ const chunks = [];
273
+ const originalEnd = res.end.bind(res);
274
+ const originalWriteHead = res.writeHead.bind(res);
275
+ let status = 200;
276
+ let headers = {};
277
+ res.writeHead = (code, reason, hdrs) => {
278
+ status = code;
279
+ const extra = typeof reason === 'object' && reason !== null ? reason : hdrs;
280
+ headers = typeof reason === 'string' ? (hdrs ?? {}) : (extra ?? {});
281
+ return res;
282
+ };
283
+ res.write = (chunk, encoding, callback) => {
284
+ if (chunk !== undefined && chunk !== null) {
285
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk, typeof encoding === 'string' ? encoding : 'utf8'));
286
+ }
287
+ if (typeof encoding === 'function') encoding();
288
+ else if (typeof callback === 'function') callback();
289
+ return true;
290
+ };
291
+ res.end = (chunk, encoding, callback) => {
292
+ if (chunk !== undefined && chunk !== null && typeof chunk !== 'function') {
293
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk, typeof encoding === 'string' ? encoding : 'utf8'));
294
+ }
295
+ const clean = { ...(typeof res.getHeaders === 'function' ? res.getHeaders() : {}), ...headers };
296
+ let body = Buffer.concat(chunks);
297
+ // 要就地改字节(请求侧已删掉 accept-encoding,正常不会压缩;压缩了也兜一层)
298
+ const encodingHeader = String(clean['content-encoding'] ?? clean['Content-Encoding'] ?? '').toLowerCase();
299
+ if (encodingHeader.includes('gzip')) {
300
+ try { body = gunzipSync(body); } catch { /* 原样透传 */ }
301
+ }
302
+ if (status === 200) body = Buffer.from(rewriteWorkbenchHtml(body.toString('utf8')), 'utf8');
303
+ for (const name of ['content-length', 'Content-Length', 'content-encoding', 'Content-Encoding']) delete clean[name];
304
+ clean['content-length'] = String(body.length);
305
+ clean['cache-control'] = 'no-store';
306
+ originalWriteHead(status, clean);
307
+ return originalEnd(body);
308
+ };
309
+ }
310
+
311
+ const server = createHttpServer((req, res) => {
312
+ const url = req.url ?? '/';
313
+ // **按路径匹配,别拿整串比**:客户端加载文档时永远带查询串(`?folder=<cwd>`),
314
+ // `url === '/'` 判断会让改写整条失效(0.3.x 就踩过这个坑);`/healthz`、`/manifest.json`
315
+ // 同理要用路径匹配,否则带查询串的探针会掉到 VS Code 那边变成 404。
316
+ const urlPath = url.split('?')[0];
317
+ if (urlPath === '/healthz') {
318
+ res.writeHead(200, { 'content-type': 'application/json; charset=utf-8', 'cache-control': 'no-store' });
319
+ res.end(JSON.stringify({
320
+ ok: true,
321
+ productPath,
322
+ pid: process.pid,
323
+ mode: args.pipe === null ? 'tcp' : 'pipe',
324
+ htmlTag: args.htmlTag,
325
+ recentUpgrades: recentUpgrades.slice(-3),
326
+ }));
327
+ return;
328
+ }
329
+ if (urlPath === '/manifest.json') {
330
+ res.writeHead(200, { 'content-type': 'application/manifest+json; charset=utf-8' });
331
+ res.end(manifestBody());
332
+ return;
333
+ }
334
+ if (urlPath.startsWith('/_static/')) { serveTreeStatic(urlPath, res); return; }
335
+ if (urlPath === '/' || urlPath === '/index.html') {
336
+ // 工作台 HTML:缓冲改写(只加 ?v= 缓存击穿;不做任何注入)
337
+ delete req.headers['accept-encoding']; // 要就地改字节,让上游发未压缩的
338
+ interceptHtmlResponse(res);
339
+ Promise.resolve(vsServer.handleRequest(req, res)).catch((error) => {
340
+ log(`handleRequest(html) failed: ${error && error.stack ? error.stack : error}`);
341
+ try { res.end(); } catch { /* ignore */ }
342
+ });
343
+ return;
344
+ }
345
+ if (!args.disableProxy) {
346
+ const target = proxyTarget(url);
347
+ if (target !== null) { handleProxy(req, res, target); return; }
348
+ }
349
+ Promise.resolve(vsServer.handleRequest(req, res)).catch((error) => {
350
+ log(`handleRequest failed: ${error && error.stack ? error.stack : error}`);
351
+ if (!res.headersSent) res.writeHead(500, { 'content-type': 'text/plain; charset=utf-8' });
352
+ if (!res.writableEnded) res.end('internal error');
353
+ });
354
+ });
355
+
356
+ server.on('upgrade', (req, socket, head) => {
357
+ const url = req.url ?? '/';
358
+ recentUpgrades.push(url);
359
+ if (recentUpgrades.length > 10) recentUpgrades.shift();
360
+ // 跨源防护(与 code-server 一致:只卡 upgrade,HTTP VS Code 仅接受 GET 且状态变更都走 WS)
361
+ if (!originAllowed(req)) {
362
+ log(`拒绝跨源 WebSocket:origin=${req.headers.origin} host=${req.headers.host ?? '-'} url=${url}`);
363
+ socket.end('HTTP/1.1 403 Forbidden\r\nConnection: close\r\nContent-Length: 0\r\n\r\n');
364
+ return;
365
+ }
366
+ if (!args.disableProxy) {
367
+ const target = proxyTarget(url);
368
+ if (target !== null) { handleProxyUpgrade(req, socket, head, target); return; }
369
+ }
370
+ socket.pause();
371
+ req.ws = socket;
372
+ req.head = head;
373
+ try {
374
+ vsServer.handleUpgrade(req, socket);
375
+ } catch (error) {
376
+ log(`handleUpgrade threw: ${error && error.message ? error.message : error}`);
377
+ socket.destroy();
378
+ }
379
+ socket.resume();
380
+ });
381
+
382
+ // ---------------------------------------------------------------- 生命周期
383
+
384
+ let shuttingDown = false;
385
+ async function shutdown(reason, code = 0) {
386
+ if (shuttingDown) return;
387
+ shuttingDown = true;
388
+ log(`shutting down (${reason})`);
389
+ try { await vsServer?.dispose?.(); } catch (error) { log(`dispose failed: ${error && error.message}`); }
390
+ const done = () => process.exit(code);
391
+ try { server.close(done); } catch { done(); }
392
+ setTimeout(() => process.exit(code), 3000).unref();
393
+ }
394
+
395
+ for (const sig of ['SIGINT', 'SIGTERM']) process.on(sig, () => { void shutdown(sig); });
396
+
397
+ // 父进程(DSH host)消失 → 自行退出,避免留下孤儿 IDE 进程
398
+ if (Number.isFinite(args.parentPid) && args.parentPid > 0) {
399
+ const parentPid = args.parentPid;
400
+ const timer = setInterval(() => {
401
+ try {
402
+ process.kill(parentPid, 0);
403
+ } catch (error) {
404
+ if (error && error.code === 'ESRCH') void shutdown(`parent ${parentPid} gone`);
405
+ }
406
+ }, 5000);
407
+ timer.unref();
408
+ }
409
+
410
+ // ---------------------------------------------------------------- 启动
411
+
412
+ try {
413
+ vsServer = await loadVscodeServer();
414
+ } catch (error) {
415
+ fail(`加载 VS Code server 失败: ${error && error.stack ? error.stack : error}`);
416
+ }
417
+
418
+ const listenTarget = args.pipe === null ? { host: args.host, port: args.port } : args.pipe;
419
+ server.on('error', (error) => { fail(`监听失败(${JSON.stringify(listenTarget)}): ${error.message}`); });
420
+
421
+ server.listen(listenTarget, () => {
422
+ const addr = server.address();
423
+ const shown = typeof addr === 'string' ? addr : `${addr.address}:${addr.port}`;
424
+ log(`listening on ${shown} (tree=${tree})`);
425
+ log(`user-data-dir=${userDataDir} extensions-dir=${extensionsDir}`);
426
+ console.log(`dshcs-ready ${productPath} ${args.pipe === null ? 'tcp' : 'pipe'} ${shown}`);
427
+ });
428
+
429
+ // 兜底:未捕获异常不应静默留下半死进程
430
+ process.on('uncaughtException', (error) => {
431
+ console.error('[dshcs-launcher] uncaughtException', error && error.stack ? error.stack : error);
432
+ void shutdown('uncaughtException', 1);
433
+ });