@taole/dd-playwright 0.0.0-stage → 0.1.1

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.
@@ -0,0 +1,183 @@
1
+ import { spawn } from "node:child_process";
2
+ import { existsSync, readFileSync, writeFileSync, unlinkSync, openSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+
6
+ /**
7
+ * 检查指定 PID 的进程是否在运行
8
+ * @param {number} pid
9
+ * @returns {boolean}
10
+ */
11
+ export function isProcessRunning(pid) {
12
+ if (!pid || typeof pid !== "number") return false;
13
+ try {
14
+ // 0 信号用于检测进程是否存在且当前用户有权限发送信号
15
+ process.kill(pid, 0);
16
+ return true;
17
+ } catch (err) {
18
+ return err.code === "EPERM"; // EPERM 表示进程存在但权限不足,也说明活着
19
+ }
20
+ }
21
+
22
+ /**
23
+ * 杀死指定 PID 进程(跨平台兼容)
24
+ * @param {number} pid
25
+ */
26
+ export async function killProcess(pid) {
27
+ if (!isProcessRunning(pid)) return;
28
+ try {
29
+ if (process.platform === "win32") {
30
+ // Windows 下通过 taskkill 递归杀掉子进程
31
+ const { execSync } = await import("node:child_process");
32
+ execSync(`taskkill /pid ${pid} /T /F`, { stdio: "ignore" });
33
+ } else {
34
+ process.kill(pid, "SIGTERM");
35
+ // 等待 1 秒若未退出则强制 SIGKILL
36
+ let count = 0;
37
+ while (isProcessRunning(pid) && count < 10) {
38
+ await new Promise((r) => setTimeout(r, 100));
39
+ count++;
40
+ }
41
+ if (isProcessRunning(pid)) {
42
+ process.kill(pid, "SIGKILL");
43
+ }
44
+ }
45
+ } catch {}
46
+ }
47
+
48
+ /**
49
+ * 读取已保存的 Web 后台进程 PID
50
+ * @param {string} pidFile
51
+ * @returns {number|null}
52
+ */
53
+ export function getStoredWebPid(pidFile) {
54
+ if (!existsSync(pidFile)) return null;
55
+ try {
56
+ const raw = readFileSync(pidFile, "utf8").trim();
57
+ const pid = parseInt(raw, 10);
58
+ if (Number.isInteger(pid) && isProcessRunning(pid)) {
59
+ return pid;
60
+ }
61
+ // 已失效的 PID 文件直接清理
62
+ try {
63
+ unlinkSync(pidFile);
64
+ } catch {}
65
+ return null;
66
+ } catch {
67
+ return null;
68
+ }
69
+ }
70
+
71
+ /**
72
+ * 启动后台常驻 Web 服务
73
+ * @param {object} options
74
+ * @param {number} options.port
75
+ * @param {string} options.dataDir
76
+ * @param {string} options.pidFile
77
+ * @param {string} options.logFile
78
+ */
79
+ export async function startWebDaemon({ port, dataDir, pidFile, logFile }) {
80
+ const existingPid = getStoredWebPid(pidFile);
81
+ if (existingPid) {
82
+ return {
83
+ started: false,
84
+ alreadyRunning: true,
85
+ pid: existingPid,
86
+ url: `http://127.0.0.1:${port}`,
87
+ };
88
+ }
89
+
90
+ // CLI 脚本路径
91
+ const cliPath = fileURLToPath(new URL("../../bin/cli.mjs", import.meta.url));
92
+
93
+ // 打开日志输出文件
94
+ const outLog = openSync(logFile, "a");
95
+ const errLog = openSync(logFile, "a");
96
+
97
+ // 以 detached 模式 spawn node 运行 serve-daemon 命令
98
+ const child = spawn(
99
+ process.execPath,
100
+ [cliPath, "serve-daemon", "--port", String(port)],
101
+ {
102
+ detached: true,
103
+ stdio: ["ignore", outLog, errLog],
104
+ windowsHide: true,
105
+ env: {
106
+ ...process.env,
107
+ },
108
+ },
109
+ );
110
+
111
+ const pid = child.pid;
112
+ writeFileSync(pidFile, String(pid), "utf8");
113
+ child.unref();
114
+
115
+ // 等待最多 5 秒检测端口服务是否就绪
116
+ let ready = false;
117
+ const deadline = Date.now() + 5000;
118
+ while (Date.now() < deadline) {
119
+ try {
120
+ const res = await fetch(`http://127.0.0.1:${port}/api/accounts`);
121
+ if (res.ok) {
122
+ ready = true;
123
+ break;
124
+ }
125
+ } catch {}
126
+ await new Promise((r) => setTimeout(r, 200));
127
+ }
128
+
129
+ return {
130
+ started: ready,
131
+ pid,
132
+ port,
133
+ url: `http://127.0.0.1:${port}`,
134
+ logFile,
135
+ };
136
+ }
137
+
138
+ /**
139
+ * 停止后台常驻 Web 服务
140
+ * @param {string} pidFile
141
+ */
142
+ export async function stopWebDaemon(pidFile) {
143
+ const pid = getStoredWebPid(pidFile);
144
+ if (!pid) {
145
+ return { stopped: false, message: "当前未发现运行中的后台 Web 服务" };
146
+ }
147
+
148
+ await killProcess(pid);
149
+
150
+ try {
151
+ if (existsSync(pidFile)) {
152
+ unlinkSync(pidFile);
153
+ }
154
+ } catch {}
155
+
156
+ return { stopped: true, pid };
157
+ }
158
+
159
+ /**
160
+ * 查询后台常驻 Web 服务运行状态
161
+ * @param {string} pidFile
162
+ * @param {number} [port]
163
+ */
164
+ export async function getWebDaemonStatus(pidFile, port = 12309) {
165
+ const pid = getStoredWebPid(pidFile);
166
+ if (!pid) {
167
+ return { running: false };
168
+ }
169
+
170
+ let responding = false;
171
+ try {
172
+ const res = await fetch(`http://127.0.0.1:${port}/api/accounts`);
173
+ responding = res.ok;
174
+ } catch {}
175
+
176
+ return {
177
+ running: true,
178
+ pid,
179
+ port,
180
+ responding,
181
+ url: `http://127.0.0.1:${port}`,
182
+ };
183
+ }