dsh-api-runner 0.1.0 → 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.md +58 -14
- package/client/client.js +1824 -1604
- package/package.json +71 -77
- package/src/java-runner.js +3 -3
- package/src/main.js +31 -2
- package/src/ops.js +218 -0
- package/src/restart.js +43 -12
- package/test/client.mjs +162 -109
- package/test/plugin.mjs +34 -0
- package/test/smoke.mjs +85 -0
package/package.json
CHANGED
|
@@ -1,77 +1,71 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "dsh-api-runner",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "DSH 插件:按配置调用后端 HTTP 接口、启停/重启本地开发服务、增量读取日志并等待日志特征,让模型能\"改完自己验\"。",
|
|
5
|
-
"type": "module",
|
|
6
|
-
"license": "MIT",
|
|
7
|
-
"author": "Moz (lhhzmz229)",
|
|
8
|
-
"repository": {
|
|
9
|
-
"type": "git",
|
|
10
|
-
"url": "git+https://github.com/lhhzmz229/dsh-api-runner.git"
|
|
11
|
-
},
|
|
12
|
-
"homepage": "https://github.com/lhhzmz229/dsh-api-runner#readme",
|
|
13
|
-
"bugs": {
|
|
14
|
-
"url": "https://github.com/lhhzmz229/dsh-api-runner/issues"
|
|
15
|
-
},
|
|
16
|
-
"keywords": [
|
|
17
|
-
"dsh",
|
|
18
|
-
"dsh-plugin",
|
|
19
|
-
"deepseek-harness",
|
|
20
|
-
"api-runner",
|
|
21
|
-
"http-client",
|
|
22
|
-
"log-tail",
|
|
23
|
-
"spring-boot",
|
|
24
|
-
"java",
|
|
25
|
-
"devtools"
|
|
26
|
-
],
|
|
27
|
-
"main": "src/main.js",
|
|
28
|
-
"exports": {
|
|
29
|
-
".": "./src/main.js",
|
|
30
|
-
"./runner": "./src/main.js",
|
|
31
|
-
"./client": "./client/client.js",
|
|
32
|
-
"./package.json": "./package.json"
|
|
33
|
-
},
|
|
34
|
-
"engines": {
|
|
35
|
-
"node": ">=20",
|
|
36
|
-
"dsh": ">=0.1.0-rc.6 <0.2.0-0 || >=0.2.0-rc.1 <0.3.0-0"
|
|
37
|
-
},
|
|
38
|
-
"scripts": {
|
|
39
|
-
"test": "node test/smoke.mjs && node test/plugin.mjs",
|
|
40
|
-
"test:all": "node test/smoke.mjs && node test/plugin.mjs && node test/client.mjs && node test/managed.mjs && node test/java.mjs",
|
|
41
|
-
"stage": "node scripts/stage.mjs",
|
|
42
|
-
"release": "node scripts/stage.mjs --release"
|
|
43
|
-
},
|
|
44
|
-
"dependencies": {
|
|
45
|
-
"yaml": "^2.8.0"
|
|
46
|
-
},
|
|
47
|
-
"
|
|
48
|
-
"
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
"
|
|
52
|
-
"
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
"
|
|
68
|
-
"
|
|
69
|
-
"
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
"test",
|
|
73
|
-
"README.md",
|
|
74
|
-
"LICENSE",
|
|
75
|
-
"dsh-api-runner.example.yml"
|
|
76
|
-
]
|
|
77
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "dsh-api-runner",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "DSH 插件:按配置调用后端 HTTP 接口、启停/重启本地开发服务、增量读取日志并等待日志特征,让模型能\"改完自己验\"。",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"author": "Moz (lhhzmz229)",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/lhhzmz229/dsh-api-runner.git"
|
|
11
|
+
},
|
|
12
|
+
"homepage": "https://github.com/lhhzmz229/dsh-api-runner#readme",
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/lhhzmz229/dsh-api-runner/issues"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"dsh",
|
|
18
|
+
"dsh-plugin",
|
|
19
|
+
"deepseek-harness",
|
|
20
|
+
"api-runner",
|
|
21
|
+
"http-client",
|
|
22
|
+
"log-tail",
|
|
23
|
+
"spring-boot",
|
|
24
|
+
"java",
|
|
25
|
+
"devtools"
|
|
26
|
+
],
|
|
27
|
+
"main": "src/main.js",
|
|
28
|
+
"exports": {
|
|
29
|
+
".": "./src/main.js",
|
|
30
|
+
"./runner": "./src/main.js",
|
|
31
|
+
"./client": "./client/client.js",
|
|
32
|
+
"./package.json": "./package.json"
|
|
33
|
+
},
|
|
34
|
+
"engines": {
|
|
35
|
+
"node": ">=20",
|
|
36
|
+
"dsh": ">=0.1.0-rc.6 <0.2.0-0 || >=0.2.0-rc.1 <0.3.0-0"
|
|
37
|
+
},
|
|
38
|
+
"scripts": {
|
|
39
|
+
"test": "node test/smoke.mjs && node test/plugin.mjs",
|
|
40
|
+
"test:all": "node test/smoke.mjs && node test/plugin.mjs && node test/client.mjs && node test/managed.mjs && node test/java.mjs",
|
|
41
|
+
"stage": "node scripts/stage.mjs",
|
|
42
|
+
"release": "node scripts/stage.mjs --release"
|
|
43
|
+
},
|
|
44
|
+
"dependencies": {
|
|
45
|
+
"yaml": "^2.8.0"
|
|
46
|
+
},
|
|
47
|
+
"dsh": {
|
|
48
|
+
"bundle": {
|
|
49
|
+
"patch": "./cordis.patch.yml"
|
|
50
|
+
},
|
|
51
|
+
"client": {
|
|
52
|
+
"platform": "web",
|
|
53
|
+
"inject": [
|
|
54
|
+
"@deepseek-ai/dsh-client-ui-layout",
|
|
55
|
+
"@deepseek-ai/dsh-client-ui-sidebar",
|
|
56
|
+
"@deepseek-ai/dsh-client-ui-sidebar-right"
|
|
57
|
+
]
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
"files": [
|
|
61
|
+
"cordis.patch.yml",
|
|
62
|
+
"src",
|
|
63
|
+
"client",
|
|
64
|
+
"docs",
|
|
65
|
+
"scripts",
|
|
66
|
+
"test",
|
|
67
|
+
"README.md",
|
|
68
|
+
"LICENSE",
|
|
69
|
+
"dsh-api-runner.example.yml"
|
|
70
|
+
]
|
|
71
|
+
}
|
package/src/java-runner.js
CHANGED
|
@@ -267,7 +267,7 @@ export class JavaRunner {
|
|
|
267
267
|
extraJvmArgs: [`-DdshApiRunner.runId=${id}`],
|
|
268
268
|
});
|
|
269
269
|
this._ensureLogHeader(logPath, `接管 pid ${sourcePid}(${id})`);
|
|
270
|
-
const spawned = spawnService({ javaExe: snapshot.javaExe, argv: built.argv, cwd: snapshot.cwd, logPath });
|
|
270
|
+
const spawned = await spawnService({ javaExe: snapshot.javaExe, argv: built.argv, cwd: snapshot.cwd, logPath });
|
|
271
271
|
if (!spawned.ok) return { ok: false, code: 'SPAWN_FAILED', message: spawned.message, compile };
|
|
272
272
|
|
|
273
273
|
const record = {
|
|
@@ -459,7 +459,7 @@ export class JavaRunner {
|
|
|
459
459
|
|
|
460
460
|
// 每次重启开一个新日志段,历史启动记录不被覆盖
|
|
461
461
|
const logPath = this.logPathOf(entry.id);
|
|
462
|
-
const spawned = spawnService({ javaExe: entry.javaExe, argv: entry.argv, cwd: entry.cwd, logPath });
|
|
462
|
+
const spawned = await spawnService({ javaExe: entry.javaExe, argv: entry.argv, cwd: entry.cwd, logPath });
|
|
463
463
|
if (!spawned.ok) return { ok: false, code: 'SPAWN_FAILED', message: spawned.message, compile };
|
|
464
464
|
|
|
465
465
|
const record = { ...entry, pid: spawned.pid, logPath, argv: entry.argv, startedAt: new Date().toISOString(), stoppedAt: '' };
|
|
@@ -530,7 +530,7 @@ export class JavaRunner {
|
|
|
530
530
|
});
|
|
531
531
|
const logPath = this.logPathOf(id);
|
|
532
532
|
this._ensureLogHeader(logPath, `按保存的配置启动 ${id}`);
|
|
533
|
-
const spawned = spawnService({ javaExe: saved.javaExe, argv: built.argv, cwd: saved.cwd, logPath });
|
|
533
|
+
const spawned = await spawnService({ javaExe: saved.javaExe, argv: built.argv, cwd: saved.cwd, logPath });
|
|
534
534
|
if (!spawned.ok) return { ok: false, code: 'SPAWN_FAILED', message: spawned.message, compile };
|
|
535
535
|
|
|
536
536
|
const record = {
|
package/src/main.js
CHANGED
|
@@ -61,8 +61,13 @@ const PARAMETERS = {
|
|
|
61
61
|
log_id: { type: 'string', description: '日志 id 或 name(log、wait_log、reset_log 用;省略时取第一个日志)' },
|
|
62
62
|
java_action: {
|
|
63
63
|
type: 'string',
|
|
64
|
-
enum: ['list', 'start', 'stop', 'restart', 'log', 'status', 'detail', 'ignore', 'unignore', 'save', 'unsave', 'start_saved', 'settings'],
|
|
65
|
-
description: 'Java 服务动作(action=java 用):list 列出;detail 服务详情+日志;start 接管启动(先编译再杀旧进程);stop/restart 只对插件启动的生效;log 读日志;ignore/unignore 在面板列表中隐藏/恢复;save 把当前进程的启动配方向保存下来(之后不用 IDEA 也能起);unsave 删除保存;start_saved
|
|
64
|
+
enum: ['list', 'start', 'stop', 'restart', 'log', 'status', 'detail', 'ignore', 'unignore', 'save', 'unsave', 'start_saved', 'start_all', 'settings'],
|
|
65
|
+
description: 'Java 服务动作(action=java 用):list 列出;detail 服务详情+日志;start 接管启动(先编译再杀旧进程);stop/restart 只对插件启动的生效;log 读日志;ignore/unignore 在面板列表中隐藏/恢复;save 把当前进程的启动配方向保存下来(之后不用 IDEA 也能起);unsave 删除保存;start_saved 按保存的配置启动一个;start_all 一键启动所有已保存的服务(已运行的跳过,单个失败不影响其余,返回每个的成败);settings 读写插件设置(数据目录/默认工作区)',
|
|
66
|
+
},
|
|
67
|
+
java_ids: {
|
|
68
|
+
type: 'array',
|
|
69
|
+
items: { type: 'string' },
|
|
70
|
+
description: 'java_action=start_all 时只启动这些服务(省略则启动全部已保存的)',
|
|
66
71
|
},
|
|
67
72
|
pid: { type: 'number', description: '要接管的 Java 进程 pid(java_action=start 用)' },
|
|
68
73
|
java_id: { type: 'string', description: '插件托管的服务名或 pid(java_action=stop/restart/log/save/unsave/start_saved 用)' },
|
|
@@ -187,6 +192,18 @@ async function runTool(service, args, projectPath) {
|
|
|
187
192
|
timeoutMs,
|
|
188
193
|
});
|
|
189
194
|
}
|
|
195
|
+
// 一键启动:把「已保存」里的服务全拉起来(或只拉 java_ids 列的那几个)
|
|
196
|
+
if (javaAction === 'start_all') {
|
|
197
|
+
const ids = Array.isArray(args.java_ids)
|
|
198
|
+
? args.java_ids.map((id) => String(id))
|
|
199
|
+
: (args.java_id ? [String(args.java_id)] : []);
|
|
200
|
+
return service.startAllSavedJava(projectPath, ids, {
|
|
201
|
+
compile: args.compile !== false,
|
|
202
|
+
restart: args.restart === true,
|
|
203
|
+
timeoutMs,
|
|
204
|
+
compileTimeoutMs: args.timeout_ms ? Number(args.timeout_ms) : undefined,
|
|
205
|
+
});
|
|
206
|
+
}
|
|
190
207
|
if (javaAction === 'settings') {
|
|
191
208
|
if (args.settings && typeof args.settings === 'object') return service.saveSettings(asObject(args.settings));
|
|
192
209
|
return service.settings();
|
|
@@ -547,6 +564,18 @@ async function handleHttp(req, res, service, projectOf) {
|
|
|
547
564
|
timeoutMs: body.timeout_ms ? Number(body.timeout_ms) : undefined,
|
|
548
565
|
}));
|
|
549
566
|
}
|
|
567
|
+
// 一键启动:后台跑,立刻返回批次 id;面板轮询 java/start-all/status 看进度
|
|
568
|
+
if (route === 'java/start-all' && req.method === 'POST') {
|
|
569
|
+
const body = await readBody();
|
|
570
|
+
return send(200, service.startSavedJavaBatch(pickCwd(cwd, url, body), Array.isArray(body.ids) ? body.ids : [], {
|
|
571
|
+
compile: body.compile !== false,
|
|
572
|
+
restart: body.restart === true,
|
|
573
|
+
timeoutMs: body.timeout_ms ? Number(body.timeout_ms) : undefined,
|
|
574
|
+
}));
|
|
575
|
+
}
|
|
576
|
+
if (route === 'java/start-all/status' && req.method === 'GET') {
|
|
577
|
+
return send(200, service.startBatchStatus(url.searchParams.get('batch') || ''));
|
|
578
|
+
}
|
|
550
579
|
if (route === 'settings' && req.method === 'GET') {
|
|
551
580
|
return send(200, service.settings());
|
|
552
581
|
}
|
package/src/ops.js
CHANGED
|
@@ -30,6 +30,15 @@ export class ApiRunnerService {
|
|
|
30
30
|
this.javaRunner = options.javaRunner || new JavaRunner({ homeDir: this.homeDir, settingsStore: this.settingsStore });
|
|
31
31
|
/** 记录最近一次错误,便于排查 */
|
|
32
32
|
this.lastError = '';
|
|
33
|
+
/**
|
|
34
|
+
* 一键启动的后台批次:`{ [batchId]: { id, cwd, startedAt, done, total, items, finished, result } }`。
|
|
35
|
+
*
|
|
36
|
+
* 为什么放后台跑:一批服务要逐个 mvn 编译,几个服务就是几分钟,
|
|
37
|
+
* 同步 HTTP 请求会一直挂着(浏览器和 fetch 都可能超时),面板也没法显示进度。
|
|
38
|
+
* 批次只存内存:DSH 重启后丢批次记录没关系,进程本身已经起来了。
|
|
39
|
+
*/
|
|
40
|
+
this.startBatches = new Map();
|
|
41
|
+
this._batchSeq = 0;
|
|
33
42
|
}
|
|
34
43
|
|
|
35
44
|
/** 面板配置转成 YAML 同构的覆盖层;没有面板存储时返回 undefined */
|
|
@@ -513,12 +522,25 @@ export class ApiRunnerService {
|
|
|
513
522
|
const result = await listJavaProcesses();
|
|
514
523
|
const runs = this.javaRunner ? this.javaRunner.runs(cwd) : [];
|
|
515
524
|
if (!result.ok) {
|
|
525
|
+
/**
|
|
526
|
+
* 没有 jps(没装 JDK)时也必须给全字段。
|
|
527
|
+
*
|
|
528
|
+
* 面板会直接读 saved / ignored / maven 并 .map 它们;早期这里只返回
|
|
529
|
+
* processes,前端拿 undefined 就崩(「已保存」页签整个白掉)。
|
|
530
|
+
* 而且用户没 JDK 时更需要看到「已保存的配方」和「怎么装 JDK」。
|
|
531
|
+
*/
|
|
532
|
+
const missingProject = this.projectStore ? this.projectStore.project(cwd) : { bindings: [], ignored: [], saved: [] };
|
|
516
533
|
return {
|
|
517
534
|
ok: false,
|
|
518
535
|
code: 'JPS_MISSING',
|
|
519
536
|
message: result.note,
|
|
520
537
|
processes: runs.map((run) => this._runView(run)),
|
|
521
538
|
runs: runs.map((run) => this._runView(run)),
|
|
539
|
+
saved: (missingProject.saved || []).map((item) => this._savedView(item)),
|
|
540
|
+
bindings: missingProject.bindings || [],
|
|
541
|
+
ignored: missingProject.ignored || [],
|
|
542
|
+
maven: await this._mavenHint(),
|
|
543
|
+
note: result.note,
|
|
522
544
|
};
|
|
523
545
|
}
|
|
524
546
|
const project = this.projectStore ? this.projectStore.project(cwd) : { bindings: [], ignored: [] };
|
|
@@ -702,6 +724,202 @@ export class ApiRunnerService {
|
|
|
702
724
|
return this.javaRunner.startSaved(cwd, saved, options);
|
|
703
725
|
}
|
|
704
726
|
|
|
727
|
+
/**
|
|
728
|
+
* 一键启动:把「已保存」里的服务批量拉起来。
|
|
729
|
+
*
|
|
730
|
+
* 为什么要串行而不是 Promise.all:
|
|
731
|
+
* 1. Maven 编译在同一个模块上是互斥的(都写 target/),并发跑会互相覆盖,
|
|
732
|
+
* 出现「编译成功但产物是半成品」这种最难查的错;
|
|
733
|
+
* 2. 一个服务起不来通常意味着环境有问题(端口、依赖、编译),
|
|
734
|
+
* 这时候继续把剩下的全拉起来,用户看到的是满屏报错而不是一个明确原因。
|
|
735
|
+
* 所以默认**逐个启动、遇错继续**:每个失败单独记账,最后一起汇报,
|
|
736
|
+
* 用户既能看到哪个成功,也能看到哪个为什么失败。
|
|
737
|
+
*
|
|
738
|
+
* 已经在跑的跳过(除非 options.restart 为 true),避免把正在调试的进程踢掉。
|
|
739
|
+
*
|
|
740
|
+
* @param {string} cwd - 项目目录
|
|
741
|
+
* @param {string[]} ids - 要启动的服务 id;空数组表示「已保存里的全部」
|
|
742
|
+
* @param {{compile?:boolean, restart?:boolean, timeoutMs?:number, compileTimeoutMs?:number, onProgress?:Function}} options
|
|
743
|
+
*/
|
|
744
|
+
async startAllSavedJava(cwd, ids = [], options = {}) {
|
|
745
|
+
if (!this.projectStore) return { ok: false, code: 'NO_STORE', message: '面板配置存储不可用' };
|
|
746
|
+
if (!this.javaRunner) return { ok: false, code: 'NO_RUNNER', message: 'Java 托管不可用' };
|
|
747
|
+
|
|
748
|
+
const all = this.projectStore.savedJava(cwd) || [];
|
|
749
|
+
const wanted = Array.isArray(ids) && ids.length > 0
|
|
750
|
+
? ids.map((id) => String(id))
|
|
751
|
+
: all.map((item) => String(item.id));
|
|
752
|
+
if (wanted.length === 0) {
|
|
753
|
+
return { ok: false, code: 'NOTHING_SAVED', message: '还没有保存的服务。先在「运行中」里对一个服务点「保存」,之后才能一键启动。' };
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
const aliveIds = new Set((this.javaRunner.runs(cwd) || []).filter((run) => run.alive).map((run) => String(run.id)));
|
|
757
|
+
const items = [];
|
|
758
|
+
let started = 0;
|
|
759
|
+
let skipped = 0;
|
|
760
|
+
let failed = 0;
|
|
761
|
+
|
|
762
|
+
for (const id of wanted) {
|
|
763
|
+
const saved = all.find((item) => String(item.id) === id);
|
|
764
|
+
if (!saved) {
|
|
765
|
+
items.push({ id, ok: false, code: 'NOT_SAVED', message: `没有保存的服务 ${id}` });
|
|
766
|
+
failed += 1;
|
|
767
|
+
if (typeof options.onProgress === 'function') {
|
|
768
|
+
try {
|
|
769
|
+
options.onProgress(items[items.length - 1], items.length, wanted.length);
|
|
770
|
+
} catch {
|
|
771
|
+
/* 进度回调出错不影响启动 */
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
continue;
|
|
775
|
+
}
|
|
776
|
+
if (aliveIds.has(id) && options.restart !== true) {
|
|
777
|
+
items.push({ id, ok: true, skipped: true, note: `${id} 已在运行,跳过` });
|
|
778
|
+
skipped += 1;
|
|
779
|
+
if (typeof options.onProgress === 'function') {
|
|
780
|
+
try {
|
|
781
|
+
options.onProgress(items[items.length - 1], items.length, wanted.length);
|
|
782
|
+
} catch {
|
|
783
|
+
/* 进度回调出错不影响启动 */
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
continue;
|
|
787
|
+
}
|
|
788
|
+
try {
|
|
789
|
+
const result = await this.javaRunner.startSaved(cwd, saved, {
|
|
790
|
+
compile: options.compile !== false,
|
|
791
|
+
timeoutMs: options.timeoutMs,
|
|
792
|
+
compileTimeoutMs: options.compileTimeoutMs,
|
|
793
|
+
});
|
|
794
|
+
items.push({
|
|
795
|
+
id,
|
|
796
|
+
ok: result.ok === true,
|
|
797
|
+
code: result.code,
|
|
798
|
+
pid: result.pid || 0,
|
|
799
|
+
port: result.port || 0,
|
|
800
|
+
logPath: result.logPath || '',
|
|
801
|
+
compile: result.compile,
|
|
802
|
+
message: result.message,
|
|
803
|
+
note: result.note,
|
|
804
|
+
});
|
|
805
|
+
if (result.ok) {
|
|
806
|
+
started += 1;
|
|
807
|
+
aliveIds.add(id);
|
|
808
|
+
} else {
|
|
809
|
+
failed += 1;
|
|
810
|
+
}
|
|
811
|
+
} catch (error) {
|
|
812
|
+
items.push({ id, ok: false, code: 'UNEXPECTED', message: String(error?.message || error) });
|
|
813
|
+
failed += 1;
|
|
814
|
+
}
|
|
815
|
+
if (typeof options.onProgress === 'function') {
|
|
816
|
+
try {
|
|
817
|
+
options.onProgress(items[items.length - 1], items.length, wanted.length);
|
|
818
|
+
} catch {
|
|
819
|
+
/* 进度回调出错不影响启动 */
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
const lines = items.map((item) => {
|
|
825
|
+
if (item.skipped) return `- ${item.id}:已在运行,跳过`;
|
|
826
|
+
if (item.ok) return `- ${item.id}:已启动${item.port ? `(端口 ${item.port})` : ''}`;
|
|
827
|
+
return `- ${item.id}:失败 —— ${item.message || item.code || '未知原因'}`;
|
|
828
|
+
});
|
|
829
|
+
return {
|
|
830
|
+
ok: failed === 0,
|
|
831
|
+
code: failed === 0 ? undefined : 'PARTIAL',
|
|
832
|
+
started,
|
|
833
|
+
skipped,
|
|
834
|
+
failed,
|
|
835
|
+
total: wanted.length,
|
|
836
|
+
items,
|
|
837
|
+
message: `一键启动完成:成功 ${started} 个,跳过 ${skipped} 个,失败 ${failed} 个。\n${lines.join('\n')}`,
|
|
838
|
+
};
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
/**
|
|
842
|
+
* 在后台跑一次一键启动,立刻返回批次 id。
|
|
843
|
+
*
|
|
844
|
+
* 面板拿到 id 后每 1 秒拉一次 `startBatchStatus`,直到 `finished`。
|
|
845
|
+
* 这样编译期间的几分钟里页面是可交互的,也能看到「3/8 正在编译」这类进度。
|
|
846
|
+
*/
|
|
847
|
+
startSavedJavaBatch(cwd, ids = [], options = {}) {
|
|
848
|
+
if (!this.projectStore) return { ok: false, code: 'NO_STORE', message: '面板配置存储不可用' };
|
|
849
|
+
if (!this.javaRunner) return { ok: false, code: 'NO_RUNNER', message: 'Java 托管不可用' };
|
|
850
|
+
const all = this.projectStore.savedJava(cwd) || [];
|
|
851
|
+
const wanted = Array.isArray(ids) && ids.length > 0 ? ids.map((id) => String(id)) : all.map((item) => String(item.id));
|
|
852
|
+
if (wanted.length === 0) {
|
|
853
|
+
return { ok: false, code: 'NOTHING_SAVED', message: '还没有保存的服务。先在「运行中」里对一个服务点「保存」,之后才能一键启动。' };
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
this._batchSeq += 1;
|
|
857
|
+
const batchId = `batch-${Date.now()}-${this._batchSeq}`;
|
|
858
|
+
const batch = {
|
|
859
|
+
id: batchId,
|
|
860
|
+
cwd,
|
|
861
|
+
startedAt: new Date().toISOString(),
|
|
862
|
+
done: 0,
|
|
863
|
+
total: wanted.length,
|
|
864
|
+
items: [],
|
|
865
|
+
finished: false,
|
|
866
|
+
result: null,
|
|
867
|
+
current: '',
|
|
868
|
+
};
|
|
869
|
+
this.startBatches.set(batchId, batch);
|
|
870
|
+
|
|
871
|
+
// 只保留最近 10 个批次,避免长时间运行的进程内存里堆一堆历史
|
|
872
|
+
if (this.startBatches.size > 10) {
|
|
873
|
+
const oldest = [...this.startBatches.values()].sort((a, b) => String(a.startedAt).localeCompare(String(b.startedAt)));
|
|
874
|
+
for (const item of oldest.slice(0, this.startBatches.size - 10)) this.startBatches.delete(item.id);
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
// 不 await:立刻把批次 id 还给面板
|
|
878
|
+
Promise.resolve()
|
|
879
|
+
.then(() => this.startAllSavedJava(cwd, wanted, {
|
|
880
|
+
...options,
|
|
881
|
+
onProgress: (item, done, total) => {
|
|
882
|
+
batch.done = done;
|
|
883
|
+
batch.total = total;
|
|
884
|
+
batch.current = item.id;
|
|
885
|
+
batch.items.push(item);
|
|
886
|
+
},
|
|
887
|
+
}))
|
|
888
|
+
.then((result) => {
|
|
889
|
+
batch.result = result;
|
|
890
|
+
batch.finished = true;
|
|
891
|
+
batch.done = result.total;
|
|
892
|
+
batch.current = '';
|
|
893
|
+
})
|
|
894
|
+
.catch((error) => {
|
|
895
|
+
batch.finished = true;
|
|
896
|
+
batch.current = '';
|
|
897
|
+
batch.result = { ok: false, code: 'UNEXPECTED', message: String(error?.message || error) };
|
|
898
|
+
});
|
|
899
|
+
|
|
900
|
+
return { ok: true, batchId, total: wanted.length, message: `已开始一键启动 ${wanted.length} 个服务` };
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
/** 读一键启动批次进度;id 省略时返回最近一个批次 */
|
|
904
|
+
startBatchStatus(batchId = '') {
|
|
905
|
+
const key = String(batchId || '');
|
|
906
|
+
const batch = key
|
|
907
|
+
? this.startBatches.get(key)
|
|
908
|
+
: [...this.startBatches.values()].sort((a, b) => String(b.startedAt).localeCompare(String(a.startedAt))).pop();
|
|
909
|
+
if (!batch) return { ok: false, code: 'NO_BATCH', message: '没有这个启动批次(可能已经过期,或 DSH 重启过)' };
|
|
910
|
+
return {
|
|
911
|
+
ok: true,
|
|
912
|
+
batchId: batch.id,
|
|
913
|
+
finished: batch.finished,
|
|
914
|
+
done: batch.done,
|
|
915
|
+
total: batch.total,
|
|
916
|
+
current: batch.current,
|
|
917
|
+
startedAt: batch.startedAt,
|
|
918
|
+
items: batch.items,
|
|
919
|
+
result: batch.result,
|
|
920
|
+
};
|
|
921
|
+
}
|
|
922
|
+
|
|
705
923
|
/** 插件级设置(数据目录 / 默认工作区 / Maven 主路径) */
|
|
706
924
|
settings() {
|
|
707
925
|
if (!this.settingsStore) {
|
package/src/restart.js
CHANGED
|
@@ -437,14 +437,8 @@ export function spawnService({ javaExe, argv, cwd, logPath }) {
|
|
|
437
437
|
} catch {
|
|
438
438
|
out = 'ignore';
|
|
439
439
|
}
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
cwd: cwd || undefined,
|
|
443
|
-
detached: true,
|
|
444
|
-
stdio: ['ignore', out, out],
|
|
445
|
-
windowsHide: true,
|
|
446
|
-
});
|
|
447
|
-
child.unref();
|
|
440
|
+
|
|
441
|
+
const closeOut = () => {
|
|
448
442
|
if (typeof out === 'number') {
|
|
449
443
|
try {
|
|
450
444
|
fs.closeSync(out);
|
|
@@ -452,10 +446,47 @@ export function spawnService({ javaExe, argv, cwd, logPath }) {
|
|
|
452
446
|
/* 已交给子进程 */
|
|
453
447
|
}
|
|
454
448
|
}
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
449
|
+
};
|
|
450
|
+
|
|
451
|
+
/**
|
|
452
|
+
* 返回 Promise:等到 'spawn' 或 'error' 再决定成败。
|
|
453
|
+
*
|
|
454
|
+
* 为什么不能直接返回 `{ok:true}`:可执行文件不存在时 spawn **不抛异常**,
|
|
455
|
+
* 而是异步发 'error';没有监听器时 Node 把它当未捕获异常,会把 DSH 整个进程崩掉。
|
|
456
|
+
* 一键启动要连续拉起多个服务,其中一个 javaExe 配错(换机器、JDK 路径变了)
|
|
457
|
+
* 就足以让宿主挂掉,所以这里必须挂监听器,并且如实报告失败原因。
|
|
458
|
+
*/
|
|
459
|
+
return new Promise((resolve) => {
|
|
460
|
+
let child;
|
|
461
|
+
try {
|
|
462
|
+
child = spawn(exe, argv, {
|
|
463
|
+
cwd: cwd || undefined,
|
|
464
|
+
detached: true,
|
|
465
|
+
stdio: ['ignore', out, out],
|
|
466
|
+
windowsHide: true,
|
|
467
|
+
});
|
|
468
|
+
} catch (error) {
|
|
469
|
+
closeOut();
|
|
470
|
+
resolve({ ok: false, message: `拉起进程失败:${error?.message || error}`, logPath: logPath || '' });
|
|
471
|
+
return;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
let settled = false;
|
|
475
|
+
const settle = (value) => {
|
|
476
|
+
if (settled) return;
|
|
477
|
+
settled = true;
|
|
478
|
+
closeOut();
|
|
479
|
+
resolve(value);
|
|
480
|
+
};
|
|
481
|
+
|
|
482
|
+
child.once('error', (error) => {
|
|
483
|
+
settle({ ok: false, message: `拉起进程失败:${error?.message || error}`, logPath: logPath || '' });
|
|
484
|
+
});
|
|
485
|
+
child.once('spawn', () => {
|
|
486
|
+
child.unref();
|
|
487
|
+
settle({ ok: true, pid: child.pid, logPath: logPath || '' });
|
|
488
|
+
});
|
|
489
|
+
});
|
|
459
490
|
}
|
|
460
491
|
|
|
461
492
|
/** 等端口开始 LISTENING(用 TCP 连接探测,不依赖 netstat) */
|