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/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/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/launcher.mjs
ADDED
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/launcher.mjs — VS Code server 的最小启动器(取代 code-server 的 out/node/** 服务层)。
|
|
3
|
+
*
|
|
4
|
+
* 它在**子进程**里加载 <tree>/lib/vscode/out/server-main.js,自建 node:http 把请求交给
|
|
5
|
+
* VS Code 的 handleRequest/handleUpgrade,并补齐 code-server 原先负责的少量 HTTP 面:
|
|
6
|
+
* /healthz 就绪探针(host 轮询;也回传最近一次 upgrade 路径,便于排障)
|
|
7
|
+
* /manifest.json PWA manifest(VS Code server 不提供)
|
|
8
|
+
* /_static/* 浏览器静态资源(favicon / PWA 图标 / serviceWorker.js)
|
|
9
|
+
* /proxy/:port/… 转发端口 HTTP 代理(Ports 面板;WS 版仅在 loopback 模式可用)
|
|
10
|
+
*
|
|
11
|
+
* 为什么是独立进程:VS Code 的 server 会改进程全局(win32 下 import 即 chdir、覆盖
|
|
12
|
+
* Error.stackTraceLimit、注册 SIGPIPE、patch Module._resolveLookupPaths、多处 process.exit),
|
|
13
|
+
* 且 node-pty/sqlite 崩溃时必须只带走 IDE,不能带走 DSH host。
|
|
14
|
+
*
|
|
15
|
+
* 用法(由 lib/index.js 调用,不面向用户):
|
|
16
|
+
* node lib/launcher.mjs --tree <VS Code 树根> --user-data-dir <dir> --extensions-dir <dir> \
|
|
17
|
+
* (--port <n> [--host 127.0.0.1] | --pipe <\\.\pipe\name|unix socket>) \
|
|
18
|
+
* [--parent-pid <pid>] [--locale zh-cn]
|
|
19
|
+
*
|
|
20
|
+
* 就绪信号:stdout 打印 `dshcs-ready <productPath> <mode> <addr>`。
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { createServer as createHttpServer, request as httpRequest } from 'node:http';
|
|
24
|
+
import { createReadStream, existsSync, mkdirSync, readFileSync, statSync } from 'node:fs';
|
|
25
|
+
import { connect } from 'node:net';
|
|
26
|
+
import { dirname, extname, join, normalize, resolve, sep } from 'node:path';
|
|
27
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
28
|
+
|
|
29
|
+
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
30
|
+
const PACKAGE_ROOT = resolve(HERE, '..');
|
|
31
|
+
|
|
32
|
+
// ---------------------------------------------------------------- 参数
|
|
33
|
+
|
|
34
|
+
function parseArgs(argv) {
|
|
35
|
+
const out = {
|
|
36
|
+
tree: process.env.DSHCS_VS_ROOT ?? null,
|
|
37
|
+
userDataDir: null,
|
|
38
|
+
extensionsDir: null,
|
|
39
|
+
host: '127.0.0.1',
|
|
40
|
+
port: null,
|
|
41
|
+
pipe: null,
|
|
42
|
+
parentPid: null,
|
|
43
|
+
locale: null,
|
|
44
|
+
disableProxy: false,
|
|
45
|
+
};
|
|
46
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
47
|
+
const a = argv[i];
|
|
48
|
+
if (a === '--tree') out.tree = argv[++i];
|
|
49
|
+
else if (a === '--user-data-dir') out.userDataDir = argv[++i];
|
|
50
|
+
else if (a === '--extensions-dir') out.extensionsDir = argv[++i];
|
|
51
|
+
else if (a === '--host') out.host = argv[++i];
|
|
52
|
+
else if (a === '--port') out.port = Number(argv[++i]);
|
|
53
|
+
else if (a === '--pipe') out.pipe = argv[++i];
|
|
54
|
+
else if (a === '--parent-pid') out.parentPid = Number(argv[++i]);
|
|
55
|
+
else if (a === '--locale') out.locale = argv[++i];
|
|
56
|
+
else if (a === '--disable-proxy') out.disableProxy = true;
|
|
57
|
+
}
|
|
58
|
+
return out;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const args = parseArgs(process.argv.slice(2));
|
|
62
|
+
const log = (...parts) => console.log('[dshcs-launcher]', ...parts);
|
|
63
|
+
const fail = (message) => { console.error('[dshcs-launcher] FATAL', message); process.exit(2); };
|
|
64
|
+
|
|
65
|
+
if (args.tree === null) fail('缺少 --tree(或环境变量 DSHCS_VS_ROOT)');
|
|
66
|
+
const tree = resolve(args.tree);
|
|
67
|
+
const serverMain = join(tree, 'lib', 'vscode', 'out', 'server-main.js');
|
|
68
|
+
if (!existsSync(serverMain)) fail(`不是一棵 VS Code 树(缺少 ${serverMain})`);
|
|
69
|
+
if (args.pipe === null && !Number.isFinite(args.port)) fail('必须给 --port 或 --pipe');
|
|
70
|
+
|
|
71
|
+
const userDataDir = resolve(args.userDataDir ?? join(PACKAGE_ROOT, '.dshcs-data', 'user-data'));
|
|
72
|
+
const extensionsDir = resolve(args.extensionsDir ?? join(userDataDir, '..', 'extensions'));
|
|
73
|
+
mkdirSync(userDataDir, { recursive: true });
|
|
74
|
+
mkdirSync(extensionsDir, { recursive: true });
|
|
75
|
+
|
|
76
|
+
const product = (() => {
|
|
77
|
+
try { return JSON.parse(readFileSync(join(tree, 'lib', 'vscode', 'product.json'), 'utf8')); } catch { return {}; }
|
|
78
|
+
})();
|
|
79
|
+
const productPath = `${product.quality ?? 'oss'}-${product.commit ?? 'dev'}`;
|
|
80
|
+
|
|
81
|
+
// ---------------------------------------------------------------- 进程护栏(必须 import 前)
|
|
82
|
+
|
|
83
|
+
process.env.CODE_SERVER_PARENT_PID ??= String(process.pid);
|
|
84
|
+
process.env.VSCODE_HANDLES_SIGPIPE ??= '1';
|
|
85
|
+
process.env.VSCODE_CWD ??= process.cwd();
|
|
86
|
+
|
|
87
|
+
let vsServer = null;
|
|
88
|
+
const recentUpgrades = [];
|
|
89
|
+
|
|
90
|
+
async function loadVscodeServer() {
|
|
91
|
+
const cwdBefore = process.cwd();
|
|
92
|
+
const mod = await import(pathToFileURL(serverMain).href);
|
|
93
|
+
// win32 下 server-main 顶层 DB() 会 chdir 到 dirname(process.execPath),立即复位
|
|
94
|
+
try { if (process.cwd() !== cwdBefore) process.chdir(cwdBefore); } catch { /* ignore */ }
|
|
95
|
+
const serverModule = await mod.loadCodeWithNls();
|
|
96
|
+
const codeArgs = {
|
|
97
|
+
auth: 'none',
|
|
98
|
+
'user-data-dir': userDataDir,
|
|
99
|
+
'extensions-dir': extensionsDir,
|
|
100
|
+
'accept-server-license-terms': true,
|
|
101
|
+
compatibility: '1.64',
|
|
102
|
+
'without-connection-token': true,
|
|
103
|
+
'disable-telemetry': true,
|
|
104
|
+
'disable-update-check': true,
|
|
105
|
+
_: [],
|
|
106
|
+
};
|
|
107
|
+
if (args.locale !== null) codeArgs.locale = args.locale;
|
|
108
|
+
return serverModule.createServer(null, codeArgs);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// ---------------------------------------------------------------- 静态资源(/_static/*)
|
|
112
|
+
|
|
113
|
+
const MIME = {
|
|
114
|
+
'.svg': 'image/svg+xml; charset=utf-8',
|
|
115
|
+
'.ico': 'image/x-icon',
|
|
116
|
+
'.png': 'image/png',
|
|
117
|
+
'.jpg': 'image/jpeg',
|
|
118
|
+
'.js': 'text/javascript; charset=utf-8',
|
|
119
|
+
'.css': 'text/css; charset=utf-8',
|
|
120
|
+
'.json': 'application/json; charset=utf-8',
|
|
121
|
+
'.txt': 'text/plain; charset=utf-8',
|
|
122
|
+
'.webmanifest': 'application/manifest+json',
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
function serveTreeStatic(urlPath, res) {
|
|
126
|
+
const rel = decodeURIComponent(urlPath.replace(/^\/_static\/?/, ''));
|
|
127
|
+
const full = normalize(join(tree, rel));
|
|
128
|
+
if (full !== tree && !full.startsWith(tree + sep)) { res.writeHead(403); res.end('forbidden'); return; }
|
|
129
|
+
let stat;
|
|
130
|
+
try { stat = statSync(full); } catch { res.writeHead(404); res.end(); return; }
|
|
131
|
+
if (!stat.isFile()) { res.writeHead(404); res.end(); return; }
|
|
132
|
+
const headers = {
|
|
133
|
+
'content-type': MIME[extname(full).toLowerCase()] ?? 'application/octet-stream',
|
|
134
|
+
'cache-control': 'public, max-age=3600',
|
|
135
|
+
};
|
|
136
|
+
if (full.endsWith('serviceWorker.js')) headers['service-worker-allowed'] = '/';
|
|
137
|
+
res.writeHead(200, headers);
|
|
138
|
+
createReadStream(full).pipe(res);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function manifestBody() {
|
|
142
|
+
return JSON.stringify({
|
|
143
|
+
name: product.nameShort ?? 'code-server',
|
|
144
|
+
short_name: product.nameShort ?? 'code-server',
|
|
145
|
+
start_url: '.',
|
|
146
|
+
display: 'fullscreen',
|
|
147
|
+
display_override: ['window-controls-overlay'],
|
|
148
|
+
description: 'Run Code on a remote server.',
|
|
149
|
+
icons: [192, 512].flatMap((size) => ([
|
|
150
|
+
{ src: `./_static/src/browser/media/pwa-icon-${size}.png`, type: 'image/png', sizes: `${size}x${size}`, purpose: 'any' },
|
|
151
|
+
{ src: `./_static/src/browser/media/pwa-icon-maskable-${size}.png`, type: 'image/png', sizes: `${size}x${size}`, purpose: 'maskable' },
|
|
152
|
+
])),
|
|
153
|
+
}, null, 2);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// ---------------------------------------------------------------- 转发端口代理(/proxy/:port、/absproxy/:port)
|
|
157
|
+
|
|
158
|
+
const PROXY_RE = /^\/(abs)?proxy\/(\d{1,5})(\/.*)?$/;
|
|
159
|
+
|
|
160
|
+
function proxyTarget(url) {
|
|
161
|
+
const m = PROXY_RE.exec(new URL(url, 'http://x').pathname);
|
|
162
|
+
if (m === null) return null;
|
|
163
|
+
const port = Number(m[2]);
|
|
164
|
+
if (!Number.isInteger(port) || port <= 0 || port > 65535) return null;
|
|
165
|
+
const rest = m[3] ?? '/';
|
|
166
|
+
const search = new URL(url, 'http://x').search;
|
|
167
|
+
return { port, path: m[1] === 'abs' ? `${rest}${search}` : `${rest}${search}` };
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function handleProxy(req, res, target) {
|
|
171
|
+
const upstream = httpRequest({
|
|
172
|
+
host: '127.0.0.1',
|
|
173
|
+
port: target.port,
|
|
174
|
+
method: req.method,
|
|
175
|
+
path: target.path,
|
|
176
|
+
headers: stripHopHeaders(req.headers),
|
|
177
|
+
}, (upstreamRes) => {
|
|
178
|
+
res.writeHead(upstreamRes.statusCode ?? 502, upstreamRes.headers);
|
|
179
|
+
upstreamRes.pipe(res);
|
|
180
|
+
});
|
|
181
|
+
upstream.on('error', (error) => {
|
|
182
|
+
log(`proxy ${target.port} failed: ${error.message}`);
|
|
183
|
+
if (!res.headersSent) res.writeHead(502, { 'content-type': 'text/plain; charset=utf-8' });
|
|
184
|
+
res.end(`proxy error: ${error.message}`);
|
|
185
|
+
});
|
|
186
|
+
req.pipe(upstream);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function stripHopHeaders(headers) {
|
|
190
|
+
const out = { ...headers };
|
|
191
|
+
delete out.connection;
|
|
192
|
+
delete out['proxy-connection'];
|
|
193
|
+
return out;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function handleProxyUpgrade(req, socket, head, target) {
|
|
197
|
+
const upstream = connect({ host: '127.0.0.1', port: target.port }, () => {
|
|
198
|
+
const lines = [`GET ${target.path} HTTP/1.1`];
|
|
199
|
+
for (let i = 0; i < req.rawHeaders.length; i += 2) lines.push(`${req.rawHeaders[i]}: ${req.rawHeaders[i + 1]}`);
|
|
200
|
+
upstream.write(`${lines.join('\r\n')}\r\n\r\n`);
|
|
201
|
+
if (head.length > 0) upstream.write(head);
|
|
202
|
+
upstream.pipe(socket);
|
|
203
|
+
socket.pipe(upstream);
|
|
204
|
+
});
|
|
205
|
+
upstream.on('error', (error) => { log(`proxy ws ${target.port} failed: ${error.message}`); socket.destroy(); });
|
|
206
|
+
socket.on('error', () => upstream.destroy());
|
|
207
|
+
socket.on('close', () => upstream.destroy());
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// ---------------------------------------------------------------- HTTP / WS 分发
|
|
211
|
+
|
|
212
|
+
const server = createHttpServer((req, res) => {
|
|
213
|
+
const url = req.url ?? '/';
|
|
214
|
+
if (url === '/healthz') {
|
|
215
|
+
res.writeHead(200, { 'content-type': 'application/json; charset=utf-8', 'cache-control': 'no-store' });
|
|
216
|
+
res.end(JSON.stringify({
|
|
217
|
+
ok: true,
|
|
218
|
+
productPath,
|
|
219
|
+
pid: process.pid,
|
|
220
|
+
mode: args.pipe === null ? 'tcp' : 'pipe',
|
|
221
|
+
recentUpgrades: recentUpgrades.slice(-3),
|
|
222
|
+
}));
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
if (url === '/manifest.json') {
|
|
226
|
+
res.writeHead(200, { 'content-type': 'application/manifest+json; charset=utf-8' });
|
|
227
|
+
res.end(manifestBody());
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
if (url.startsWith('/_static/')) { serveTreeStatic(url, res); return; }
|
|
231
|
+
if (!args.disableProxy) {
|
|
232
|
+
const target = proxyTarget(url);
|
|
233
|
+
if (target !== null) { handleProxy(req, res, target); return; }
|
|
234
|
+
}
|
|
235
|
+
Promise.resolve(vsServer.handleRequest(req, res)).catch((error) => {
|
|
236
|
+
log(`handleRequest failed: ${error && error.stack ? error.stack : error}`);
|
|
237
|
+
if (!res.headersSent) res.writeHead(500, { 'content-type': 'text/plain; charset=utf-8' });
|
|
238
|
+
if (!res.writableEnded) res.end('internal error');
|
|
239
|
+
});
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
server.on('upgrade', (req, socket, head) => {
|
|
243
|
+
const url = req.url ?? '/';
|
|
244
|
+
recentUpgrades.push(url);
|
|
245
|
+
if (recentUpgrades.length > 10) recentUpgrades.shift();
|
|
246
|
+
if (!args.disableProxy) {
|
|
247
|
+
const target = proxyTarget(url);
|
|
248
|
+
if (target !== null) { handleProxyUpgrade(req, socket, head, target); return; }
|
|
249
|
+
}
|
|
250
|
+
socket.pause();
|
|
251
|
+
req.ws = socket;
|
|
252
|
+
req.head = head;
|
|
253
|
+
try {
|
|
254
|
+
vsServer.handleUpgrade(req, socket);
|
|
255
|
+
} catch (error) {
|
|
256
|
+
log(`handleUpgrade threw: ${error && error.message ? error.message : error}`);
|
|
257
|
+
socket.destroy();
|
|
258
|
+
}
|
|
259
|
+
socket.resume();
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
// ---------------------------------------------------------------- 生命周期
|
|
263
|
+
|
|
264
|
+
let shuttingDown = false;
|
|
265
|
+
async function shutdown(reason, code = 0) {
|
|
266
|
+
if (shuttingDown) return;
|
|
267
|
+
shuttingDown = true;
|
|
268
|
+
log(`shutting down (${reason})`);
|
|
269
|
+
try { await vsServer?.dispose?.(); } catch (error) { log(`dispose failed: ${error && error.message}`); }
|
|
270
|
+
const done = () => process.exit(code);
|
|
271
|
+
try { server.close(done); } catch { done(); }
|
|
272
|
+
setTimeout(() => process.exit(code), 3000).unref();
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
for (const sig of ['SIGINT', 'SIGTERM']) process.on(sig, () => { void shutdown(sig); });
|
|
276
|
+
|
|
277
|
+
// 父进程(DSH host)消失 → 自行退出,避免留下孤儿 IDE 进程
|
|
278
|
+
if (Number.isFinite(args.parentPid) && args.parentPid > 0) {
|
|
279
|
+
const parentPid = args.parentPid;
|
|
280
|
+
const timer = setInterval(() => {
|
|
281
|
+
try {
|
|
282
|
+
process.kill(parentPid, 0);
|
|
283
|
+
} catch (error) {
|
|
284
|
+
if (error && error.code === 'ESRCH') void shutdown(`parent ${parentPid} gone`);
|
|
285
|
+
}
|
|
286
|
+
}, 5000);
|
|
287
|
+
timer.unref();
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// ---------------------------------------------------------------- 启动
|
|
291
|
+
|
|
292
|
+
try {
|
|
293
|
+
vsServer = await loadVscodeServer();
|
|
294
|
+
} catch (error) {
|
|
295
|
+
fail(`加载 VS Code server 失败: ${error && error.stack ? error.stack : error}`);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
const listenTarget = args.pipe === null ? { host: args.host, port: args.port } : args.pipe;
|
|
299
|
+
server.on('error', (error) => { fail(`监听失败(${JSON.stringify(listenTarget)}): ${error.message}`); });
|
|
300
|
+
|
|
301
|
+
server.listen(listenTarget, () => {
|
|
302
|
+
const addr = server.address();
|
|
303
|
+
const shown = typeof addr === 'string' ? addr : `${addr.address}:${addr.port}`;
|
|
304
|
+
log(`listening on ${shown} (tree=${tree})`);
|
|
305
|
+
log(`user-data-dir=${userDataDir} extensions-dir=${extensionsDir}`);
|
|
306
|
+
console.log(`dshcs-ready ${productPath} ${args.pipe === null ? 'tcp' : 'pipe'} ${shown}`);
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
// 兜底:未捕获异常不应静默留下半死进程
|
|
310
|
+
process.on('uncaughtException', (error) => {
|
|
311
|
+
console.error('[dshcs-launcher] uncaughtException', error && error.stack ? error.stack : error);
|
|
312
|
+
void shutdown('uncaughtException', 1);
|
|
313
|
+
});
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/serve-dsh.mjs — 把 VS Code server 挂到 DSH 自身 HTTP 端口上的两段式注册(serve=dsh)。
|
|
3
|
+
*
|
|
4
|
+
* ctx.webServer.register({ kind: 'prefix', path: '/code-server', handler }) ← HTTP
|
|
5
|
+
* ctx.webServer.registerUpgrade({ path: '/code-server/<productPath>', handler }) ← WebSocket(精确匹配)
|
|
6
|
+
*
|
|
7
|
+
* 两条路径都必须先过 ctx.connection.requestRejection():DSH 的 webServer 本身不做任何校验,
|
|
8
|
+
* 防护是逐路由的(Host/Origin fence + 浏览器 cookie 认证),缺了它 IDE 会成为源站上唯一的无认证面。
|
|
9
|
+
*
|
|
10
|
+
* 转发策略(Phase 0 实测):**剥掉挂载前缀**后转给 launcher 的命名管道 —— VS Code 侧保持根挂载,
|
|
11
|
+
* 与今天已验证的配置完全一致;客户端渲染出的资源引用全是相对路径,浏览器按 /code-server/ 解析,
|
|
12
|
+
* 因此同一套 prefix 路由就能覆盖 HTML、静态资源、vscode-remote-resource 与 /_static/*。
|
|
13
|
+
*
|
|
14
|
+
* 已知退化:转发端口(/proxy/:port)的 WebSocket 无法用精确升级路由覆盖(端口号在路径里),
|
|
15
|
+
* 需要 DSH 上游提供 prefix upgrade;HTTP 转发端口不受影响。
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { request as httpRequest } from 'node:http';
|
|
19
|
+
import { connect } from 'node:net';
|
|
20
|
+
|
|
21
|
+
export const MOUNT_PATH = '/code-server';
|
|
22
|
+
|
|
23
|
+
/** 前缀匹配(与 DSH webServer 的 prefix 语义一致:命中 p 与 p/<anything>)。 */
|
|
24
|
+
function matchesPrefix(pathname, prefix) {
|
|
25
|
+
return pathname === prefix || pathname.startsWith(`${prefix}/`);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** 把 /code-server/xxx 转成上游要的 /xxx(空则 '/'),保留 query。 */
|
|
29
|
+
function stripPrefix(url, prefix) {
|
|
30
|
+
const rest = url.slice(prefix.length);
|
|
31
|
+
return rest === '' ? '/' : rest;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function rejectionOf(connection, req) {
|
|
35
|
+
try {
|
|
36
|
+
return connection.requestRejection({ headers: req.headers }) ?? undefined;
|
|
37
|
+
} catch (error) {
|
|
38
|
+
return { error };
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** 转发目标的两种形态:命名管道(kind:'pipe')或回环 TCP(kind:'tcp')。
|
|
43
|
+
* 注意 http.request 用 socketPath、net.connect 用 path —— 两者键名不同,必须在这里翻译。 */
|
|
44
|
+
function httpOptions(target, extra) {
|
|
45
|
+
return target.kind === 'pipe'
|
|
46
|
+
? { socketPath: target.pipe, ...extra }
|
|
47
|
+
: { host: target.host, port: target.port, ...extra };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function connectTo(target, onConnect) {
|
|
51
|
+
return target.kind === 'pipe'
|
|
52
|
+
? connect({ path: target.pipe }, onConnect)
|
|
53
|
+
: connect({ host: target.host, port: target.port }, onConnect);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* 注册 IDE 的同源挂载点。
|
|
58
|
+
* @param {object} options
|
|
59
|
+
* @param {object} options.webServer - DSH webServer 服务(register / registerUpgrade)
|
|
60
|
+
* @param {object} options.connection - DSH connection 服务(requestRejection)
|
|
61
|
+
* @param {() => ({socketPath: string} | {host: string, port: number} | null)} options.getTarget
|
|
62
|
+
* 当前 launcher 的转发目标(命名管道为主;loopback TCP 用于测试/回退)
|
|
63
|
+
* @param {string} options.productPath - VS Code 客户端路径(quality-commit)
|
|
64
|
+
* @param {string} [options.mount] - 挂载前缀,默认 /code-server
|
|
65
|
+
* @param {(msg: string) => void} [options.log]
|
|
66
|
+
* @returns {{ disposers: (() => void)[], upgradePath: string }}
|
|
67
|
+
*/
|
|
68
|
+
export function mountOnWebServer({ webServer, connection, getTarget, productPath, mount = MOUNT_PATH, log = () => {} }) {
|
|
69
|
+
const upgradePath = `${mount}/${productPath}`;
|
|
70
|
+
const disposers = [];
|
|
71
|
+
|
|
72
|
+
/** 请求被 fence 拒绝时按 DSH 的约定回 401/403(upgrade 走裸 socket)。 */
|
|
73
|
+
function fence(req) {
|
|
74
|
+
const rejection = rejectionOf(connection, req);
|
|
75
|
+
if (rejection === undefined) return null;
|
|
76
|
+
if (typeof rejection === 'object' && rejection.error !== undefined) {
|
|
77
|
+
log(`requestRejection 抛错,按 403 处理: ${rejection.error?.message ?? rejection.error}`);
|
|
78
|
+
return 403;
|
|
79
|
+
}
|
|
80
|
+
return rejection;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
disposers.push(webServer.register({
|
|
84
|
+
kind: 'prefix',
|
|
85
|
+
path: mount,
|
|
86
|
+
handler: (req, res) => {
|
|
87
|
+
try {
|
|
88
|
+
const pathname = new URL(req.url ?? '/', 'http://x').pathname;
|
|
89
|
+
if (!matchesPrefix(pathname, mount)) { res.writeHead(404); res.end(); return; }
|
|
90
|
+
const status = fence(req);
|
|
91
|
+
if (status !== null) {
|
|
92
|
+
res.writeHead(status, { 'content-type': 'text/plain; charset=utf-8' });
|
|
93
|
+
res.end(status === 401 ? 'unauthorized\n' : 'forbidden\n');
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
const target = getTarget();
|
|
97
|
+
if (target === null) {
|
|
98
|
+
res.writeHead(503, { 'content-type': 'text/plain; charset=utf-8' });
|
|
99
|
+
res.end('code-server 未启动\n');
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
const upstream = httpRequest(httpOptions(target, {
|
|
103
|
+
method: req.method,
|
|
104
|
+
path: stripPrefix(req.url ?? '/', mount),
|
|
105
|
+
headers: req.headers,
|
|
106
|
+
}), (upstreamRes) => {
|
|
107
|
+
res.writeHead(upstreamRes.statusCode ?? 502, upstreamRes.headers);
|
|
108
|
+
upstreamRes.pipe(res);
|
|
109
|
+
});
|
|
110
|
+
upstream.on('error', (error) => {
|
|
111
|
+
log(`HTTP 转发失败(${req.method} ${req.url}): ${error.message}`);
|
|
112
|
+
if (!res.headersSent) res.writeHead(502, { 'content-type': 'text/plain; charset=utf-8' });
|
|
113
|
+
if (!res.writableEnded) res.end('code-server 不可达\n');
|
|
114
|
+
});
|
|
115
|
+
req.on('aborted', () => upstream.destroy());
|
|
116
|
+
req.pipe(upstream);
|
|
117
|
+
} catch (error) {
|
|
118
|
+
// 路由回调里的异常绝不能冒泡到 DSH 的 webServer(会变成未捕获异常)
|
|
119
|
+
log(`HTTP 挂载点异常(${req.method} ${req.url}): ${error && error.stack ? error.stack : error}`);
|
|
120
|
+
if (!res.headersSent) res.writeHead(500, { 'content-type': 'text/plain; charset=utf-8' });
|
|
121
|
+
if (!res.writableEnded) res.end('code-server 挂载点异常\n');
|
|
122
|
+
}
|
|
123
|
+
},
|
|
124
|
+
}));
|
|
125
|
+
|
|
126
|
+
disposers.push(webServer.registerUpgrade({
|
|
127
|
+
path: upgradePath,
|
|
128
|
+
handler: (req, socket, head) => {
|
|
129
|
+
try {
|
|
130
|
+
const status = fence(req);
|
|
131
|
+
if (status !== null) {
|
|
132
|
+
socket.end(`HTTP/1.1 ${status} ${status === 401 ? 'Unauthorized' : 'Forbidden'}\r\nConnection: close\r\n\r\n`);
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
const target = getTarget();
|
|
136
|
+
if (target === null) { socket.destroy(); return; }
|
|
137
|
+
const upstream = connectTo(target, () => {
|
|
138
|
+
const lines = [`GET ${stripPrefix(req.url ?? '/', mount)} HTTP/1.1`];
|
|
139
|
+
for (let i = 0; i < req.rawHeaders.length; i += 2) {
|
|
140
|
+
lines.push(`${req.rawHeaders[i]}: ${req.rawHeaders[i + 1]}`);
|
|
141
|
+
}
|
|
142
|
+
upstream.write(`${lines.join('\r\n')}\r\n\r\n`);
|
|
143
|
+
if (head !== undefined && head.length > 0) upstream.write(head);
|
|
144
|
+
upstream.pipe(socket);
|
|
145
|
+
socket.pipe(upstream);
|
|
146
|
+
});
|
|
147
|
+
upstream.on('error', (error) => { log(`WS 转发失败(${req.url}): ${error.message}`); socket.destroy(); });
|
|
148
|
+
socket.on('error', () => upstream.destroy());
|
|
149
|
+
socket.on('close', () => upstream.destroy());
|
|
150
|
+
} catch (error) {
|
|
151
|
+
log(`WS 挂载点异常(${req.url}): ${error && error.stack ? error.stack : error}`);
|
|
152
|
+
socket.destroy();
|
|
153
|
+
}
|
|
154
|
+
},
|
|
155
|
+
}));
|
|
156
|
+
|
|
157
|
+
log(`已挂载 ${mount}/ (HTTP prefix)+ ${upgradePath} (WS exact)`);
|
|
158
|
+
return {
|
|
159
|
+
upgradePath,
|
|
160
|
+
disposers,
|
|
161
|
+
};
|
|
162
|
+
}
|
package/lib/vendor.js
CHANGED
|
@@ -1,15 +1,17 @@
|
|
|
1
|
-
// lib/vendor.js — 内置
|
|
1
|
+
// lib/vendor.js — 内置 VS Code 产物的路径工具(host 与脚本共用)。
|
|
2
2
|
//
|
|
3
|
-
// 模型(0.
|
|
4
|
-
// -
|
|
5
|
-
// @<scope>/dshcs-
|
|
6
|
-
//
|
|
7
|
-
//
|
|
3
|
+
// 模型(重构后,0.2.0 起):
|
|
4
|
+
// - VS Code 树 + 少量浏览器静态资源打成**平台无关子包**发布:
|
|
5
|
+
// @<scope>/dshcs-vscode-server@<code-server 版本>
|
|
6
|
+
// 子包内布局:<pkg>/vscode/{lib/vscode,out/browser,src/browser,…};
|
|
7
|
+
// 旧的全量树 @<scope>/dshcs-code-server@<版本> 仍然可作回退(0.1.43 及更早)。
|
|
8
|
+
// - code-server 的 Node 服务层(out/node/** + 136 个依赖)不再随包发布:
|
|
9
|
+
// 由插件自带的 lib/launcher.mjs 取代(见 docs/analysis-code-server-as-dsh-plugin.md)。
|
|
8
10
|
// - VS Code 内部依赖(约 1GB)与需编译的原生模块仍由包管理器安装:
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
// - 插件包内 `vendor/
|
|
11
|
+
// 纯 JS 部分写在插件 package.json 的 dependencies;
|
|
12
|
+
// pnpm 拒绝安装的原生包由 scripts/vendor-repacks.mjs 重打包成 @<scope>/dshcs-*,
|
|
13
|
+
// 再由平台聚合包 @<scope>/dsh-code-server-runtime-<platform>-<arch> 用 npm: 别名装回原名字;
|
|
14
|
+
// - 插件包内 `vendor/vscode/` 只作为**开发期/兼容**回退(发布包不含 vendor)。
|
|
13
15
|
import { createRequire } from 'node:module';
|
|
14
16
|
import { existsSync, readFileSync } from 'node:fs';
|
|
15
17
|
import { dirname, join, resolve } from 'node:path';
|
|
@@ -18,8 +20,10 @@ import { fileURLToPath } from 'node:url';
|
|
|
18
20
|
const here = dirname(fileURLToPath(import.meta.url)); // <pkg>/lib
|
|
19
21
|
const require_ = createRequire(import.meta.url);
|
|
20
22
|
export const PACKAGE_ROOT = resolve(here, '..');
|
|
21
|
-
/** 包内
|
|
22
|
-
export const VENDOR_TREE = join(PACKAGE_ROOT, 'vendor', '
|
|
23
|
+
/** 包内 VS Code 树(新模型;发布包不含,仅开发期/兼容回退)。 */
|
|
24
|
+
export const VENDOR_TREE = join(PACKAGE_ROOT, 'vendor', 'vscode');
|
|
25
|
+
/** 包内旧全量 code-server 树(0.1.43 及更早;仅兼容回退)。 */
|
|
26
|
+
export const LEGACY_VENDOR_TREE = join(PACKAGE_ROOT, 'vendor', 'code-server');
|
|
23
27
|
const VENDOR_META = join(PACKAGE_ROOT, 'vendor', 'VENDOR.json');
|
|
24
28
|
/** 旧版安装根目录名(0.1.35 及更早);仅用于迁移提示。 */
|
|
25
29
|
export const APP_DIR_NAME = '.code-server-app';
|
|
@@ -37,14 +41,31 @@ export function legacyInstallRoot() {
|
|
|
37
41
|
return profileRoot !== null ? join(profileRoot, APP_DIR_NAME) : null;
|
|
38
42
|
}
|
|
39
43
|
|
|
40
|
-
|
|
41
|
-
export function codeServerPackageName() {
|
|
44
|
+
function declaredDependencyNames() {
|
|
42
45
|
try {
|
|
43
46
|
const manifest = JSON.parse(readFileSync(join(PACKAGE_ROOT, 'package.json'), 'utf8'));
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
47
|
+
return {
|
|
48
|
+
...(manifest.dependencies ?? {}),
|
|
49
|
+
...(manifest.optionalDependencies ?? {}),
|
|
50
|
+
};
|
|
51
|
+
} catch {
|
|
52
|
+
return {};
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** VS Code 树包名(0.2.0+ 平台无关):取插件 dependencies 里的 @<scope>/dshcs-vscode-server。 */
|
|
57
|
+
export function vscodeServerPackageName() {
|
|
58
|
+
const declared = declaredDependencyNames();
|
|
59
|
+
const hit = Object.keys(declared).find((name) => /^@[^/]+\/dshcs-vscode-server$/.test(name));
|
|
60
|
+
if (hit !== undefined) return hit;
|
|
61
|
+
return '@jinsiyu/dshcs-vscode-server';
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** 旧 code-server 全量树包名(0.1.43 及更早):@<scope>/dshcs-code-server。 */
|
|
65
|
+
export function codeServerPackageName() {
|
|
66
|
+
const declared = declaredDependencyNames();
|
|
67
|
+
const hit = Object.keys(declared).find((name) => /^@[^/]+\/dshcs-code-server$/.test(name));
|
|
68
|
+
if (hit !== undefined) return hit;
|
|
48
69
|
return '@jinsiyu/dshcs-code-server';
|
|
49
70
|
}
|
|
50
71
|
|
|
@@ -53,34 +74,73 @@ export function legacyCodeServerPackageName(platform = process.platform, arch =
|
|
|
53
74
|
return `@jinsiyu/dshcs-code-server-${platform}-${arch}`;
|
|
54
75
|
}
|
|
55
76
|
|
|
56
|
-
/** 解析某个包目录下的
|
|
77
|
+
/** 解析某个包目录下的 VS Code 树根。
|
|
78
|
+
* 新布局:<pkg>/vscode/lib/vscode/out/server-main.js;旧布局:<pkg>/code-server/out/node/entry.js。 */
|
|
57
79
|
function rootOfPackage(name) {
|
|
58
80
|
try {
|
|
59
81
|
const manifest = require_.resolve(`${name}/package.json`);
|
|
60
|
-
const
|
|
61
|
-
|
|
82
|
+
const base = dirname(manifest);
|
|
83
|
+
const slim = join(base, 'vscode');
|
|
84
|
+
if (existsSync(join(slim, 'lib', 'vscode', 'out', 'server-main.js'))) return slim;
|
|
85
|
+
const legacy = join(base, 'code-server');
|
|
86
|
+
if (existsSync(join(legacy, 'out', 'node', 'entry.js'))) return legacy;
|
|
62
87
|
} catch { /* 未安装 */ }
|
|
63
88
|
return null;
|
|
64
89
|
}
|
|
65
90
|
|
|
66
|
-
/**
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
91
|
+
/** 是否是一棵「精简 VS Code 树」(含 lib/vscode/out/server-main.js,不含 code-server 服务层)。 */
|
|
92
|
+
export function isVscodeOnlyTree(root) {
|
|
93
|
+
return root !== null && existsSync(join(root, 'lib', 'vscode', 'out', 'server-main.js'));
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** VS Code 树根(新包 > 旧包 > 旧平台子包 > 包内 vendor/vscode > 包内 vendor/code-server)。
|
|
97
|
+
* @returns {string|null} 绝对路径,如 <profile>\node_modules\@jinsiyu\dshcs-vscode-server\vscode */
|
|
98
|
+
export function vsRoot() {
|
|
99
|
+
const root = rootOfPackage(vscodeServerPackageName())
|
|
100
|
+
?? rootOfPackage(codeServerPackageName())
|
|
101
|
+
?? rootOfPackage(legacyCodeServerPackageName());
|
|
70
102
|
if (root !== null) return root;
|
|
71
|
-
if (existsSync(join(VENDOR_TREE, '
|
|
103
|
+
if (existsSync(join(VENDOR_TREE, 'lib', 'vscode', 'out', 'server-main.js'))) return VENDOR_TREE;
|
|
104
|
+
if (existsSync(join(LEGACY_VENDOR_TREE, 'out', 'node', 'entry.js'))) return LEGACY_VENDOR_TREE;
|
|
72
105
|
return null;
|
|
73
106
|
}
|
|
74
107
|
|
|
75
|
-
/**
|
|
108
|
+
/** VS Code server 入口(<树根>/lib/vscode/out/server-main.js);不可用时返回 null。 */
|
|
109
|
+
export function vsServerEntry() {
|
|
110
|
+
const root = vsRoot();
|
|
111
|
+
if (root === null) return null;
|
|
112
|
+
const entry = join(root, 'lib', 'vscode', 'out', 'server-main.js');
|
|
113
|
+
return existsSync(entry) ? entry : null;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** VS Code 树根(兼容别名:0.1.43 及更早的调用点用这个名字)。 */
|
|
117
|
+
export function codeServerRoot() {
|
|
118
|
+
return vsRoot();
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** code-server 入口脚本(旧全量树的 out/node/entry.js);精简树下返回 null。 */
|
|
76
122
|
export function codeServerEntry() {
|
|
77
|
-
const root =
|
|
78
|
-
|
|
123
|
+
const root = vsRoot();
|
|
124
|
+
if (root === null) return null;
|
|
125
|
+
const entry = join(root, 'out', 'node', 'entry.js');
|
|
126
|
+
return existsSync(entry) ? entry : null;
|
|
79
127
|
}
|
|
80
128
|
|
|
81
129
|
/** code-server 运行根(旧名,脚本/调用方沿用)。 */
|
|
82
130
|
export function codeServerTarget() {
|
|
83
|
-
return
|
|
131
|
+
return vsRoot();
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** VS Code 客户端约定的产品路径(quality-commit);从 product.json 现算,禁止硬编码。 */
|
|
135
|
+
export function productPath(root = vsRoot()) {
|
|
136
|
+
if (root !== null) {
|
|
137
|
+
try {
|
|
138
|
+
const product = JSON.parse(readFileSync(join(root, 'lib', 'vscode', 'product.json'), 'utf8'));
|
|
139
|
+
return `${product.quality ?? 'oss'}-${product.commit ?? 'dev'}`;
|
|
140
|
+
} catch { /* 回退 VENDOR.json */ }
|
|
141
|
+
}
|
|
142
|
+
const meta = readVendorMeta();
|
|
143
|
+
return typeof meta?.productPath === 'string' && meta.productPath !== '' ? meta.productPath : null;
|
|
84
144
|
}
|
|
85
145
|
|
|
86
146
|
export function readTreeVersion(dir) {
|
|
@@ -108,10 +168,11 @@ export function vendoredVersion() {
|
|
|
108
168
|
if (meta !== null && typeof meta.codeServerVersion === 'string' && meta.codeServerVersion !== '') {
|
|
109
169
|
return meta.codeServerVersion;
|
|
110
170
|
}
|
|
111
|
-
return readTreeVersion(VENDOR_TREE);
|
|
171
|
+
return readTreeVersion(VENDOR_TREE) ?? readTreeVersion(LEGACY_VENDOR_TREE);
|
|
112
172
|
}
|
|
113
173
|
|
|
114
|
-
/**
|
|
174
|
+
/** VS Code 树是否可用(包已安装或包内 vendor 在位)。 */
|
|
115
175
|
export function vendorReady() {
|
|
116
|
-
return
|
|
176
|
+
return vsRoot() !== null;
|
|
117
177
|
}
|
|
178
|
+
|