Skip to content

Commit 7d08758

Browse files
committed
✨ feat: 新增 socket 服务
1 parent 2554a38 commit 7d08758

9 files changed

Lines changed: 476 additions & 8 deletions

File tree

electron/main/index.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,12 @@ import { initTray, MainTray } from "./tray";
77
import { processLog } from "./logger";
88
import { existsSync, mkdirSync } from "fs";
99
import { join } from "path";
10+
import { trySendCustomProtocol } from "./utils/protocol";
11+
import { SocketService } from "./services/SocketService";
1012
import initAppServer from "../server";
1113
import loadWindow from "./windows/load-window";
1214
import mainWindow from "./windows/main-window";
1315
import initIpc from "./ipc";
14-
import { trySendCustomProtocol } from "./utils/protocol";
1516

1617
// 屏蔽报错
1718
process.env.ELECTRON_DISABLE_SECURITY_WARNINGS = "true";
@@ -57,6 +58,8 @@ class MainProcess {
5758
this.mainTray = initTray(this.mainWindow!);
5859
// 注册 IPC 通信
5960
initIpc();
61+
// 自动启动 WebSocket
62+
SocketService.tryAutoStart();
6063
});
6164
}
6265
// 应用程序事件

electron/main/ipc/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import initUpdateIpc from "./ipc-update";
99
import initWindowsIpc from "./ipc-window";
1010
import initProtocolIpc from "./ipc-protocol";
1111
import initCacheIpc from "./ipc-cache";
12+
import initSocketIpc from "./ipc-socket";
1213

