mioku-service-webui 4.4.0 → 4.4.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.
package/index.ts CHANGED
@@ -30,6 +30,7 @@ import {
30
30
  createStrangerRoutes,
31
31
  createAccessControlRoutes,
32
32
  } from "./routes";
33
+ import { freePort } from "./port-utils";
33
34
 
34
35
  export interface WebUIServiceAPI {
35
36
  getSettings(): WebUISettings;
@@ -44,7 +45,7 @@ class WebUIRuntime {
44
45
  logger.info(`[webui-action] ${action}${text}`);
45
46
  }
46
47
 
47
- public initRoutes(): void {
48
+ public async initRoutes(): Promise<void> {
48
49
  const { upgradeWebSocket, injectWebSocket } = createNodeWebSocket({
49
50
  app: this.app,
50
51
  });
@@ -172,6 +173,12 @@ class WebUIRuntime {
172
173
  });
173
174
 
174
175
  const settings = getWebUISettings();
176
+ const freed = await freePort(settings.port);
177
+ if (freed.remainingPids.length > 0) {
178
+ throw new Error(
179
+ `webui 端口 ${settings.port} 被占用且无法结束 (PID: ${freed.remainingPids.join(", ")})`,
180
+ );
181
+ }
175
182
  const server = serve({
176
183
  fetch: this.app.fetch,
177
184
  port: settings.port,
@@ -232,7 +239,7 @@ const webUIService: MiokuService = {
232
239
  initBuiltinDatasources();
233
240
  ensureAuthConfig();
234
241
  await ensureWebUIDist();
235
- runtime.initRoutes();
242
+ await runtime.initRoutes();
236
243
  },
237
244
 
238
245
  async dispose() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mioku-service-webui",
3
- "version": "4.4.0",
3
+ "version": "4.4.1",
4
4
  "description": "Mioku WebUI 服务",
5
5
  "main": "index.ts",
6
6
  "type": "module",
package/port-utils.ts ADDED
@@ -0,0 +1,159 @@
1
+ import { execFile } from "node:child_process";
2
+ import { promisify } from "node:util";
3
+ import { logger } from "mioki";
4
+
5
+ const execFileAsync = promisify(execFile);
6
+
7
+ export interface FreePortLogger {
8
+ info: (msg: string) => void;
9
+ warn: (msg: string) => void;
10
+ error: (msg: string) => void;
11
+ }
12
+
13
+ export interface FreePortOptions {
14
+ logger?: FreePortLogger;
15
+ graceMs?: number;
16
+ sigkillDelayMs?: number;
17
+ }
18
+
19
+ export interface FreePortResult {
20
+ port: number;
21
+ wasOccupied: boolean;
22
+ killedPids: number[];
23
+ remainingPids: number[];
24
+ lsofAvailable: boolean;
25
+ }
26
+
27
+ function sleep(ms: number): Promise<void> {
28
+ return new Promise((resolve) => setTimeout(resolve, ms));
29
+ }
30
+
31
+ function parsePids(raw: string): number[] {
32
+ const pids = new Set<number>();
33
+ for (const line of raw.split(/\r?\n/)) {
34
+ const trimmed = line.trim();
35
+ if (!trimmed) continue;
36
+ const pid = Number(trimmed);
37
+ if (Number.isFinite(pid) && pid > 0 && Math.floor(pid) === pid) {
38
+ pids.add(pid);
39
+ }
40
+ }
41
+ return Array.from(pids);
42
+ }
43
+
44
+ async function listListeningPids(
45
+ port: number,
46
+ ): Promise<{ pids: number[]; lsofAvailable: boolean }> {
47
+ try {
48
+ const { stdout } = await execFileAsync("lsof", [
49
+ "-nP",
50
+ `-iTCP:${port}`,
51
+ "-sTCP:LISTEN",
52
+ "-t",
53
+ ]);
54
+ return { pids: parsePids(stdout), lsofAvailable: true };
55
+ } catch (err: any) {
56
+ if (err?.code === "ENOENT") {
57
+ return { pids: [], lsofAvailable: false };
58
+ }
59
+ return { pids: [], lsofAvailable: true };
60
+ }
61
+ }
62
+
63
+ async function sendSignal(
64
+ pid: number,
65
+ signal: NodeJS.Signals,
66
+ ): Promise<boolean> {
67
+ if (pid === process.pid) return false;
68
+ try {
69
+ process.kill(pid, signal);
70
+ return true;
71
+ } catch {
72
+ return false;
73
+ }
74
+ }
75
+
76
+ export async function freePort(
77
+ port: number,
78
+ options: FreePortOptions = {},
79
+ ): Promise<FreePortResult> {
80
+ const log = options.logger ?? (logger as unknown as FreePortLogger);
81
+ const graceMs = options.graceMs ?? 1500;
82
+ const sigkillDelayMs = options.sigkillDelayMs ?? 500;
83
+
84
+ const initial = await listListeningPids(port);
85
+
86
+ if (!initial.lsofAvailable) {
87
+ log.warn(
88
+ `未检测到 lsof 命令,无法定位端口 ${port} 的占用进程,建议安装 lsof`,
89
+ );
90
+ return {
91
+ port,
92
+ wasOccupied: false,
93
+ killedPids: [],
94
+ remainingPids: [],
95
+ lsofAvailable: false,
96
+ };
97
+ }
98
+
99
+ const occupied = initial.pids.filter((pid) => pid !== process.pid);
100
+ if (occupied.length === 0) {
101
+ return {
102
+ port,
103
+ wasOccupied: false,
104
+ killedPids: [],
105
+ remainingPids: [],
106
+ lsofAvailable: true,
107
+ };
108
+ }
109
+
110
+ log.warn(
111
+ `端口 ${port} 被占用,进程 PID: ${occupied.join(", ")},正在尝试结束...`,
112
+ );
113
+
114
+ const killed: number[] = [];
115
+ for (const pid of occupied) {
116
+ if (await sendSignal(pid, "SIGTERM")) {
117
+ killed.push(pid);
118
+ }
119
+ }
120
+
121
+ if (killed.length > 0) {
122
+ await sleep(graceMs);
123
+ }
124
+
125
+ let remaining = (await listListeningPids(port)).pids.filter(
126
+ (pid) => pid !== process.pid,
127
+ );
128
+
129
+ if (remaining.length > 0) {
130
+ log.warn(
131
+ `端口 ${port} 仍被占用 (PID: ${remaining.join(", ")}),发送 SIGKILL 强制结束...`,
132
+ );
133
+ for (const pid of remaining) {
134
+ if (await sendSignal(pid, "SIGKILL")) {
135
+ killed.push(pid);
136
+ }
137
+ }
138
+ await sleep(sigkillDelayMs);
139
+ remaining = (await listListeningPids(port)).pids.filter(
140
+ (pid) => pid !== process.pid,
141
+ );
142
+ }
143
+
144
+ if (remaining.length > 0) {
145
+ log.error(
146
+ `端口 ${port} 仍被占用,无法结束进程 PID: ${remaining.join(", ")}`,
147
+ );
148
+ } else {
149
+ log.info(`端口 ${port} 已释放`);
150
+ }
151
+
152
+ return {
153
+ port,
154
+ wasOccupied: true,
155
+ killedPids: killed,
156
+ remainingPids: remaining,
157
+ lsofAvailable: true,
158
+ };
159
+ }
@@ -514,7 +514,7 @@ export function createDataManagementRoutes() {
514
514
 
515
515
  app.post("/node-modules/refresh", async (c) => {
516
516
  const settings = getWebUISettings();
517
- const command = getInstallCommand(settings.packageManager);
517
+ const command = getInstallCommand();
518
518
  const startedAt = Date.now();
519
519
  const result = await runCommand(command.cmd, command.args, process.cwd());
520
520
  const finishedAt = Date.now();
package/system.ts CHANGED
@@ -96,6 +96,66 @@ const BOOT_CONFIG_PATH = path.join(
96
96
  "base.json",
97
97
  );
98
98
 
99
+ export interface BootSystemCommandsConfig {
100
+ update: {
101
+ enabled: boolean;
102
+ selectTimeoutMs: number;
103
+ };
104
+ install: {
105
+ enabled: boolean;
106
+ };
107
+ market: {
108
+ enabled: boolean;
109
+ };
110
+ restart: {
111
+ enabled: boolean;
112
+ };
113
+ }
114
+
115
+ const DEFAULT_BOOT_SYSTEM_COMMANDS_CONFIG: BootSystemCommandsConfig = {
116
+ update: {
117
+ enabled: true,
118
+ selectTimeoutMs: 60000,
119
+ },
120
+ install: {
121
+ enabled: true,
122
+ },
123
+ market: {
124
+ enabled: true,
125
+ },
126
+ restart: {
127
+ enabled: true,
128
+ },
129
+ };
130
+
131
+ function normalizeBootSystemCommandsConfig(
132
+ input: any,
133
+ ): BootSystemCommandsConfig {
134
+ const merged = deepMerge(
135
+ cloneJson(DEFAULT_BOOT_SYSTEM_COMMANDS_CONFIG),
136
+ input || {},
137
+ );
138
+ const rawTimeout = Number(merged?.update?.selectTimeoutMs);
139
+ return {
140
+ update: {
141
+ enabled: Boolean(merged?.update?.enabled),
142
+ selectTimeoutMs:
143
+ Number.isFinite(rawTimeout) && rawTimeout >= 1000
144
+ ? Math.floor(rawTimeout)
145
+ : DEFAULT_BOOT_SYSTEM_COMMANDS_CONFIG.update.selectTimeoutMs,
146
+ },
147
+ install: {
148
+ enabled: Boolean(merged?.install?.enabled),
149
+ },
150
+ market: {
151
+ enabled: Boolean(merged?.market?.enabled),
152
+ },
153
+ restart: {
154
+ enabled: Boolean(merged?.restart?.enabled),
155
+ },
156
+ };
157
+ }
158
+
99
159
  interface AccessRuleConfig {
100
160
  whitelist: Array<string | number>;
101
161
  blacklist: Array<string | number>;
@@ -1345,8 +1405,7 @@ export async function getManagedPackageDetail(
1345
1405
  const missingServices = checkDependentServices(pkg);
1346
1406
 
1347
1407
  const isSystemPlugin = target === "plugin" ? isSystemPluginName(name) : false;
1348
- const isSystemService =
1349
- target === "service" ? isSystemServiceName(name) : false;
1408
+ const isSystemService = false;
1350
1409
  return {
1351
1410
  ok: true,
1352
1411
  data: {