dsh-code-server-app 0.1.17 → 0.1.20
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/LICENSE +21 -0
- package/README.md +4 -3
- package/assets/extensions/dshcs-open-file/extension.js +61 -0
- package/assets/extensions/dshcs-open-file/package.json +14 -0
- package/assets/favicon.ico +0 -0
- package/assets/favicon.svg +4 -0
- package/lib/client.js +3 -3
- package/lib/index.js +804 -665
- package/package.json +56 -52
- package/scripts/setup-code-server.mjs +155 -155
package/lib/index.js
CHANGED
|
@@ -1,665 +1,804 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* dsh-code-server — host 半部:code-server 进程的启动/停止/状态管理 + /code-server JSON API。
|
|
3
|
-
*
|
|
4
|
-
* 零外部依赖:只用 Node 内置模块(child_process / http / fs / path / os)。
|
|
5
|
-
* 静态 profile 插件,与 dsh-webproxy-router-plugin 同形态:
|
|
6
|
-
* - exports.name = 插件名(与 cordis.patch.yml 行 id 一致)
|
|
7
|
-
* - exports.inject = ['webServer'](硬依赖:等待 webserver 服务就绪)
|
|
8
|
-
* - apply(ctx, config) 注册 3 条 exact JSON 路由:status / start / stop
|
|
9
|
-
*
|
|
10
|
-
* 设计要点:
|
|
11
|
-
* - 进程生命周期归本插件:启动写 pid.json,停止用树级终止(taskkill /T 或
|
|
12
|
-
* 进程组 SIGTERM→SIGKILL),退出监听更新状态。
|
|
13
|
-
* - host 重启后 adopt:pid.json 中的进程仍存活且 /healthz 响应 → 接管为
|
|
14
|
-
* running(不重复启动);否则清理 pid.json 视为 stopped。绝不误杀别的进程。
|
|
15
|
-
* - 就绪探测轮询 /healthz;失败时 status 携带启动日志尾部与错误信息。
|
|
16
|
-
* - auth=none 仅允许回环 host;非回环强制 password(未配置 token 则拒绝启动)。
|
|
17
|
-
* - 挂载于 ctx.effect:插件销毁(host 关闭/卸载)时回收自己启动的进程。
|
|
18
|
-
*
|
|
19
|
-
* API(同源 fetch,与 webproxy-plugin 的 webServer JSON API 同机制):
|
|
20
|
-
* GET /code-server/status → { ok, running, status, port, pid, cwd, url,
|
|
21
|
-
* version, error, logTail[, adopted] }
|
|
22
|
-
* POST /code-server/start { cwd? } → 幂等启动(换 cwd 则先停后启) → status
|
|
23
|
-
* POST /code-server/stop → 停止 → status
|
|
24
|
-
*/
|
|
25
|
-
|
|
26
|
-
import { spawn, execFile, execFileSync } from 'node:child_process';
|
|
27
|
-
import * as fs from 'node:fs';
|
|
28
|
-
import * as path from 'node:path';
|
|
29
|
-
import * as os from 'node:os';
|
|
30
|
-
import * as http from 'node:http';
|
|
31
|
-
import { fileURLToPath } from 'node:url';
|
|
32
|
-
import { createRequire } from 'node:module';
|
|
33
|
-
|
|
34
|
-
// schemastery 由 DSH 部署自带(官方核心依赖),仿 auto-open-web 的解析策略:
|
|
35
|
-
// 常规 import 优先,不可用时回退到全局 npm 布局的 DSH 部署副本。
|
|
36
|
-
let z = null;
|
|
37
|
-
try {
|
|
38
|
-
z = (await import('@deepseek-ai/schemastery')).default;
|
|
39
|
-
} catch {
|
|
40
|
-
try {
|
|
41
|
-
const globalRoot = process.env.APPDATA ? path.join(process.env.APPDATA, 'npm', 'node_modules') : '';
|
|
42
|
-
const dshEntry = path.join(globalRoot, '@deepseek-ai', 'dsh', 'package.json');
|
|
43
|
-
if (fs.existsSync(dshEntry)) z = createRequire(dshEntry)('@deepseek-ai/schemastery');
|
|
44
|
-
} catch {
|
|
45
|
-
/* 两次解析均失败 → 下方抛错 */
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
if (z === null || z === undefined) {
|
|
49
|
-
throw new Error(
|
|
50
|
-
'[code-server] schemastery not found (neither local nor DSH deployment); ' +
|
|
51
|
-
'this plugin cannot build its settings schema. Check the DSH deployment.',
|
|
52
|
-
);
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
export const name = 'code-server';
|
|
56
|
-
export const inject = ['webServer', 'settings'];
|
|
57
|
-
|
|
58
|
-
/** 设置命名空间:卡片经官方 settings 域读写,持久化到官方 settings 文档。 */
|
|
59
|
-
export const SETTINGS_NS = 'code-server';
|
|
60
|
-
|
|
61
|
-
/** 设置卡片 schema(参照 auto-open-web 的 Config 形态)。 */
|
|
62
|
-
export const Config = z.object({
|
|
63
|
-
/** reserveComposer=true(默认)窗口不盖输入框(初始/缩放/最大化止于输入栏上方);
|
|
64
|
-
* false 时允许盖住输入框(最大化到视口底)。 */
|
|
65
|
-
reserveComposer: z.boolean().default(true),
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
*
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
function
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
return
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
'
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
state.status = 'error';
|
|
487
|
-
state.error =
|
|
488
|
-
return snapshot();
|
|
489
|
-
}
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
state.
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
state.
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
}
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
}
|
|
638
|
-
|
|
639
|
-
const
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* dsh-code-server — host 半部:code-server 进程的启动/停止/状态管理 + /code-server JSON API。
|
|
3
|
+
*
|
|
4
|
+
* 零外部依赖:只用 Node 内置模块(child_process / http / fs / path / os)。
|
|
5
|
+
* 静态 profile 插件,与 dsh-webproxy-router-plugin 同形态:
|
|
6
|
+
* - exports.name = 插件名(与 cordis.patch.yml 行 id 一致)
|
|
7
|
+
* - exports.inject = ['webServer'](硬依赖:等待 webserver 服务就绪)
|
|
8
|
+
* - apply(ctx, config) 注册 3 条 exact JSON 路由:status / start / stop
|
|
9
|
+
*
|
|
10
|
+
* 设计要点:
|
|
11
|
+
* - 进程生命周期归本插件:启动写 pid.json,停止用树级终止(taskkill /T 或
|
|
12
|
+
* 进程组 SIGTERM→SIGKILL),退出监听更新状态。
|
|
13
|
+
* - host 重启后 adopt:pid.json 中的进程仍存活且 /healthz 响应 → 接管为
|
|
14
|
+
* running(不重复启动);否则清理 pid.json 视为 stopped。绝不误杀别的进程。
|
|
15
|
+
* - 就绪探测轮询 /healthz;失败时 status 携带启动日志尾部与错误信息。
|
|
16
|
+
* - auth=none 仅允许回环 host;非回环强制 password(未配置 token 则拒绝启动)。
|
|
17
|
+
* - 挂载于 ctx.effect:插件销毁(host 关闭/卸载)时回收自己启动的进程。
|
|
18
|
+
*
|
|
19
|
+
* API(同源 fetch,与 webproxy-plugin 的 webServer JSON API 同机制):
|
|
20
|
+
* GET /code-server/status → { ok, running, status, port, pid, cwd, url,
|
|
21
|
+
* version, error, logTail[, adopted] }
|
|
22
|
+
* POST /code-server/start { cwd? } → 幂等启动(换 cwd 则先停后启) → status
|
|
23
|
+
* POST /code-server/stop → 停止 → status
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import { spawn, execFile, execFileSync } from 'node:child_process';
|
|
27
|
+
import * as fs from 'node:fs';
|
|
28
|
+
import * as path from 'node:path';
|
|
29
|
+
import * as os from 'node:os';
|
|
30
|
+
import * as http from 'node:http';
|
|
31
|
+
import { fileURLToPath } from 'node:url';
|
|
32
|
+
import { createRequire } from 'node:module';
|
|
33
|
+
|
|
34
|
+
// schemastery 由 DSH 部署自带(官方核心依赖),仿 auto-open-web 的解析策略:
|
|
35
|
+
// 常规 import 优先,不可用时回退到全局 npm 布局的 DSH 部署副本。
|
|
36
|
+
let z = null;
|
|
37
|
+
try {
|
|
38
|
+
z = (await import('@deepseek-ai/schemastery')).default;
|
|
39
|
+
} catch {
|
|
40
|
+
try {
|
|
41
|
+
const globalRoot = process.env.APPDATA ? path.join(process.env.APPDATA, 'npm', 'node_modules') : '';
|
|
42
|
+
const dshEntry = path.join(globalRoot, '@deepseek-ai', 'dsh', 'package.json');
|
|
43
|
+
if (fs.existsSync(dshEntry)) z = createRequire(dshEntry)('@deepseek-ai/schemastery');
|
|
44
|
+
} catch {
|
|
45
|
+
/* 两次解析均失败 → 下方抛错 */
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
if (z === null || z === undefined) {
|
|
49
|
+
throw new Error(
|
|
50
|
+
'[code-server] schemastery not found (neither local nor DSH deployment); ' +
|
|
51
|
+
'this plugin cannot build its settings schema. Check the DSH deployment.',
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export const name = 'code-server';
|
|
56
|
+
export const inject = ['webServer', 'settings'];
|
|
57
|
+
|
|
58
|
+
/** 设置命名空间:卡片经官方 settings 域读写,持久化到官方 settings 文档。 */
|
|
59
|
+
export const SETTINGS_NS = 'code-server';
|
|
60
|
+
|
|
61
|
+
/** 设置卡片 schema(参照 auto-open-web 的 Config 形态)。 */
|
|
62
|
+
export const Config = z.object({
|
|
63
|
+
/** reserveComposer=true(默认)窗口不盖输入框(初始/缩放/最大化止于输入栏上方);
|
|
64
|
+
* false 时允许盖住输入框(最大化到视口底)。 */
|
|
65
|
+
reserveComposer: z.boolean().default(true),
|
|
66
|
+
/** windowedOpen=false(默认)点击悬浮球打开内部浮动窗口;
|
|
67
|
+
* true 时改为在浏览器新标签页打开 code-server(自动启动并跟随工作区)。 */
|
|
68
|
+
windowedOpen: z.boolean().default(false),
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
const DEFAULT_CONFIG = {
|
|
72
|
+
bin: 'code-server',
|
|
73
|
+
host: '127.0.0.1',
|
|
74
|
+
port: 8090,
|
|
75
|
+
auth: 'none',
|
|
76
|
+
passwordToken: '',
|
|
77
|
+
userDataDir: '',
|
|
78
|
+
extensionsDir: '',
|
|
79
|
+
readyTimeoutMs: 60000,
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const LOOPBACK_HOSTS = new Set(['127.0.0.1', 'localhost', '::1']);
|
|
83
|
+
const LOG_TAIL_MAX = 6000;
|
|
84
|
+
|
|
85
|
+
function dshHome() {
|
|
86
|
+
return process.env.DSH_HOME || path.join(os.homedir(), '.dsh');
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function dataRoot(config) {
|
|
90
|
+
return path.join(dshHome(), 'code-server');
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function pidFile(config) {
|
|
94
|
+
return path.join(dataRoot(config), 'pid.json');
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function isAlive(pid) {
|
|
98
|
+
if (!Number.isInteger(pid) || pid <= 0) return false;
|
|
99
|
+
try {
|
|
100
|
+
process.kill(pid, 0);
|
|
101
|
+
return true;
|
|
102
|
+
} catch (err) {
|
|
103
|
+
return err && err.code === 'EPERM';
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function win32() {
|
|
108
|
+
return process.platform === 'win32';
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** 插件自带 code-server 入口探测,兼容两种依赖布局:
|
|
112
|
+
* 1. profile 专用目录:profileRoot/.code-server-app/node_modules/code-server/...
|
|
113
|
+
* (方案 D 默认安装位,独立项目、避开 profile 依赖树冲突);
|
|
114
|
+
* 2. 包内:插件目录/node_modules/code-server/...(开发期 link:/独立 npm install 布局)。
|
|
115
|
+
* 不存在 → 回退配置/PATH。 */
|
|
116
|
+
function bundledRuntimeEntry() {
|
|
117
|
+
try {
|
|
118
|
+
const here = path.dirname(fileURLToPath(import.meta.url)); // .../dsh-code-server-app/lib 或 .../dsh-code-server/lib
|
|
119
|
+
const candidates = [
|
|
120
|
+
// profile 专用目录:here=.../dsh-code-server/lib → ../..=node_modules → ../../..=profile根
|
|
121
|
+
path.join(here, '..', '..', '..', '.code-server-app', 'node_modules', 'code-server', 'out', 'node', 'entry.js'),
|
|
122
|
+
// 包内(dev link / 独立目录):here=.../lib → ../node_modules
|
|
123
|
+
path.join(here, '..', 'node_modules', 'code-server', 'out', 'node', 'entry.js'),
|
|
124
|
+
];
|
|
125
|
+
for (const entry of candidates) {
|
|
126
|
+
if (fs.existsSync(entry)) return entry;
|
|
127
|
+
}
|
|
128
|
+
return null;
|
|
129
|
+
} catch {
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** 扩展安装目标:VS Code "内置扩展"目录 = code-server 程序根的 lib/vscode/extensions。
|
|
135
|
+
* (位于程序内置目录的扩展被 VS Code 视为内置——用户视图显示为"内置",不可卸载;
|
|
136
|
+
* --extensions-dir 的是用户级扩展,可被用户禁用/卸载。)
|
|
137
|
+
* 返回 { dst, builtin }——builtin=true 时为核心路径;找不到 code-server 根则回退用户级。 */
|
|
138
|
+
function extensionTarget(extensionsDir) {
|
|
139
|
+
try {
|
|
140
|
+
const entry = bundledRuntimeEntry();
|
|
141
|
+
if (entry !== null) {
|
|
142
|
+
const csRoot = path.dirname(path.dirname(path.dirname(entry))); // .../code-server
|
|
143
|
+
const vscodeExt = path.join(csRoot, 'lib', 'vscode', 'extensions');
|
|
144
|
+
if (fs.existsSync(vscodeExt)) {
|
|
145
|
+
return { dst: path.join(vscodeExt, 'dshcs-open-file'), builtin: true };
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
} catch { /* fall through */ }
|
|
149
|
+
return { dst: path.join(extensionsDir, 'dshcs-open-file'), builtin: false };
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** 内置扩展安装:dshcs-open-file(host 信号文件 → VS Code 打开文件)。
|
|
153
|
+
* 优先装进 code-server 内置扩展目录(不可卸载);同时清理用户级旧副本。
|
|
154
|
+
* 每次启动调用:缺失即拷(自愈,code-server 升级后自动找回)。 */
|
|
155
|
+
function installBundledExtension(extensionsDir, userDataDir) {
|
|
156
|
+
try {
|
|
157
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
158
|
+
const src = path.join(here, '..', 'assets', 'extensions', 'dshcs-open-file');
|
|
159
|
+
if (!fs.existsSync(path.join(src, 'package.json'))) return;
|
|
160
|
+
const target = extensionTarget(extensionsDir);
|
|
161
|
+
const dst = target.dst;
|
|
162
|
+
if (!fs.existsSync(path.join(dst, 'extension.js'))) {
|
|
163
|
+
fs.mkdirSync(dst, { recursive: true });
|
|
164
|
+
fs.copyFileSync(path.join(src, 'package.json'), path.join(dst, 'package.json'));
|
|
165
|
+
fs.copyFileSync(path.join(src, 'extension.js'), path.join(dst, 'extension.js'));
|
|
166
|
+
}
|
|
167
|
+
// 清理用户级旧副本(避免重复/可卸载副本)
|
|
168
|
+
const legacy = path.join(extensionsDir, 'dshcs-open-file');
|
|
169
|
+
if (legacy !== dst && fs.existsSync(legacy)) {
|
|
170
|
+
fs.rmSync(legacy, { recursive: true, force: true });
|
|
171
|
+
}
|
|
172
|
+
console.log(`[code-server] bundled extension dshcs-open-file -> ${dst}${target.builtin ? ' (内置,不可卸载)' : ' (用户级回退)'}`);
|
|
173
|
+
} catch (err) {
|
|
174
|
+
console.warn('[code-server] bundled extension install failed:', err && err.message ? err.message : String(err));
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** 打开文件信号文件:<user-data>/User/dshcs-open.json(扩展轮询此文件)。 */
|
|
179
|
+
function openFileSignalPath(userDataDir) {
|
|
180
|
+
return path.join(userDataDir, 'User', 'dshcs-open.json');
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/** 环境检测:code-server 入口 + 原生模块 + VS Code 内部依赖 是否就绪。
|
|
184
|
+
* 返回 { ok, entry, native, vscodeInner, node, platform, arch, pathToSetup }。 */
|
|
185
|
+
function envCheck() {
|
|
186
|
+
const entry = bundledRuntimeEntry();
|
|
187
|
+
const csRoot = entry ? path.dirname(path.dirname(path.dirname(entry))) : null; // .../code-server
|
|
188
|
+
const nativeOk = csRoot !== null && (
|
|
189
|
+
fs.existsSync(path.join(csRoot, 'node_modules', 'argon2', 'build', 'Release', 'argon2.node')) ||
|
|
190
|
+
fs.existsSync(path.join(csRoot, 'node_modules', 'argon2', 'prebuilds'))
|
|
191
|
+
);
|
|
192
|
+
const innerOk = csRoot !== null &&
|
|
193
|
+
fs.existsSync(path.join(csRoot, 'lib', 'vscode', 'node_modules'));
|
|
194
|
+
return {
|
|
195
|
+
ok: entry !== null && nativeOk && innerOk,
|
|
196
|
+
entry: entry !== null ? entry.replace(/\\/g, '/') : null,
|
|
197
|
+
native: nativeOk,
|
|
198
|
+
vscodeInner: innerOk,
|
|
199
|
+
node: process.version,
|
|
200
|
+
platform: process.platform,
|
|
201
|
+
arch: process.arch,
|
|
202
|
+
pathToSetup: path.join(path.dirname(fileURLToPath(import.meta.url)), '..', 'scripts', 'setup-code-server.mjs').replace(/\\/g, '/'),
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/** 解析 code-server 启动方式:返回 { launched, command, script } | { command, args }。
|
|
207
|
+
* - `bin` 为可执行文件(裸名/绝对路径 .exe/.cmd):直接 spawn;
|
|
208
|
+
* - `bin` 为 JS 入口(依赖安装的 out/node/entry.js):自动用 node 运行;
|
|
209
|
+
* - 未配置 `bin` 时:先探测插件依赖安装的 code-server,再回退 PATH 裸名 code-server。 */
|
|
210
|
+
function resolveLaunch(config) {
|
|
211
|
+
const probe = bundledRuntimeEntry();
|
|
212
|
+
let preferred = config.bin || DEFAULT_CONFIG.bin;
|
|
213
|
+
// 依赖安装的 code-server 优先,除非用户显式配置了非默认 bin
|
|
214
|
+
if (preferred === DEFAULT_CONFIG.bin && probe !== null) preferred = probe;
|
|
215
|
+
console.log(`[code-server] resolveLaunch: configuredBin=${config.bin ?? '(none)'} probe=${probe ?? '(none)'} -> ${preferred}`);
|
|
216
|
+
if (/\.(js|mjs|cjs)$/i.test(preferred)) {
|
|
217
|
+
// JS 入口:node <entry> ...(code-server 的 out/node/entry.js 是官方发布形态)
|
|
218
|
+
if (!fs.existsSync(preferred)) {
|
|
219
|
+
throw new Error(`code-server 入口不存在: ${preferred}`);
|
|
220
|
+
}
|
|
221
|
+
return { kind: 'node', script: preferred };
|
|
222
|
+
}
|
|
223
|
+
if (path.isAbsolute(preferred)) return { kind: 'bin', command: preferred };
|
|
224
|
+
try {
|
|
225
|
+
if (win32()) {
|
|
226
|
+
const out = execFileSync('where.exe', [preferred], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] });
|
|
227
|
+
const lines = out.split(/\r?\n/).map((s) => s.trim()).filter(Boolean);
|
|
228
|
+
return { kind: 'bin', command: lines.find((l) => /\.cmd$/i.test(l)) ?? lines.find((l) => /\.exe$/i.test(l)) ?? lines[0] };
|
|
229
|
+
}
|
|
230
|
+
const out = execFileSync('which', [preferred], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] });
|
|
231
|
+
return { kind: 'bin', command: out.split('\n')[0].trim() };
|
|
232
|
+
} catch {
|
|
233
|
+
throw new Error(
|
|
234
|
+
`code-server 未找到("${preferred}" 不在 PATH)。请安装最新版并确保可执行: ` +
|
|
235
|
+
`npm install -g code-server@latest(Windows 原生需配套最新版 node-gyp 与 VS Spectre 缓解库),` +
|
|
236
|
+
`或在 cordis.patch.yml 的 code-server config 中把 bin 指向已安装的 code-server 可执行文件 / out/node/entry.js。`,
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function healthCheck(host, port, timeoutMs = 1500) {
|
|
242
|
+
return new Promise((resolve) => {
|
|
243
|
+
const req = http.get({ host, port, path: '/healthz', timeout: timeoutMs, method: 'GET' }, (res) => {
|
|
244
|
+
res.resume();
|
|
245
|
+
resolve({ ok: res.statusCode >= 200 && res.statusCode < 500, statusCode: res.statusCode });
|
|
246
|
+
});
|
|
247
|
+
req.on('error', () => resolve({ ok: false }));
|
|
248
|
+
req.on('timeout', () => {
|
|
249
|
+
req.destroy();
|
|
250
|
+
resolve({ ok: false });
|
|
251
|
+
});
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function readPidFile(config) {
|
|
256
|
+
try {
|
|
257
|
+
const raw = JSON.parse(fs.readFileSync(pidFile(config), 'utf8'));
|
|
258
|
+
return raw && typeof raw.pid === 'number' ? raw : null;
|
|
259
|
+
} catch {
|
|
260
|
+
return null;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function writePidFile(config, record) {
|
|
265
|
+
fs.mkdirSync(path.dirname(pidFile(config)), { recursive: true });
|
|
266
|
+
fs.writeFileSync(pidFile(config), JSON.stringify(record, null, 2), 'utf8');
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function removePidFile(config) {
|
|
270
|
+
try {
|
|
271
|
+
fs.rmSync(pidFile(config), { force: true });
|
|
272
|
+
} catch {
|
|
273
|
+
// ignore
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
export async function apply(ctx, config) {
|
|
278
|
+
const cfg = { ...DEFAULT_CONFIG, ...(config ?? {}) };
|
|
279
|
+
|
|
280
|
+
// ---- 设置:行配置为种子;settings 命名空间持久化(设置卡片写入) ----
|
|
281
|
+
const settingsSvc = ctx.get('settings');
|
|
282
|
+
let reserveComposer = true;
|
|
283
|
+
let windowedOpen = false;
|
|
284
|
+
if (settingsSvc !== undefined && typeof settingsSvc.register === 'function') {
|
|
285
|
+
try {
|
|
286
|
+
const scope = settingsSvc.register(SETTINGS_NS, Config);
|
|
287
|
+
const rawDoc = settingsSvc.get(SETTINGS_NS);
|
|
288
|
+
if (rawDoc !== undefined && rawDoc !== null) {
|
|
289
|
+
const resolved = scope.get();
|
|
290
|
+
reserveComposer = resolved && typeof resolved.reserveComposer === 'boolean' ? resolved.reserveComposer : true;
|
|
291
|
+
windowedOpen = resolved && typeof resolved.windowedOpen === 'boolean' ? resolved.windowedOpen : false;
|
|
292
|
+
}
|
|
293
|
+
scope.watch((next) => {
|
|
294
|
+
if (next != null && typeof next.reserveComposer === 'boolean') {
|
|
295
|
+
reserveComposer = next.reserveComposer;
|
|
296
|
+
console.log(`[code-server] reserveComposer updated: ${reserveComposer}`);
|
|
297
|
+
}
|
|
298
|
+
if (next != null && typeof next.windowedOpen === 'boolean') {
|
|
299
|
+
windowedOpen = next.windowedOpen;
|
|
300
|
+
console.log(`[code-server] windowedOpen updated: ${windowedOpen}`);
|
|
301
|
+
}
|
|
302
|
+
});
|
|
303
|
+
} catch (error) {
|
|
304
|
+
console.error(`[code-server] settings unavailable; using defaults (reserveComposer=true, windowedOpen=false): ${error.message}`);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
const state = {
|
|
309
|
+
status: 'stopped', // stopped | starting | running | stopping | error
|
|
310
|
+
pid: null,
|
|
311
|
+
port: cfg.port,
|
|
312
|
+
cwd: null,
|
|
313
|
+
version: null,
|
|
314
|
+
error: null,
|
|
315
|
+
logTail: '',
|
|
316
|
+
startedAt: null,
|
|
317
|
+
adopted: false,
|
|
318
|
+
env: envCheck(), // 环境检测(code-server 入口/native/内部依赖)
|
|
319
|
+
setup: { running: false, done: false, ok: false, logTail: '', startedAt: null, finishedAt: null }, // 环境安装任务
|
|
320
|
+
};
|
|
321
|
+
|
|
322
|
+
let child = null;
|
|
323
|
+
let pollTimer = null;
|
|
324
|
+
let disposeKilled = false;
|
|
325
|
+
|
|
326
|
+
function snapshot() {
|
|
327
|
+
const running = state.status === 'running' && state.pid !== null;
|
|
328
|
+
return {
|
|
329
|
+
ok: state.status !== 'error' || running,
|
|
330
|
+
running,
|
|
331
|
+
status: state.status,
|
|
332
|
+
port: state.port,
|
|
333
|
+
host: cfg.host,
|
|
334
|
+
pid: state.pid,
|
|
335
|
+
cwd: state.cwd,
|
|
336
|
+
url: running ? `http://${cfg.host}:${state.port}/` : null,
|
|
337
|
+
version: state.version,
|
|
338
|
+
error: state.error,
|
|
339
|
+
logTail: state.logTail.slice(-LOG_TAIL_MAX),
|
|
340
|
+
adopted: state.adopted,
|
|
341
|
+
reserveComposer,
|
|
342
|
+
windowedOpen,
|
|
343
|
+
env: state.env,
|
|
344
|
+
setup: {
|
|
345
|
+
running: state.setup.running,
|
|
346
|
+
done: state.setup.done,
|
|
347
|
+
ok: state.setup.ok,
|
|
348
|
+
logTail: state.setup.logTail.slice(-LOG_TAIL_MAX),
|
|
349
|
+
},
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function appendLog(chunk) {
|
|
354
|
+
try {
|
|
355
|
+
const text = typeof chunk === 'string' ? chunk : chunk.toString('utf8');
|
|
356
|
+
state.logTail = (state.logTail + text).slice(-LOG_TAIL_MAX * 2);
|
|
357
|
+
} catch {
|
|
358
|
+
// ignore
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function stopPolling() {
|
|
363
|
+
if (pollTimer !== null) {
|
|
364
|
+
clearInterval(pollTimer);
|
|
365
|
+
pollTimer = null;
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
function killTree(pid) {
|
|
370
|
+
return new Promise((resolve) => {
|
|
371
|
+
if (!isAlive(pid)) return resolve(false);
|
|
372
|
+
if (win32()) {
|
|
373
|
+
execFile('taskkill.exe', ['/PID', String(pid), '/T', '/F'], (err) => resolve(!err));
|
|
374
|
+
} else {
|
|
375
|
+
try {
|
|
376
|
+
process.kill(-pid, 'SIGTERM');
|
|
377
|
+
} catch {
|
|
378
|
+
try {
|
|
379
|
+
process.kill(pid, 'SIGTERM');
|
|
380
|
+
} catch {
|
|
381
|
+
return resolve(false);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
const timer = setTimeout(() => {
|
|
385
|
+
try {
|
|
386
|
+
process.kill(-pid, 'SIGKILL');
|
|
387
|
+
} catch {
|
|
388
|
+
try {
|
|
389
|
+
process.kill(pid, 'SIGKILL');
|
|
390
|
+
} catch {
|
|
391
|
+
// gone
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
resolve(true);
|
|
395
|
+
}, 3000);
|
|
396
|
+
timer.unref?.();
|
|
397
|
+
}
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
async function stop(reason) {
|
|
402
|
+
stopPolling();
|
|
403
|
+
const wasRunning = state.status === 'running' || state.status === 'starting';
|
|
404
|
+
const pid = state.pid;
|
|
405
|
+
if (child !== null) {
|
|
406
|
+
child.stdout?.removeAllListeners?.('data');
|
|
407
|
+
child.stderr?.removeAllListeners?.('data');
|
|
408
|
+
try {
|
|
409
|
+
child.removeAllListeners?.('exit');
|
|
410
|
+
child.removeAllListeners?.('error');
|
|
411
|
+
} catch {
|
|
412
|
+
// ignore
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
child = null;
|
|
416
|
+
if (pid) {
|
|
417
|
+
await killTree(pid);
|
|
418
|
+
}
|
|
419
|
+
removePidFile(cfg);
|
|
420
|
+
state.status = 'stopped';
|
|
421
|
+
state.pid = null;
|
|
422
|
+
state.cwd = null;
|
|
423
|
+
state.startedAt = null;
|
|
424
|
+
state.adopted = false;
|
|
425
|
+
if (reason) state.error = null;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
function beginPollingReady() {
|
|
429
|
+
stopPolling();
|
|
430
|
+
const deadline = Date.now() + (Number(cfg.readyTimeoutMs) || DEFAULT_CONFIG.readyTimeoutMs);
|
|
431
|
+
// 首探延迟 800ms(spawn 后进程初始化),之后每 500ms 一次——缩短"已就绪但 UI 在转"的窗口
|
|
432
|
+
let first = true;
|
|
433
|
+
pollTimer = setInterval(async () => {
|
|
434
|
+
if (first) {
|
|
435
|
+
first = false;
|
|
436
|
+
return; // 等 800ms 才首次探测(给 Node 启动留时间)
|
|
437
|
+
}
|
|
438
|
+
const probe = await healthCheck(cfg.host, state.port);
|
|
439
|
+
if (probe.ok) {
|
|
440
|
+
stopPolling();
|
|
441
|
+
state.status = 'running';
|
|
442
|
+
return;
|
|
443
|
+
}
|
|
444
|
+
if (Date.now() > deadline) {
|
|
445
|
+
stopPolling();
|
|
446
|
+
state.status = 'error';
|
|
447
|
+
state.error = `启动超时(${cfg.readyTimeoutMs}ms 内 /healthz 未就绪);code-server 启动日志尾部:\n${state.logTail.slice(-2000)}`;
|
|
448
|
+
}
|
|
449
|
+
}, 500);
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
async function start(cwdArg) {
|
|
453
|
+
const cwd = typeof cwdArg === 'string' && cwdArg.trim() !== '' ? cwdArg : undefined;
|
|
454
|
+
if (state.status === 'running' && state.pid !== null) {
|
|
455
|
+
if (cwd === undefined || cwd === state.cwd) return snapshot();
|
|
456
|
+
await stop('restart');
|
|
457
|
+
}
|
|
458
|
+
// 上一次启动仍在进行:等待它结算后再按本次 cwd 启动,避免 cwd 切换被吞
|
|
459
|
+
if (state.status === 'starting') {
|
|
460
|
+
const deadline = Date.now() + 10000;
|
|
461
|
+
while (state.status === 'starting' && Date.now() < deadline) {
|
|
462
|
+
await new Promise((r) => setTimeout(r, 250));
|
|
463
|
+
}
|
|
464
|
+
if (state.status === 'starting') {
|
|
465
|
+
state.status = 'error';
|
|
466
|
+
state.error = '启动长时间未结算(10s),请查看启动日志;后可重试';
|
|
467
|
+
stopPolling();
|
|
468
|
+
return snapshot();
|
|
469
|
+
}
|
|
470
|
+
if (state.status === 'running') {
|
|
471
|
+
if (cwd === undefined || cwd === state.cwd) return snapshot();
|
|
472
|
+
await stop('restart');
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
const launch = resolveLaunch(cfg); // throws with install guidance when missing
|
|
477
|
+
|
|
478
|
+
// 认证:非回环 host 必须 password;显式 password 必须带 token
|
|
479
|
+
const auth = cfg.auth || 'none';
|
|
480
|
+
if (auth === 'none' && !LOOPBACK_HOSTS.has(cfg.host)) {
|
|
481
|
+
state.status = 'error';
|
|
482
|
+
state.error = `auth=none 仅允许回环绑定(当前 host="${cfg.host}");请改用 auth=password 并配置 passwordToken`;
|
|
483
|
+
return snapshot();
|
|
484
|
+
}
|
|
485
|
+
if (auth === 'password' && !cfg.passwordToken) {
|
|
486
|
+
state.status = 'error';
|
|
487
|
+
state.error = 'auth=password 需要配置 passwordToken(cordis.patch.yml 的 config.passwordToken)';
|
|
488
|
+
return snapshot();
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
// 端口占用则尝试 adopt(pid.json 有效 + /healthz 响应),否则报错
|
|
492
|
+
const probe = await healthCheck(cfg.host, cfg.port, 800);
|
|
493
|
+
if (probe.ok) {
|
|
494
|
+
const record = readPidFile(cfg);
|
|
495
|
+
if (record && isAlive(record.pid)) {
|
|
496
|
+
state.status = 'running';
|
|
497
|
+
state.pid = record.pid;
|
|
498
|
+
state.cwd = cwd ?? record.cwd ?? null;
|
|
499
|
+
state.startedAt = record.startedAt ?? null;
|
|
500
|
+
state.adopted = true;
|
|
501
|
+
return snapshot();
|
|
502
|
+
}
|
|
503
|
+
state.status = 'error';
|
|
504
|
+
state.error = `端口 ${cfg.port} 已被占用且没有有效的 pid.json 记录(拒绝误杀);请释放端口或修改 port 配置`;
|
|
505
|
+
return snapshot();
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
// 重建数据目录
|
|
509
|
+
const root = dataRoot(cfg);
|
|
510
|
+
const userDataDir = cfg.userDataDir || path.join(root, 'user-data');
|
|
511
|
+
const extensionsDir = cfg.extensionsDir || path.join(root, 'extensions');
|
|
512
|
+
fs.mkdirSync(userDataDir, { recursive: true });
|
|
513
|
+
fs.mkdirSync(extensionsDir, { recursive: true });
|
|
514
|
+
|
|
515
|
+
// 安装内置扩展(dshcs-open-file:host 信号文件 → VS Code 打开文件)
|
|
516
|
+
installBundledExtension(extensionsDir, userDataDir);
|
|
517
|
+
|
|
518
|
+
const args = [
|
|
519
|
+
'--bind-addr', `${cfg.host}:${cfg.port}`,
|
|
520
|
+
'--auth', auth === 'password' ? 'password' : 'none',
|
|
521
|
+
'--user-data-dir', userDataDir,
|
|
522
|
+
'--extensions-dir', extensionsDir,
|
|
523
|
+
'--disable-telemetry',
|
|
524
|
+
'--disable-update-check',
|
|
525
|
+
];
|
|
526
|
+
if (cwd !== undefined) args.push(cwd);
|
|
527
|
+
|
|
528
|
+
state.error = null;
|
|
529
|
+
state.logTail = '';
|
|
530
|
+
state.status = 'starting';
|
|
531
|
+
state.cwd = cwd ?? null;
|
|
532
|
+
state.startedAt = Date.now();
|
|
533
|
+
state.adopted = false;
|
|
534
|
+
|
|
535
|
+
const env = { ...process.env };
|
|
536
|
+
if (auth === 'password') env.PASSWORD = cfg.passwordToken;
|
|
537
|
+
// 内置扩展信号文件路径(host → 扩展 打开文件)
|
|
538
|
+
env.DSHCS_OPEN_FILE_SIGNAL = openFileSignalPath(userDataDir);
|
|
539
|
+
|
|
540
|
+
let proc;
|
|
541
|
+
try {
|
|
542
|
+
const isCmd = launch.kind === 'bin' && win32() && /\.cmd$/i.test(launch.command);
|
|
543
|
+
const command = launch.kind === 'node' ? process.execPath : launch.command;
|
|
544
|
+
const spawnArgs = launch.kind === 'node' ? [launch.script, ...args] : args;
|
|
545
|
+
// shell 仅对 .cmd shim(Windows npm 全局包)必要:它必须经 cmd.exe 解析。
|
|
546
|
+
// 含空格路径由 spawn 数组传参,不再经 shell 拼接,避免 'C:\Program' 拆分。
|
|
547
|
+
proc = spawn(isCmd ? `"${command}"` : command, spawnArgs, {
|
|
548
|
+
cwd: cwd ?? process.cwd(),
|
|
549
|
+
env,
|
|
550
|
+
shell: isCmd,
|
|
551
|
+
windowsHide: true,
|
|
552
|
+
detached: !win32() && !isCmd,
|
|
553
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
554
|
+
});
|
|
555
|
+
} catch (err) {
|
|
556
|
+
state.status = 'error';
|
|
557
|
+
state.error = `spawn 失败: ${err && err.message ? err.message : String(err)}`;
|
|
558
|
+
return snapshot();
|
|
559
|
+
}
|
|
560
|
+
child = proc;
|
|
561
|
+
state.pid = proc.pid ?? null;
|
|
562
|
+
writePidFile(cfg, {
|
|
563
|
+
pid: proc.pid,
|
|
564
|
+
startedAt: state.startedAt,
|
|
565
|
+
cwd: cwd ?? null,
|
|
566
|
+
host: cfg.host,
|
|
567
|
+
port: cfg.port,
|
|
568
|
+
launchKind: launch.kind,
|
|
569
|
+
launchCommand: launch.kind === 'node' ? launch.script : launch.command,
|
|
570
|
+
});
|
|
571
|
+
|
|
572
|
+
proc.stdout?.on?.('data', appendLog);
|
|
573
|
+
proc.stderr?.on?.('data', appendLog);
|
|
574
|
+
|
|
575
|
+
proc.on('error', (err) => {
|
|
576
|
+
if (child !== proc) return;
|
|
577
|
+
child = null;
|
|
578
|
+
removePidFile(cfg);
|
|
579
|
+
state.status = 'error';
|
|
580
|
+
state.error = `code-server 启动失败: ${err && err.message ? err.message : String(err)}\n${state.logTail.slice(-1000)}`;
|
|
581
|
+
});
|
|
582
|
+
|
|
583
|
+
proc.on('exit', (code, signal) => {
|
|
584
|
+
if (child !== proc) return; // 已被 stop/dispose 接管
|
|
585
|
+
child = null;
|
|
586
|
+
removePidFile(cfg);
|
|
587
|
+
if (disposeKilled) return;
|
|
588
|
+
state.status = 'error';
|
|
589
|
+
state.error = `code-server 意外退出${code !== null ? `(exit ${code})` : signal ? `(signal ${signal})` : ''}:\n${state.logTail.slice(-1500)}`;
|
|
590
|
+
});
|
|
591
|
+
|
|
592
|
+
beginPollingReady();
|
|
593
|
+
return snapshot();
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
/** 后台启动环境安装(setup 脚本):npm 自装 code-server + native + vscode 内部依赖。
|
|
597
|
+
* 异步运行,进度经 status.setup 轮询返回;完成后刷新 envCheck。 */
|
|
598
|
+
function startSetup() {
|
|
599
|
+
if (state.setup.running) return;
|
|
600
|
+
const script = envCheck().pathToSetup;
|
|
601
|
+
state.setup = { running: true, done: false, ok: false, logTail: '', startedAt: Date.now(), finishedAt: null };
|
|
602
|
+
let proc;
|
|
603
|
+
try {
|
|
604
|
+
proc = spawn(process.execPath, [script], {
|
|
605
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
606
|
+
windowsHide: true,
|
|
607
|
+
});
|
|
608
|
+
} catch (err) {
|
|
609
|
+
state.setup = { running: false, done: true, ok: false, logTail: `spawn 失败: ${err.message}`, startedAt: Date.now(), finishedAt: Date.now() };
|
|
610
|
+
return;
|
|
611
|
+
}
|
|
612
|
+
const onChunk = (chunk) => {
|
|
613
|
+
try {
|
|
614
|
+
const text = typeof chunk === 'string' ? chunk : chunk.toString('utf8');
|
|
615
|
+
state.setup.logTail = (state.setup.logTail + text).slice(-LOG_TAIL_MAX * 2);
|
|
616
|
+
} catch { /* ignore */ }
|
|
617
|
+
};
|
|
618
|
+
proc.stdout?.on?.('data', onChunk);
|
|
619
|
+
proc.stderr?.on?.('data', onChunk);
|
|
620
|
+
proc.on('error', (err) => {
|
|
621
|
+
state.setup = { ...state.setup, running: false, done: true, ok: false, logTail: state.setup.logTail + `\nspawn 错误: ${err.message}`, finishedAt: Date.now() };
|
|
622
|
+
state.env = envCheck();
|
|
623
|
+
});
|
|
624
|
+
proc.on('exit', (code) => {
|
|
625
|
+
state.setup = { ...state.setup, running: false, done: true, ok: code === 0, finishedAt: Date.now() };
|
|
626
|
+
state.env = envCheck();
|
|
627
|
+
console.log(`[code-server] setup finished code=${code} env.ok=${state.env.ok}`);
|
|
628
|
+
});
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
async function handleApi(req, res) {
|
|
632
|
+
const method = (req.method || 'GET').toUpperCase();
|
|
633
|
+
const payload = { ok: false, error: 'unknown route' };
|
|
634
|
+
try {
|
|
635
|
+
if (req.url === '/code-server/status' && method === 'GET') {
|
|
636
|
+
Object.assign(payload, snapshot());
|
|
637
|
+
} else if (req.url === '/code-server/start' && method === 'POST') {
|
|
638
|
+
let body = {};
|
|
639
|
+
for await (const chunk of req) {
|
|
640
|
+
const text = chunk.toString('utf8');
|
|
641
|
+
if (text) {
|
|
642
|
+
try {
|
|
643
|
+
body = { ...body, ...JSON.parse(text) };
|
|
644
|
+
} catch {
|
|
645
|
+
// ignore malformed fragments; keep best-effort
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
const cwd = body && typeof body.cwd === 'string' ? body.cwd : undefined;
|
|
650
|
+
Object.assign(payload, await start(cwd));
|
|
651
|
+
} else if (req.url === '/code-server/stop' && method === 'POST') {
|
|
652
|
+
await stop('user');
|
|
653
|
+
Object.assign(payload, snapshot());
|
|
654
|
+
} else if (req.url === '/code-server/setup' && method === 'POST') {
|
|
655
|
+
// 环境安装:后台执行 setup 脚本(npm 自装 code-server + native + vscode 内部依赖)
|
|
656
|
+
if (state.setup.running) {
|
|
657
|
+
// 已在安装中:同样视为"已发起"(客户端会轮询 setup 状态直至结束)
|
|
658
|
+
payload.ok = true;
|
|
659
|
+
payload.message = '环境安装已在进行中,请等待完成';
|
|
660
|
+
payload.error = null;
|
|
661
|
+
} else {
|
|
662
|
+
startSetup();
|
|
663
|
+
Object.assign(payload, snapshot());
|
|
664
|
+
payload.ok = true; // 安装任务已启动;服务态 error(如未安装)不掩盖安装态
|
|
665
|
+
}
|
|
666
|
+
} else if (req.url === '/code-server/open-file' && method === 'POST') {
|
|
667
|
+
// 打开文件:host 写信号文件,内置扩展(dshcs-open-file)在 VS Code 中打开它
|
|
668
|
+
let body = {};
|
|
669
|
+
for await (const chunk of req) {
|
|
670
|
+
const text = chunk.toString('utf8');
|
|
671
|
+
if (text) {
|
|
672
|
+
try {
|
|
673
|
+
body = { ...body, ...JSON.parse(text) };
|
|
674
|
+
} catch {
|
|
675
|
+
// ignore malformed fragments; keep best-effort
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
const file = body && typeof body.file === 'string' ? body.file : null;
|
|
680
|
+
if (file === null || file === '') {
|
|
681
|
+
payload.error = '需要 file 字段(要打开的绝对路径)';
|
|
682
|
+
payload.status = 400;
|
|
683
|
+
res.writeHead(400, { 'content-type': 'application/json; charset=utf-8' });
|
|
684
|
+
res.end(JSON.stringify(payload));
|
|
685
|
+
return;
|
|
686
|
+
}
|
|
687
|
+
const root = dataRoot(cfg);
|
|
688
|
+
const userDataDir = cfg.userDataDir || path.join(root, 'user-data');
|
|
689
|
+
const signal = openFileSignalPath(userDataDir);
|
|
690
|
+
try {
|
|
691
|
+
fs.mkdirSync(path.dirname(signal), { recursive: true });
|
|
692
|
+
fs.writeFileSync(signal, JSON.stringify({ file, ts: Date.now() }), 'utf8');
|
|
693
|
+
// 同时确保 code-server 运行(信号由扩展消费)
|
|
694
|
+
payload.ok = true;
|
|
695
|
+
payload.signal = signal;
|
|
696
|
+
} catch (err) {
|
|
697
|
+
payload.error = err && err.message ? err.message : String(err);
|
|
698
|
+
}
|
|
699
|
+
} else {
|
|
700
|
+
payload.error = 'unknown route';
|
|
701
|
+
payload.status = 404;
|
|
702
|
+
res.writeHead(404, { 'content-type': 'application/json; charset=utf-8' });
|
|
703
|
+
res.end(JSON.stringify(payload));
|
|
704
|
+
return;
|
|
705
|
+
}
|
|
706
|
+
res.writeHead(200, { 'content-type': 'application/json; charset=utf-8' });
|
|
707
|
+
res.end(JSON.stringify(payload));
|
|
708
|
+
} catch (err) {
|
|
709
|
+
payload.ok = false;
|
|
710
|
+
payload.error = err && err.message ? err.message : String(err);
|
|
711
|
+
payload.runner = 'code-server';
|
|
712
|
+
res.writeHead(200, { 'content-type': 'application/json; charset=utf-8' });
|
|
713
|
+
res.end(JSON.stringify(payload));
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
/** 固化图标服务:返回插件携带的 code-server 官方图标(不依赖运行时安装)。
|
|
718
|
+
* - /code-server/icon.ico → assets/favicon.ico(标签页/桌面图标)
|
|
719
|
+
* - /code-server/icon.svg → assets/favicon.svg(PWA/深色模式) */
|
|
720
|
+
function handleIcon(req, res) {
|
|
721
|
+
const isSvg = req.url === '/code-server/icon.svg';
|
|
722
|
+
const file = isSvg ? 'favicon.svg' : 'favicon.ico';
|
|
723
|
+
const p = path.join(path.dirname(fileURLToPath(import.meta.url)), '..', 'assets', file);
|
|
724
|
+
try {
|
|
725
|
+
if (!fs.existsSync(p)) {
|
|
726
|
+
res.writeHead(404, { 'content-type': 'text/plain; charset=utf-8' });
|
|
727
|
+
res.end('icon not found');
|
|
728
|
+
return;
|
|
729
|
+
}
|
|
730
|
+
const data = fs.readFileSync(p);
|
|
731
|
+
res.writeHead(200, {
|
|
732
|
+
'content-type': isSvg ? 'image/svg+xml' : 'image/x-icon',
|
|
733
|
+
'cache-control': 'public, max-age=86400',
|
|
734
|
+
});
|
|
735
|
+
res.end(data);
|
|
736
|
+
} catch (err) {
|
|
737
|
+
res.writeHead(500, { 'content-type': 'application/json; charset=utf-8' });
|
|
738
|
+
res.end(JSON.stringify({ ok: false, error: err && err.message ? err.message : String(err) }));
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
const webServer = ctx.get('webServer');
|
|
743
|
+
if (webServer === undefined) {
|
|
744
|
+
console.error('[code-server] webServer service unavailable; plugin registered but idle');
|
|
745
|
+
return;
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
const disposers = [
|
|
749
|
+
webServer.register({ kind: 'exact', path: '/code-server/status', handler: handleApi }),
|
|
750
|
+
webServer.register({ kind: 'exact', path: '/code-server/start', handler: handleApi }),
|
|
751
|
+
webServer.register({ kind: 'exact', path: '/code-server/stop', handler: handleApi }),
|
|
752
|
+
webServer.register({ kind: 'exact', path: '/code-server/setup', handler: handleApi }),
|
|
753
|
+
webServer.register({ kind: 'exact', path: '/code-server/open-file', handler: handleApi }),
|
|
754
|
+
webServer.register({ kind: 'exact', path: '/code-server/icon.ico', handler: handleIcon }),
|
|
755
|
+
webServer.register({ kind: 'exact', path: '/code-server/icon.svg', handler: handleIcon }),
|
|
756
|
+
];
|
|
757
|
+
|
|
758
|
+
ctx.effect(() => {
|
|
759
|
+
return () => {
|
|
760
|
+
disposeKilled = true;
|
|
761
|
+
stopPolling();
|
|
762
|
+
for (const d of disposers) {
|
|
763
|
+
try {
|
|
764
|
+
d();
|
|
765
|
+
} catch {
|
|
766
|
+
// ignore
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
if (child !== null || (state.pid !== null && isAlive(state.pid))) {
|
|
770
|
+
const pid = state.pid;
|
|
771
|
+
child = null;
|
|
772
|
+
if (pid) {
|
|
773
|
+
killTree(pid).then(() => removePidFile(cfg));
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
};
|
|
777
|
+
}, 'code-server: lifecycle');
|
|
778
|
+
|
|
779
|
+
// DSH host 重启后 adopt:pid.json 有效且进程存活且 /healthz 响应 → 接管
|
|
780
|
+
const record = readPidFile(cfg);
|
|
781
|
+
if (record && isAlive(record.pid)) {
|
|
782
|
+
const probe = await healthCheck(cfg.host, cfg.port, 800);
|
|
783
|
+
if (probe.ok) {
|
|
784
|
+
state.status = 'running';
|
|
785
|
+
state.pid = record.pid;
|
|
786
|
+
state.cwd = record.cwd ?? null;
|
|
787
|
+
state.startedAt = record.startedAt ?? null;
|
|
788
|
+
state.adopted = true;
|
|
789
|
+
console.log(`[code-server] adopted running instance pid=${record.pid} port=${cfg.port}`);
|
|
790
|
+
} else {
|
|
791
|
+
removePidFile(cfg);
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
// ---- 预启动:环境就绪且未运行 → 后台自动拉起(不等待) ----
|
|
796
|
+
// 悬浮球打开时若已 running,iframe 立即加载(消除冷启动等待)。
|
|
797
|
+
if (state.env.ok && state.status !== 'running' && state.status !== 'starting') {
|
|
798
|
+
start().catch((err) => {
|
|
799
|
+
console.error('[code-server] prestart failed:', err && err.message ? err.message : String(err));
|
|
800
|
+
});
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
console.log(`[code-server] static plugin loaded (host=${cfg.host} port=${cfg.port} auth=${cfg.auth})`);
|
|
804
|
+
}
|