1314
/**
1415
* 初始化全部 IPC 通信
@@ -26,6 +27,7 @@ const initIpc = (): void => {
2627
initShortcutIpc();
2728
initProtocolIpc();
2829
initCacheIpc();
30+
initSocketIpc();
2931
};
3032

3133
export default initIpc;

electron/main/ipc/ipc-socket.ts

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import { ipcMain } from "electron";
2+
import { SocketService } from "../services/SocketService";
3+
import { processLog } from "../logger";
4+
import { useStore } from "../store";
5+
6+
type SocketIpcResult<T = unknown> = {
7+
success: boolean;
8+
data?: T;
9+
message?: string;
10+
};
11+
12+
const withErrorCatch = async <T>(action: () => Promise<T>): Promise<SocketIpcResult<T>> => {
13+
try {
14+
const data = await action();
15+
return { success: true, data };
16+
} catch (error) {
17+
const message = error instanceof Error ? error.message : String(error);
18+
processLog.error("❌ IPC socket error:", error);
19+
return { success: false, message };
20+
}
21+
};
22+
23+
/**
24+
* 初始化 WebSocket 相关 IPC
25+
*/
26+
const initSocketIpc = (): void => {
27+
const socketService = SocketService.getInstance();
28+
29+
// 启动 WebSocket 服务
30+
ipcMain.handle("socket-start", async (): Promise<SocketIpcResult<{ port: number }>> => {
31+
return withErrorCatch(async () => {
32+
const store = useStore();
33+
const websocketConfig = store.get("websocket");
34+
const port = websocketConfig?.port ?? 25885;
35+
// 先检查端口是否可用
36+
const isAvailable = await socketService.testPort(port);
37+
if (!isAvailable) {
38+
throw new Error(`端口 ${port} 不可用,请先测试并保存可用端口`);
39+
}
40+
const result = await socketService.start(port, true);
41+
store.set("websocket", { enabled: true, port });
42+
return result;
43+
});
44+
});
45+
46+
// 停止 WebSocket 服务
47+
ipcMain.handle("socket-stop", async (): Promise<SocketIpcResult<null>> => {
48+
return withErrorCatch(async () => {
49+
await socketService.stop();
50+
const store = useStore();
51+
store.set("websocket.enabled", false);
52+
53+
return null;
54+
});
55+
});
56+
57+
// 查询当前 WebSocket 状态
58+
ipcMain.handle(
59+
"socket-status",
60+
async (): Promise<SocketIpcResult<{ running: boolean; port: number | null }>> => {
61+
return withErrorCatch(async () => {
62+
return {
63+
running: socketService.isRunning(),
64+
port: socketService.getPort(),
65+
};
66+
});
67+
},
68+
);
69+
70+
// 测试指定端口
71+
ipcMain.handle(
72+
"socket-test-port",
73+
async (_event, port: number): Promise<SocketIpcResult<null>> => {
74+
return withErrorCatch(async () => {
75+
const isAvailable = await socketService.testPort(port);
76+
if (!isAvailable) throw new Error(`端口 ${port} 不可用`);
77+
return null;
78+
});
79+
},
80+
);
81+
};
82+
83+
export default initSocketIpc;
Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
1+
import { WebSocketServer, type WebSocket } from "ws";
2+
import { createServer } from "net";
3+
import { serverLog } from "../logger";
4+
import { useStore } from "../store";
5+
6+
/**
7+
* WebSocket 主服务
8+
*/
9+
export class SocketService {
10+
private static instance: SocketService;
11+
12+
private wss: WebSocketServer | null = null;
13+
private clients: Set<WebSocket> = new Set();
14+
private currentPort: number | null = null;
15+
16+
private constructor() {}
17+
18+
public static getInstance(): SocketService {
19+
if (!SocketService.instance) {
20+
SocketService.instance = new SocketService();
21+
}
22+
return SocketService.instance;
23+
}
24+
25+
/**
26+
* 当前是否已启动 WebSocket 服务
27+
*/
28+
public isRunning(): boolean {
29+
return this.wss !== null;
30+
}
31+
32+
/**
33+
* 获取当前监听端口
34+
*/
35+
public getPort(): number | null {
36+
return this.currentPort;
37+
}
38+
39+
/**
40+
* 启动 WebSocket 服务
41+
* @param portOverride 可选端口
42+
* @param forceRestart 是否强制重启
43+
*/
44+
public async start(
45+
portOverride?: number,
46+
forceRestart: boolean = false,
47+
): Promise<{ port: number }> {
48+
const store = useStore();
49+
const websocketConfig = store.get("websocket");
50+
const portFromStore = websocketConfig?.port;
51+
const port = portOverride ?? portFromStore ?? 25885;
52+
53+
// 如果服务已在运行
54+
if (this.wss && this.currentPort !== null) {
55+
// 如果端口相同,直接返回
56+
if (this.currentPort === port) {
57+
return { port: this.currentPort };
58+
}
59+
// 如果端口不同且需要强制重启,先停止再启动
60+
if (forceRestart) {
61+
await this.stop();
62+
} else {
63+
// 否则返回当前端口
64+
return { port: this.currentPort };
65+
}
66+
}
67+
68+
serverLog.info(`🔌 Trying to start WebSocket server on port ${port}`);
69+
70+
// 先验证端口是否可用
71+
const isAvailable = await this.testPort(port);
72+
if (!isAvailable) throw new Error(`端口 ${port} 不可用`);
73+
74+
return new Promise<{ port: number }>((resolve, reject) => {
75+
try {
76+
const wss = new WebSocketServer({ port });
77+
this.wss = wss;
78+
this.currentPort = port;
79+
80+
wss.on("connection", (socket: WebSocket) => {
81+
this.clients.add(socket);
82+
serverLog.info("🔗 WebSocket client connected");
83+
84+
socket.on("close", () => {
85+
this.clients.delete(socket);
86+
serverLog.info("🔌 WebSocket client disconnected");
87+
});
88+
89+
socket.on("error", (error: Error) => {
90+
serverLog.error("⚠️ WebSocket client error:", error);
91+
});
92+
});
93+
94+
wss.once("listening", () => {
95+
serverLog.info(`✅ WebSocket server started on port ${port}`);
96+
resolve({ port });
97+
});
98+
99+
wss.once("error", (error: Error) => {
100+
serverLog.error("❌ WebSocket server failed to start:", error);
101+
this.cleanupServer();
102+
reject(error);
103+
});
104+
} catch (error) {
105+
serverLog.error("❌ WebSocket server creation error:", error);
106+
this.cleanupServer();
107+
reject(error instanceof Error ? error : new Error(String(error)));
108+
}
109+
});
110+
}
111+
112+
/**
113+
* 测试 socket 端口是否可用(可以绑定)
114+
* @param port 要测试的端口
115+
* @returns 如果端口可用返回 true,否则返回 false
116+
*/
117+
public async testPort(port: number): Promise<boolean> {
118+
return new Promise<boolean>((resolve) => {
119+
const server = createServer();
120+
121+
server.once("error", (error: NodeJS.ErrnoException) => {
122+
// 端口被占用或权限不足
123+
if (error.code === "EADDRINUSE" || error.code === "EACCES") {
124+
resolve(false);
125+
} else {
126+
resolve(false);
127+
}
128+
});
129+
130+
server.once("listening", () => {
131+
// 端口可用,立即关闭测试服务器
132+
server.close(() => {
133+
resolve(true);
134+
});
135+
});
136+
137+
try {
138+
server.listen(port, "127.0.0.1");
139+
} catch {
140+
resolve(false);
141+
}
142+
});
143+
}
144+
145+
/**
146+
* 尝试自动启动
147+
*/
148+
public async tryAutoStart(): Promise<void> {
149+
const store = useStore();
150+
try {
151+
const websocketConfig = store.get("websocket");
152+
if (!websocketConfig?.enabled) return;
153+
const { port } = await this.start(websocketConfig.port, false);
154+
serverLog.info(`🔌 Auto-start WebSocket server on port ${port}`);
155+
store.set("websocket", { enabled: true, port });
156+
} catch (error) {
157+
serverLog.error("❌ Error while auto-starting WebSocket server from store:", error);
158+
store.set("websocket.enabled", false);
159+
}
160+
}
161+
162+
/**
163+
* 关闭 WebSocket 服务
164+
*/
165+
public async stop(): Promise<void> {
166+
if (!this.wss) return;
167+
168+
const server = this.wss;
169+
serverLog.info("🛑 Stopping WebSocket server...");
170+
171+
// 关闭所有客户端
172+
for (const client of this.clients) {
173+
try {
174+
client.close();
175+
} catch {
176+
// ignore
177+
}
178+
}
179+
this.clients.clear();
180+
181+
await new Promise<void>((resolve) => {
182+
server.close(() => {
183+
serverLog.info("✅ WebSocket server stopped");
184+
resolve();
185+
});
186+
});
187+
188+
this.cleanupServer();
189+
}
190+
191+
/**
192+
* 清理 WebSocket 服务
193+
*/
194+
private cleanupServer(): void {
195+
this.wss = null;
196+
this.currentPort = null;
197+
this.clients.clear();
198+
}
199+
200+
/**
201+
* 便于主进程调用自动启动
202+
*/
203+
public static async tryAutoStart(): Promise<void> {
204+
const instance = SocketService.getInstance();
205+
await instance.tryAutoStart();
206+
}
207+
}

