1- const { app, BrowserWindow, Menu, shell, ipcMain } = require ( 'electron' ) ;
1+ const { app, BrowserWindow, Menu, shell, ipcMain, dialog } = require ( 'electron' ) ;
22const path = require ( 'path' ) ;
33const net = require ( 'net' ) ;
44
@@ -7,6 +7,21 @@ const DEFAULT_PORT = 3000;
77let backendServer = null ;
88let currentBackendPort = null ;
99
10+ // 单实例锁
11+ const gotTheLock = app . requestSingleInstanceLock ( ) ;
12+
13+ if ( ! gotTheLock ) {
14+ // 已有实例在运行,退出当前实例
15+ // 注意:不能在这里直接调用 dialog,因为 app 可能还未准备好
16+ console . log ( 'WebSSH 应用已在运行中,退出当前实例...' ) ;
17+
18+ // 使用延迟退出,确保应用完全退出
19+ process . nextTick ( ( ) => {
20+ app . quit ( ) ;
21+ process . exit ( 0 ) ;
22+ } ) ;
23+ }
24+
1025function isPortInUse ( port ) {
1126 return new Promise ( ( resolve ) => {
1227 const server = net . createServer ( ) ;
@@ -19,6 +34,29 @@ function isPortInUse(port) {
1934 } ) ;
2035}
2136
37+ // 检查应用是否已在运行(端口检测作为后备)
38+ async function isAppAlreadyRunning ( ) {
39+ // 检查默认端口是否被占用
40+ const defaultPortInUse = await isPortInUse ( DEFAULT_PORT ) ;
41+ if ( defaultPortInUse ) {
42+ // 检查是否是WebSSH后端(发送HTTP请求验证)
43+ try {
44+ const response = await new Promise ( ( resolve ) => {
45+ const http = require ( 'http' ) ;
46+ const req = http . get ( `http://localhost:${ DEFAULT_PORT } /api/crypto/public-key` , ( res ) => {
47+ resolve ( res . statusCode === 200 ) ;
48+ } ) ;
49+ req . on ( 'error' , ( ) => resolve ( false ) ) ;
50+ req . setTimeout ( 1000 , ( ) => resolve ( false ) ) ;
51+ } ) ;
52+ return response ;
53+ } catch ( error ) {
54+ return false ;
55+ }
56+ }
57+ return false ;
58+ }
59+
2260function getAvailablePort ( startPort = DEFAULT_PORT , maxPort = 3100 ) {
2361 return new Promise ( ( resolve ) => {
2462 const tryPort = ( port ) => {
@@ -195,7 +233,44 @@ ipcMain.handle('get-backend-port', () => {
195233 return currentBackendPort || DEFAULT_PORT ;
196234} ) ;
197235
198- app . whenReady ( ) . then ( ( ) => {
236+ // 当第二个实例启动时,激活已有窗口
237+ app . on ( 'second-instance' , ( event , commandLine , workingDirectory ) => {
238+ // 有人尝试运行第二个实例,我们应该聚焦到已有窗口
239+ if ( mainWindow ) {
240+ if ( mainWindow . isMinimized ( ) ) {
241+ mainWindow . restore ( ) ;
242+ }
243+ mainWindow . focus ( ) ;
244+ }
245+ } ) ;
246+
247+ app . whenReady ( ) . then ( async ( ) => {
248+ // 如果单实例锁获取失败,显示对话框并退出
249+ if ( ! gotTheLock ) {
250+ dialog . showMessageBoxSync ( {
251+ type : 'info' ,
252+ title : 'WebSSH' ,
253+ message : 'WebSSH 应用已在运行中,请勿重复启动。' ,
254+ detail : '如果您需要启动新窗口,请在已运行的实例中打开新标签页。'
255+ } ) ;
256+ app . quit ( ) ;
257+ return ;
258+ }
259+
260+ // 额外检查应用是否已在运行
261+ const alreadyRunning = await isAppAlreadyRunning ( ) ;
262+ if ( alreadyRunning ) {
263+ // 双重确认,如果检测到应用在运行,仍然退出
264+ dialog . showMessageBoxSync ( {
265+ type : 'info' ,
266+ title : 'WebSSH' ,
267+ message : '检测到 WebSSH 应用已在运行中,请勿重复启动。' ,
268+ detail : '如果您需要启动新窗口,请在已运行的实例中打开新标签页。'
269+ } ) ;
270+ app . quit ( ) ;
271+ return ;
272+ }
273+
199274 createWindow ( ) ;
200275} ) ;
201276
0 commit comments