dsh-code-server-app 0.1.31 → 0.1.32
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 +25 -8
- package/README.md +24 -7
- package/lib/client.js +3 -3
- package/lib/index.js +114 -121
- package/package.json +2 -2
package/lib/index.js
CHANGED
|
@@ -1,11 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* dsh-code-server — host 半部:code-server 进程的启动/停止/状态管理 + /code-server JSON API。
|
|
2
|
+
* dsh-code-server — host 半部:code-server 进程的启动/停止/状态管理 + /api/code-server JSON API。
|
|
3
3
|
*
|
|
4
4
|
* 零外部依赖:只用 Node 内置模块(child_process / http / fs / path / os)。
|
|
5
5
|
* 静态 profile 插件,与 dsh-webproxy-router-plugin 同形态:
|
|
6
6
|
* - exports.name = 插件名(与 cordis.patch.yml 行 id 一致)
|
|
7
|
-
* - exports.inject = ['
|
|
8
|
-
* - apply(ctx, config)
|
|
7
|
+
* - exports.inject = ['connection', 'settings'](connection 承载 client↔host 通道)
|
|
8
|
+
* - apply(ctx, config) 在共享 /api 通道注册 5 条 exact Fetch 路由:
|
|
9
|
+
* status / start / stop / setup / open-file
|
|
10
|
+
*
|
|
11
|
+
* 通道选择(重要):**不依赖 webServer**。路由经 ctx.connection.fetch.register
|
|
12
|
+
* 挂在 Connection 服务的共享 /api 通道上:
|
|
13
|
+
* - web profile:Connection 自己把 /api 前缀挂到 webServer(带 Host/Origin + 浏览器鉴权);
|
|
14
|
+
* - desktop profile:apps/desktop-host 把 /api/* 交给同一个 createSharedFetchHandler('/api')
|
|
15
|
+
* (IPC 帧管道,无 HTTP 服务器)→ 插件在 desktop 下同样可用。
|
|
16
|
+
* 客户端因此只需同源 fetch('/api/code-server/<op>')(见 src/factory.js 的 api())。
|
|
9
17
|
*
|
|
10
18
|
* 设计要点:
|
|
11
19
|
* - 进程生命周期归本插件:启动写 pid.json,停止用树级终止(taskkill /T 或
|
|
@@ -14,13 +22,13 @@
|
|
|
14
22
|
* running(不重复启动);否则清理 pid.json 视为 stopped。绝不误杀别的进程。
|
|
15
23
|
* - 就绪探测轮询 /healthz;失败时 status 携带启动日志尾部与错误信息。
|
|
16
24
|
* - auth=none 仅允许回环 host;非回环强制 password(未配置 token 则拒绝启动)。
|
|
17
|
-
* - 挂载于 ctx.effect:插件销毁(host 关闭/卸载)
|
|
25
|
+
* - 挂载于 ctx.effect:插件销毁(host 关闭/卸载)时回收自己启动的进程与路由。
|
|
18
26
|
*
|
|
19
|
-
* API(同源 fetch
|
|
20
|
-
* GET /code-server/status → { ok, running, status, port, pid, cwd, url,
|
|
21
|
-
*
|
|
22
|
-
* POST /code-server/start { cwd? } → 幂等启动(换 cwd 则先停后启) → status
|
|
23
|
-
* POST /code-server/stop → 停止 → status
|
|
27
|
+
* API(同源 fetch;web 与 desktop 同一套路径):
|
|
28
|
+
* GET /api/code-server/status → { ok, running, status, port, pid, cwd, url,
|
|
29
|
+
* version, error, logTail[, adopted] }
|
|
30
|
+
* POST /api/code-server/start { cwd? } → 幂等启动(换 cwd 则先停后启) → status
|
|
31
|
+
* POST /api/code-server/stop → 停止 → status
|
|
24
32
|
*/
|
|
25
33
|
|
|
26
34
|
import { spawn, execFile, execFileSync } from 'node:child_process';
|
|
@@ -53,7 +61,9 @@ if (z === null || z === undefined) {
|
|
|
53
61
|
}
|
|
54
62
|
|
|
55
63
|
export const name = 'code-server';
|
|
56
|
-
|
|
64
|
+
// connection:client↔host 通道(web 走 webServer 的 /api,desktop 走 IPC 帧管道);
|
|
65
|
+
// settings:设置卡片的数据域。两者在 web 与 desktop profile 下都存在。
|
|
66
|
+
export const inject = ['connection', 'settings'];
|
|
57
67
|
|
|
58
68
|
/** 设置命名空间:卡片经官方 settings 域读写,持久化到官方 settings 文档。 */
|
|
59
69
|
export const SETTINGS_NS = 'code-server';
|
|
@@ -659,132 +669,113 @@ export async function apply(ctx, config) {
|
|
|
659
669
|
});
|
|
660
670
|
}
|
|
661
671
|
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
672
|
+
// ---- /api/code-server 路由(Connection 共享通道;web 与 desktop 通用) ----
|
|
673
|
+
const API_BASE = '/api/code-server';
|
|
674
|
+
|
|
675
|
+
/** JSON 响应(与旧 webServer JSON API 相同的载荷形状)。 */
|
|
676
|
+
function jsonResponse(value, status = 200) {
|
|
677
|
+
return new Response(JSON.stringify(value), {
|
|
678
|
+
status,
|
|
679
|
+
headers: { 'content-type': 'application/json; charset=utf-8' },
|
|
680
|
+
});
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
/** 读取 JSON 请求体;空体 / 非法 JSON 回退为 {}(旧实现是逐块 best-effort 合并)。 */
|
|
684
|
+
async function readJsonBody(request) {
|
|
665
685
|
try {
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
if (file === null || file === '') {
|
|
712
|
-
payload.error = '需要 file 字段(要打开的绝对路径)';
|
|
713
|
-
payload.status = 400;
|
|
714
|
-
res.writeHead(400, { 'content-type': 'application/json; charset=utf-8' });
|
|
715
|
-
res.end(JSON.stringify(payload));
|
|
716
|
-
return;
|
|
717
|
-
}
|
|
718
|
-
const root = dataRoot(cfg);
|
|
719
|
-
const userDataDir = cfg.userDataDir || path.join(root, 'user-data');
|
|
720
|
-
const signal = openFileSignalPath(userDataDir);
|
|
721
|
-
try {
|
|
722
|
-
fs.mkdirSync(path.dirname(signal), { recursive: true });
|
|
723
|
-
fs.writeFileSync(signal, JSON.stringify({ file, ts: Date.now() }), 'utf8');
|
|
724
|
-
// 同时确保 code-server 运行(信号由扩展消费)
|
|
725
|
-
payload.ok = true;
|
|
726
|
-
payload.signal = signal;
|
|
727
|
-
} catch (err) {
|
|
728
|
-
payload.error = err && err.message ? err.message : String(err);
|
|
729
|
-
}
|
|
730
|
-
} else {
|
|
731
|
-
payload.error = 'unknown route';
|
|
732
|
-
payload.status = 404;
|
|
733
|
-
res.writeHead(404, { 'content-type': 'application/json; charset=utf-8' });
|
|
734
|
-
res.end(JSON.stringify(payload));
|
|
735
|
-
return;
|
|
686
|
+
const value = await request.json();
|
|
687
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value) ? value : {};
|
|
688
|
+
} catch {
|
|
689
|
+
return {};
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
/** 统一的失败载荷(HTTP 200 + ok:false,客户端按 ok 判定)。 */
|
|
694
|
+
function failureResponse(err) {
|
|
695
|
+
return jsonResponse({
|
|
696
|
+
ok: false,
|
|
697
|
+
error: err && err.message ? err.message : String(err),
|
|
698
|
+
runner: 'code-server',
|
|
699
|
+
});
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
async function handleStatus() {
|
|
703
|
+
return jsonResponse(snapshot());
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
async function handleStart(request) {
|
|
707
|
+
try {
|
|
708
|
+
const body = await readJsonBody(request);
|
|
709
|
+
const cwd = typeof body.cwd === 'string' ? body.cwd : undefined;
|
|
710
|
+
return jsonResponse(await start(cwd));
|
|
711
|
+
} catch (err) {
|
|
712
|
+
return failureResponse(err);
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
async function handleStop() {
|
|
717
|
+
try {
|
|
718
|
+
await stop('user');
|
|
719
|
+
return jsonResponse(snapshot());
|
|
720
|
+
} catch (err) {
|
|
721
|
+
return failureResponse(err);
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
async function handleSetup() {
|
|
726
|
+
try {
|
|
727
|
+
// 环境安装:后台执行 setup 脚本(npm 自装 code-server + native + vscode 内部依赖)
|
|
728
|
+
if (state.setup.running) {
|
|
729
|
+
// 已在安装中:同样视为"已发起"(客户端会轮询 setup 状态直至结束)
|
|
730
|
+
return jsonResponse({ ...snapshot(), ok: true, message: '环境安装已在进行中,请等待完成', error: null });
|
|
736
731
|
}
|
|
737
|
-
|
|
738
|
-
|
|
732
|
+
startSetup();
|
|
733
|
+
// 安装任务已启动;服务态 error(如未安装)不掩盖安装态
|
|
734
|
+
return jsonResponse({ ...snapshot(), ok: true });
|
|
739
735
|
} catch (err) {
|
|
740
|
-
|
|
741
|
-
payload.error = err && err.message ? err.message : String(err);
|
|
742
|
-
payload.runner = 'code-server';
|
|
743
|
-
res.writeHead(200, { 'content-type': 'application/json; charset=utf-8' });
|
|
744
|
-
res.end(JSON.stringify(payload));
|
|
736
|
+
return failureResponse(err);
|
|
745
737
|
}
|
|
746
738
|
}
|
|
747
739
|
|
|
748
|
-
|
|
749
|
-
* - /code-server/icon.ico → assets/favicon.ico(标签页/桌面图标)
|
|
750
|
-
* - /code-server/icon.svg → assets/favicon.svg(PWA/深色模式) */
|
|
751
|
-
function handleIcon(req, res) {
|
|
752
|
-
const isSvg = req.url === '/code-server/icon.svg';
|
|
753
|
-
const file = isSvg ? 'favicon.svg' : 'favicon.ico';
|
|
754
|
-
const p = path.join(path.dirname(fileURLToPath(import.meta.url)), '..', 'assets', file);
|
|
740
|
+
async function handleOpenFile(request) {
|
|
755
741
|
try {
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
742
|
+
// 打开文件:host 写信号文件,内置扩展(dshcs-open-file)在 VS Code 中打开它
|
|
743
|
+
const body = await readJsonBody(request);
|
|
744
|
+
const file = typeof body.file === 'string' ? body.file : null;
|
|
745
|
+
if (file === null || file === '') {
|
|
746
|
+
return jsonResponse({ ok: false, error: '需要 file 字段(要打开的绝对路径)' }, 400);
|
|
760
747
|
}
|
|
761
|
-
const
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
});
|
|
766
|
-
|
|
748
|
+
const root = dataRoot(cfg);
|
|
749
|
+
const userDataDir = cfg.userDataDir || path.join(root, 'user-data');
|
|
750
|
+
const signal = openFileSignalPath(userDataDir);
|
|
751
|
+
fs.mkdirSync(path.dirname(signal), { recursive: true });
|
|
752
|
+
fs.writeFileSync(signal, JSON.stringify({ file, ts: Date.now() }), 'utf8');
|
|
753
|
+
return jsonResponse({ ok: true, signal });
|
|
767
754
|
} catch (err) {
|
|
768
|
-
|
|
769
|
-
res.end(JSON.stringify({ ok: false, error: err && err.message ? err.message : String(err) }));
|
|
755
|
+
return failureResponse(err);
|
|
770
756
|
}
|
|
771
757
|
}
|
|
772
758
|
|
|
773
|
-
const
|
|
774
|
-
if (
|
|
775
|
-
console.error('[code-server]
|
|
759
|
+
const connection = ctx.get('connection');
|
|
760
|
+
if (connection === undefined || connection.fetch === undefined) {
|
|
761
|
+
console.error('[code-server] connection service unavailable; plugin registered but idle');
|
|
776
762
|
return;
|
|
777
763
|
}
|
|
778
764
|
|
|
765
|
+
// 每条操作一条 exact Fetch 路由。desktop 的 assetHandler 只把 /api/* 交给
|
|
766
|
+
// createSharedFetchHandler('/api'),所以路径必须落在 /api 下。
|
|
779
767
|
const disposers = [
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
768
|
+
{ path: `${API_BASE}/status`, methods: ['GET'], fetch: handleStatus },
|
|
769
|
+
{ path: `${API_BASE}/start`, methods: ['POST'], fetch: handleStart },
|
|
770
|
+
{ path: `${API_BASE}/stop`, methods: ['POST'], fetch: handleStop },
|
|
771
|
+
{ path: `${API_BASE}/setup`, methods: ['POST'], fetch: handleSetup },
|
|
772
|
+
{ path: `${API_BASE}/open-file`, methods: ['POST'], fetch: handleOpenFile },
|
|
773
|
+
].map(route => connection.fetch.register({
|
|
774
|
+
path: route.path,
|
|
775
|
+
methods: route.methods,
|
|
776
|
+
requestBody: 'buffered',
|
|
777
|
+
fetch: route.fetch,
|
|
778
|
+
}));
|
|
788
779
|
|
|
789
780
|
ctx.effect(() => {
|
|
790
781
|
return () => {
|
|
@@ -792,7 +783,9 @@ export async function apply(ctx, config) {
|
|
|
792
783
|
stopPolling();
|
|
793
784
|
for (const d of disposers) {
|
|
794
785
|
try {
|
|
795
|
-
|
|
786
|
+
// connection.fetch.register 的 disposer 是异步的(返回 Promise);
|
|
787
|
+
// 注册本身也挂在当前 fiber 上,这里显式回收只是双保险。
|
|
788
|
+
Promise.resolve(d()).catch(() => {});
|
|
796
789
|
} catch {
|
|
797
790
|
// ignore
|
|
798
791
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-code-server-app",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "Integrate code-server (VS Code in the browser) into DSH Web: right-sidebar tab (DSH >= 0.1.5-alpha.1) with a floating-ball fallback for older hosts, over
|
|
3
|
+
"version": "0.1.32",
|
|
4
|
+
"description": "Integrate code-server (VS Code in the browser) into DSH Web and Desktop: right-sidebar tab (DSH >= 0.1.5-alpha.1) with a floating-ball fallback for older hosts, over the Connection /api channel (ctx.connection.fetch; no webServer dependency) with host-side process lifecycle.",
|
|
5
5
|
"homepage": "https://github.com/jinsiyu/dsh-code-server-app",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|