comp-hub 0.42.2 → 0.42.3
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/bin/cli.js +5 -1
- package/bin/lib/http.js +69 -1
- package/bin/lib/startup.js +66 -15
- package/dist/master.js +34 -24
- package/package.json +1 -1
package/bin/cli.js
CHANGED
package/bin/lib/http.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
const http = require("http");
|
|
2
2
|
const { HTTP_TIMEOUT_GET, HTTP_TIMEOUT_POST } = require("./constants");
|
|
3
|
+
const { apiUrl } = require("./utils");
|
|
3
4
|
|
|
4
5
|
function parseBody(raw) {
|
|
5
6
|
try {
|
|
@@ -59,4 +60,71 @@ function httpPost(url, data) {
|
|
|
59
60
|
});
|
|
60
61
|
}
|
|
61
62
|
|
|
62
|
-
|
|
63
|
+
/**
|
|
64
|
+
* SSE 日志流 — 连接 master 的 /__master__/projects/:hash/logs 端点
|
|
65
|
+
* 持续输出 MidwayJS 子进程的实时日志到终端
|
|
66
|
+
* 返回一个 Promise,在连接关闭时 resolve
|
|
67
|
+
*/
|
|
68
|
+
function streamLogs(url, _hash) {
|
|
69
|
+
return new Promise((resolve, reject) => {
|
|
70
|
+
const urlObj = new URL(url);
|
|
71
|
+
const options = {
|
|
72
|
+
hostname: urlObj.hostname,
|
|
73
|
+
port: urlObj.port,
|
|
74
|
+
path: urlObj.pathname,
|
|
75
|
+
method: "GET",
|
|
76
|
+
headers: { Accept: "text/event-stream" }
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const req = http.request(options, res => {
|
|
80
|
+
if (res.statusCode !== 200) {
|
|
81
|
+
reject(new Error(`SSE connection failed (HTTP ${res.statusCode})`));
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
let buffer = "";
|
|
86
|
+
|
|
87
|
+
res.on("data", chunk => {
|
|
88
|
+
buffer += chunk.toString();
|
|
89
|
+
// SSE 格式: "data: <json>\n\n"
|
|
90
|
+
const events = buffer.split("\n\n");
|
|
91
|
+
buffer = events.pop() || ""; // 保留不完整的最后一个事件
|
|
92
|
+
|
|
93
|
+
for (const event of events) {
|
|
94
|
+
const dataLine = event.startsWith("data: ") ? event.slice(6) : event;
|
|
95
|
+
try {
|
|
96
|
+
const line = JSON.parse(dataLine);
|
|
97
|
+
// 直接输出日志内容(已有换行符)
|
|
98
|
+
process.stdout.write(line);
|
|
99
|
+
} catch {
|
|
100
|
+
// 非 JSON 数据,忽略
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
res.on("end", () => {
|
|
106
|
+
resolve();
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
res.on("error", err => {
|
|
110
|
+
reject(err);
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
req.on("error", err => {
|
|
115
|
+
reject(err);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
req.setTimeout(0); // 无超时限制
|
|
119
|
+
req.end();
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* 从 master 注销项目(Ctrl+C 时通知 master 停止对应 MidwayJS 子进程)
|
|
125
|
+
*/
|
|
126
|
+
function deregisterProject(masterPort, hash) {
|
|
127
|
+
return httpPost(apiUrl(masterPort, "/__master__/deregister"), { hash });
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
module.exports = { httpGet, httpPost, streamLogs, deregisterProject };
|
package/bin/lib/startup.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
const path = require("path");
|
|
2
2
|
const fs = require("fs");
|
|
3
3
|
const { spawn } = require("child_process");
|
|
4
|
-
const { httpPost } = require("./http");
|
|
4
|
+
const { httpPost, streamLogs, deregisterProject } = require("./http");
|
|
5
5
|
const { computeHash, projectRoot, readMasterRuntime, loadComphubConfig, colors, apiUrl } = require("./utils");
|
|
6
6
|
const { cyan, green, white, yellow, red } = colors;
|
|
7
7
|
const { ADMIN_API, POLL_INTERVAL, MAX_RETRIES, DEFAULT_PORT } = require("./constants");
|
|
@@ -34,7 +34,7 @@ async function waitForMasterRuntime() {
|
|
|
34
34
|
return null;
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
async function spawnMaster(publicPort, pkg, cwd
|
|
37
|
+
async function spawnMaster(publicPort, pkg, cwd) {
|
|
38
38
|
const masterEntry = path.join(projectRoot, "dist/master.js");
|
|
39
39
|
|
|
40
40
|
if (!fs.existsSync(masterEntry)) {
|
|
@@ -42,8 +42,12 @@ async function spawnMaster(publicPort, pkg, cwd, _opts) {
|
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
const masterArgs = ["--port", String(publicPort), "--public-port", String(publicPort)];
|
|
45
|
+
// 不再使用 stdio:inherit,改为 pipe stdout/stderr:
|
|
46
|
+
// - stdin: inherit(Ctrl+C 信号需要)
|
|
47
|
+
// - stdout: pipe(过滤掉 MidwayJS 子进程日志,只保留 master 自身消息)
|
|
48
|
+
// - stderr: pipe(转发给 CLI 用于崩溃诊断)
|
|
45
49
|
const child = spawn(process.execPath, ["--no-deprecation", masterEntry, ...masterArgs], {
|
|
46
|
-
stdio: "inherit",
|
|
50
|
+
stdio: ["inherit", "pipe", "pipe"],
|
|
47
51
|
cwd,
|
|
48
52
|
env: {
|
|
49
53
|
...process.env,
|
|
@@ -51,6 +55,32 @@ async function spawnMaster(publicPort, pkg, cwd, _opts) {
|
|
|
51
55
|
}
|
|
52
56
|
});
|
|
53
57
|
|
|
58
|
+
// stdout 行缓冲(跨 data 事件的未完成行)
|
|
59
|
+
let stdoutPending = "";
|
|
60
|
+
|
|
61
|
+
// 转发 stderr 给 CLI(用于崩溃诊断)
|
|
62
|
+
if (child.stderr) {
|
|
63
|
+
child.stderr.on("data", chunk => {
|
|
64
|
+
process.stderr.write(chunk);
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// 过滤 stdout:只转发 master 自身消息,跳过带 [hash] 前缀的 MidwayJS 子进程日志
|
|
69
|
+
// MidwayJS 日志会通过 SSE 按项目过滤后展示
|
|
70
|
+
if (child.stdout) {
|
|
71
|
+
child.stdout.on("data", chunk => {
|
|
72
|
+
stdoutPending += chunk.toString();
|
|
73
|
+
const lines = stdoutPending.split("\n");
|
|
74
|
+
stdoutPending = lines.pop() || ""; // 保留不完整行
|
|
75
|
+
for (const line of lines) {
|
|
76
|
+
// 跳过 [8位hex_hash] 前缀的行(属于 MidwayJS 子进程日志)
|
|
77
|
+
if (!/^\[[0-9a-f]{8}\]/.test(line)) {
|
|
78
|
+
process.stdout.write(line + "\n");
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
54
84
|
// 转发父进程信号,确保 Ctrl+C 时 master 也能优雅退出
|
|
55
85
|
["SIGINT", "SIGTERM"].forEach(sig => {
|
|
56
86
|
process.on(sig, () => {
|
|
@@ -60,6 +90,10 @@ async function spawnMaster(publicPort, pkg, cwd, _opts) {
|
|
|
60
90
|
|
|
61
91
|
// Master 退出时 CLI 也跟着退出
|
|
62
92
|
child.on("exit", code => {
|
|
93
|
+
// 输出剩余的 pending 数据
|
|
94
|
+
if (stdoutPending && !/^\[[0-9a-f]{8}\]/.test(stdoutPending)) {
|
|
95
|
+
process.stdout.write(stdoutPending);
|
|
96
|
+
}
|
|
63
97
|
process.exit(code || 0);
|
|
64
98
|
});
|
|
65
99
|
|
|
@@ -69,7 +103,7 @@ async function spawnMaster(publicPort, pkg, cwd, _opts) {
|
|
|
69
103
|
throw new Error("Master startup timeout");
|
|
70
104
|
}
|
|
71
105
|
|
|
72
|
-
return { runtime
|
|
106
|
+
return { runtime };
|
|
73
107
|
}
|
|
74
108
|
|
|
75
109
|
/**
|
|
@@ -79,7 +113,6 @@ async function spawnMaster(publicPort, pkg, cwd, _opts) {
|
|
|
79
113
|
async function ensureMasterRunning(opts, pkg) {
|
|
80
114
|
let portMismatch = false;
|
|
81
115
|
let runningPublicPort = 0;
|
|
82
|
-
let masterChild = null;
|
|
83
116
|
|
|
84
117
|
const existingRuntime = await readMasterRuntime();
|
|
85
118
|
|
|
@@ -90,17 +123,16 @@ async function ensureMasterRunning(opts, pkg) {
|
|
|
90
123
|
portMismatch = true;
|
|
91
124
|
runningPublicPort = existingRuntime.publicPort;
|
|
92
125
|
}
|
|
93
|
-
return { runtime: existingRuntime, portMismatch, runningPublicPort
|
|
126
|
+
return { runtime: existingRuntime, portMismatch, runningPublicPort };
|
|
94
127
|
}
|
|
95
128
|
|
|
96
129
|
// 启动新 master(前台运行,日志输出到终端)
|
|
97
130
|
const portHint = opts.port != null ? opts.port : DEFAULT_PORT;
|
|
98
131
|
const publicPort = await getPort({ port: getPort.makeRange(portHint, portHint + 100) });
|
|
99
132
|
|
|
100
|
-
const { runtime
|
|
101
|
-
masterChild = child;
|
|
133
|
+
const { runtime } = await spawnMaster(publicPort, pkg, process.cwd());
|
|
102
134
|
|
|
103
|
-
return { runtime, portMismatch, runningPublicPort
|
|
135
|
+
return { runtime, portMismatch, runningPublicPort };
|
|
104
136
|
}
|
|
105
137
|
|
|
106
138
|
// ---- Registration ----
|
|
@@ -183,7 +215,7 @@ async function main(pkg, opts) {
|
|
|
183
215
|
|
|
184
216
|
console.log(`${green("✓")} ${pkg.name} v${pkg.version}`);
|
|
185
217
|
|
|
186
|
-
const { runtime, portMismatch, runningPublicPort
|
|
218
|
+
const { runtime, portMismatch, runningPublicPort } = await ensureMasterRunning(opts, pkg);
|
|
187
219
|
|
|
188
220
|
const masterPort = runtime.managementPort;
|
|
189
221
|
const displayPort = runtime.publicPort;
|
|
@@ -192,11 +224,30 @@ async function main(pkg, opts) {
|
|
|
192
224
|
|
|
193
225
|
printBanner(pkg, cwd, hash, displayPort, opts, { portMismatch, runningPublicPort });
|
|
194
226
|
|
|
195
|
-
//
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
227
|
+
// Ctrl+C / kill 时通知 master 注销本项目,使 MidwayJS 子进程能够停止
|
|
228
|
+
let deregistered = false;
|
|
229
|
+
const cleanup = async () => {
|
|
230
|
+
if (deregistered) return;
|
|
231
|
+
deregistered = true;
|
|
232
|
+
try {
|
|
233
|
+
await deregisterProject(masterPort, hash);
|
|
234
|
+
} catch {
|
|
235
|
+
// 忽略注销失败(master 可能已关闭)
|
|
236
|
+
}
|
|
237
|
+
process.exit(0);
|
|
238
|
+
};
|
|
239
|
+
process.on("SIGINT", cleanup);
|
|
240
|
+
process.on("SIGTERM", cleanup);
|
|
241
|
+
|
|
242
|
+
// 统一通过 SSE 实时接收本项目 MidwayJS 子进程日志(按 hash 过滤)
|
|
243
|
+
// 第一个 CLI(spawn master 的)不再通过 stdio:inherit 看到其他项目的日志
|
|
244
|
+
console.log(`\n ${white("─")} Streaming logs from MidwayJS (${hash}) ${white("─")}\n`);
|
|
245
|
+
|
|
246
|
+
const logsUrl = `http://localhost:${masterPort}/__master__/projects/${hash}/logs`;
|
|
247
|
+
try {
|
|
248
|
+
await streamLogs(logsUrl, hash);
|
|
249
|
+
} catch (err) {
|
|
250
|
+
console.error(`\n ${red("✗")} Log stream error: ${err.message}`);
|
|
200
251
|
}
|
|
201
252
|
}
|
|
202
253
|
|