electron/main/store/index.ts

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,29 +9,47 @@ import Store from "electron-store";
99
storeLog.info("🌱 Store init");
1010

1111
export interface StoreType {
12+
/** 窗口 */
1213
window: {
14+
/** 窗口宽度 */
1315
width: number;
16+
/** 窗口高度 */
1417
height: number;
18+
/** 窗口位置 x */
1519
x?: number;
20+
/** 窗口位置 y */
1621
y?: number;
22+
/** 是否最大化 */
1723
maximized?: boolean;
1824
};
25+
/** 歌词 */
1926
lyric: {
20-
// 窗口位置
27+
/** 窗口位置 x */
2128
x?: number;
29+
/** 窗口位置 y */
2230
y?: number;
31+
/** 窗口宽度 */
2332
width?: number;
33+
/** 窗口高度 */
2434
height?: number;
25-
// 配置
35+
/** 配置 */
2636
config?: LyricConfig;
2737
};
38+
/** 代理 */
2839
proxy: string;
29-
// amll-db-server
40+
/** amll-db-server */
3041
amllDbServer: string;
31-
// 缓存地址
42+
/** 缓存地址 */
3243
cachePath: string;
33-
// 缓存大小限制 (GB)
44+
/** 缓存大小限制 (GB) */
3445
cacheLimit: number;
46+
/** websocket */
47+
websocket: {
48+
/** 是否启用 */
49+
enabled: boolean;
50+
/** 端口 */
51+
port: number;
52+
};
3553
}
3654

3755
/**
@@ -58,6 +76,11 @@ export const useStore = () => {
5876
amllDbServer: defaultAMLLDbServer,
5977
cachePath: join(app.getPath("userData"), "DataCache"),
6078
cacheLimit: 10, // 默认 10GB
79+
// websocket
80+
websocket: {
81+
enabled: false,
82+
port: 25885,
83+
},
6184
},
6285
});
6386
};

0 commit comments

Comments
 (0)