Fall back to next port on EADDRINUSE; show dialog and update tray URL
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+13
-2
@@ -1,4 +1,4 @@
|
|||||||
const { app, BrowserWindow, ipcMain } = require('electron');
|
const { app, BrowserWindow, ipcMain, dialog } = require('electron');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const { startServer } = require('./server');
|
const { startServer } = require('./server');
|
||||||
const { createTray } = require('./tray');
|
const { createTray } = require('./tray');
|
||||||
@@ -40,6 +40,17 @@ async function createWindow() {
|
|||||||
app.whenReady().then(async () => {
|
app.whenReady().then(async () => {
|
||||||
serverHandle = await startServer({ port: PORT, overlayDir: OVERLAY_DIR });
|
serverHandle = await startServer({ port: PORT, overlayDir: OVERLAY_DIR });
|
||||||
|
|
||||||
|
if (serverHandle.port !== PORT) {
|
||||||
|
const actual = serverHandle.port;
|
||||||
|
await dialog.showMessageBox({
|
||||||
|
type: 'info',
|
||||||
|
title: 'Port in use',
|
||||||
|
message: `Port ${PORT} was in use; using ${actual} instead.`,
|
||||||
|
detail: `If you have an OBS browser source pointed at this app, update it to http://localhost:${actual}/`,
|
||||||
|
buttons: ['OK'],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
ipcMain.on('wtm:state', (_event, payload) => {
|
ipcMain.on('wtm:state', (_event, payload) => {
|
||||||
const next = serverHandle.setState(payload || { isPlaying: false });
|
const next = serverHandle.setState(payload || { isPlaying: false });
|
||||||
if (tray) tray.update({ title: next.title, artist: next.artist });
|
if (tray) tray.update({ title: next.title, artist: next.artist });
|
||||||
@@ -48,7 +59,7 @@ app.whenReady().then(async () => {
|
|||||||
await createWindow();
|
await createWindow();
|
||||||
|
|
||||||
tray = createTray({
|
tray = createTray({
|
||||||
port: PORT,
|
port: serverHandle.port,
|
||||||
onShow: () => {
|
onShow: () => {
|
||||||
if (!mainWindow) return;
|
if (!mainWindow) return;
|
||||||
if (mainWindow.isMinimized()) mainWindow.restore();
|
if (mainWindow.isMinimized()) mainWindow.restore();
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ const { initialState, mergeState } = require('./state');
|
|||||||
* @param {string} [opts.overlayDir] Absolute path to the overlay/ directory to serve as static files. Optional (omit for tests).
|
* @param {string} [opts.overlayDir] Absolute path to the overlay/ directory to serve as static files. Optional (omit for tests).
|
||||||
* @returns {Promise<{ setState: (update: object) => object, stop: () => Promise<void> }>}
|
* @returns {Promise<{ setState: (update: object) => object, stop: () => Promise<void> }>}
|
||||||
*/
|
*/
|
||||||
function startServer({ port, overlayDir }) {
|
async function startServer({ port, overlayDir }) {
|
||||||
let currentState = { ...initialState };
|
let currentState = { ...initialState };
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
@@ -82,13 +82,49 @@ function startServer({ port, overlayDir }) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
// Try the preferred port first, then up to 4 sequential fallbacks on EADDRINUSE.
|
||||||
server.listen(port, '127.0.0.1', () => {
|
const MAX_PORT_ATTEMPTS = 5;
|
||||||
console.log(`[wtm] server listening on http://127.0.0.1:${port}`);
|
let actualPort = null;
|
||||||
resolve({ setState, stop });
|
let lastErr = null;
|
||||||
});
|
|
||||||
server.on('error', reject);
|
// The WebSocketServer re-emits server 'error' events on itself.
|
||||||
|
// Add a no-op sink so Node doesn't crash on unhandled 'error' during port scanning.
|
||||||
|
const wssErrorSink = () => {};
|
||||||
|
wss.on('error', wssErrorSink);
|
||||||
|
|
||||||
|
for (let i = 0; i < MAX_PORT_ATTEMPTS; i++) {
|
||||||
|
const tryPort = port + i;
|
||||||
|
try {
|
||||||
|
await new Promise((resolve, reject) => {
|
||||||
|
const onError = (err) => {
|
||||||
|
server.removeListener('listening', onListening);
|
||||||
|
reject(err);
|
||||||
|
};
|
||||||
|
const onListening = () => {
|
||||||
|
server.removeListener('error', onError);
|
||||||
|
resolve();
|
||||||
|
};
|
||||||
|
server.once('error', onError);
|
||||||
|
server.once('listening', onListening);
|
||||||
|
server.listen(tryPort, '127.0.0.1');
|
||||||
});
|
});
|
||||||
|
actualPort = tryPort;
|
||||||
|
break;
|
||||||
|
} catch (err) {
|
||||||
|
lastErr = err;
|
||||||
|
if (err.code !== 'EADDRINUSE') throw err;
|
||||||
|
// EADDRINUSE — fall through and try the next port.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
wss.removeListener('error', wssErrorSink);
|
||||||
|
|
||||||
|
if (actualPort === null) {
|
||||||
|
throw lastErr || new Error(`No free port in range ${port}..${port + MAX_PORT_ATTEMPTS - 1}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`[wtm] server listening on http://127.0.0.1:${actualPort}`);
|
||||||
|
return { setState, stop, port: actualPort };
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { startServer };
|
module.exports = { startServer };
|
||||||
|
|||||||
Reference in New Issue
Block a user