mirror of
https://github.com/tiddly-gittly/TidGi-Desktop.git
synced 2026-03-07 06:20:50 -08:00
Add port utilities; use port checks in wiki
Introduce port helper functions and tests: add src/services/libs/port.ts with isPortAvailable and findAvailablePort (tries start port, +10, then incremental attempts) and corresponding Vitest tests in src/services/libs/__tests__/port.test.ts. Integrate pre-flight port availability checks into src/services/wiki/index.ts: import findAvailablePort, verify and choose an available port before starting a wiki, update workspace metadata and URLs when a different port is selected, and log/report failures. Add an emergency fallback on EADDRINUSE at runtime to attempt finding and applying an alternative port. Also adjust the worker import comment to use @ts-ignore for the Vite nodeWorker import.
This commit is contained in:
parent
c30a4cc05c
commit
fe0f0c9700
3 changed files with 159 additions and 11 deletions
40
src/services/libs/__tests__/port.test.ts
Normal file
40
src/services/libs/__tests__/port.test.ts
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
import { describe, expect, it } from 'vitest';
|
||||
import { findAvailablePort, isPortAvailable } from '../port';
|
||||
|
||||
describe('Port Utilities', () => {
|
||||
describe('isPortAvailable', () => {
|
||||
it('should return true for available high-numbered port', async () => {
|
||||
const available = await isPortAvailable(59999);
|
||||
expect(available).toBe(true);
|
||||
});
|
||||
|
||||
it('should handle occupied ports gracefully', async () => {
|
||||
const result = await isPortAvailable(80);
|
||||
expect(typeof result).toBe('boolean');
|
||||
});
|
||||
});
|
||||
|
||||
describe('findAvailablePort', () => {
|
||||
it('should return the same port if available', async () => {
|
||||
const testPort = 59998;
|
||||
const result = await findAvailablePort(testPort);
|
||||
expect(result).toBe(testPort);
|
||||
});
|
||||
|
||||
it('should find an available port when starting from a commonly used port', async () => {
|
||||
const testPort = 5212;
|
||||
const result = await findAvailablePort(testPort);
|
||||
expect(result).not.toBeNull();
|
||||
if (result !== null) {
|
||||
expect(result).toBeGreaterThanOrEqual(testPort);
|
||||
}
|
||||
});
|
||||
|
||||
it('should eventually find an available port', async () => {
|
||||
const testPort = 59997;
|
||||
const result = await findAvailablePort(testPort);
|
||||
expect(result).not.toBeNull();
|
||||
expect(typeof result).toBe('number');
|
||||
});
|
||||
});
|
||||
});
|
||||
62
src/services/libs/port.ts
Normal file
62
src/services/libs/port.ts
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
import { Server } from 'node:net';
|
||||
|
||||
/**
|
||||
* Check if a port is available on the local machine
|
||||
* Optimized for minimal overhead - closes server immediately after binding
|
||||
* @param port The port number to check
|
||||
* @returns true if port is available, false if in use
|
||||
*/
|
||||
export async function isPortAvailable(port: number): Promise<boolean> {
|
||||
return new Promise((resolve) => {
|
||||
const server = new Server();
|
||||
|
||||
server.once('error', (error: NodeJS.ErrnoException) => {
|
||||
if (error.code === 'EADDRINUSE') {
|
||||
resolve(false);
|
||||
} else {
|
||||
// Other errors treated as unavailable
|
||||
resolve(false);
|
||||
}
|
||||
});
|
||||
|
||||
server.once('listening', () => {
|
||||
// Close with callback to ensure cleanup before resolving
|
||||
server.close(() => {
|
||||
resolve(true);
|
||||
});
|
||||
});
|
||||
|
||||
// Bind to 0.0.0.0 to check all network interfaces
|
||||
server.listen(port, '0.0.0.0');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Find an available port starting from the given port
|
||||
* First tries port, then port+10, then keeps incrementing by 1
|
||||
* @param startPort The port to start checking from
|
||||
* @param maxAttempts Maximum number of attempts (default 100)
|
||||
* @returns An available port number, or null if none found
|
||||
*/
|
||||
export async function findAvailablePort(startPort: number, maxAttempts = 100): Promise<number | null> {
|
||||
// First try the original port
|
||||
if (await isPortAvailable(startPort)) {
|
||||
return startPort;
|
||||
}
|
||||
|
||||
// Then try +10
|
||||
const portPlusTen = startPort + 10;
|
||||
if (await isPortAvailable(portPlusTen)) {
|
||||
return portPlusTen;
|
||||
}
|
||||
|
||||
// Then increment by 1 from +10
|
||||
for (let attempt = 1; attempt < maxAttempts; attempt++) {
|
||||
const portToTry = portPlusTen + attempt;
|
||||
if (await isPortAvailable(portToTry)) {
|
||||
return portToTry;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
|
@ -1,3 +1,4 @@
|
|||
import { findAvailablePort } from '@services/libs/port';
|
||||
import { createWorkerProxy, terminateWorker } from '@services/libs/workerAdapter';
|
||||
import { dialog, shell } from 'electron';
|
||||
import { attachWorker } from 'electron-ipc-cat/server';
|
||||
|
|
@ -6,7 +7,8 @@ import { copy, exists, mkdir, mkdirs, pathExists, readdir, readFile } from 'fs-e
|
|||
import { inject, injectable } from 'inversify';
|
||||
import path from 'path';
|
||||
import { Worker } from 'worker_threads';
|
||||
// @ts-expect-error - Vite worker import with ?nodeWorker query
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore - Vite worker import with ?nodeWorker query
|
||||
import WikiWorkerFactory from './wikiWorker/index?nodeWorker';
|
||||
|
||||
import { container } from '@services/container';
|
||||
|
|
@ -118,7 +120,8 @@ export class Wiki implements IWikiService {
|
|||
logger.error('Try to start wiki, but workspace is not a wiki workspace', { workspace, workspaceID });
|
||||
return;
|
||||
}
|
||||
const { port, rootTiddler, readOnlyMode, tokenAuth, homeUrl, lastUrl, https, excludedPlugins, isSubWiki, wikiFolderLocation, name, enableHTTPAPI, authToken } = workspace;
|
||||
const { rootTiddler, readOnlyMode, tokenAuth, https, excludedPlugins, isSubWiki, wikiFolderLocation, name, enableHTTPAPI, authToken } = workspace;
|
||||
let { port, homeUrl, lastUrl } = workspace;
|
||||
logger.debug('startWiki: Got workspace from workspaceService', {
|
||||
workspaceID,
|
||||
name,
|
||||
|
|
@ -131,6 +134,39 @@ export class Wiki implements IWikiService {
|
|||
logger.error('Try to start wiki, but workspace is sub wiki', { workspace, workspaceID });
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if port is available before starting wiki
|
||||
const availablePort = await findAvailablePort(port);
|
||||
if (availablePort === null) {
|
||||
logger.error('Could not find available port for wiki', { workspaceID, requestedPort: port });
|
||||
await workspaceService.updateMetaData(workspaceID, {
|
||||
isLoading: false,
|
||||
didFailLoadErrorMessage: `Could not find available port starting from ${port}`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Update workspace settings if port changed
|
||||
if (availablePort !== port) {
|
||||
logger.info('Port conflict detected, using alternative port', {
|
||||
workspaceID,
|
||||
originalPort: port,
|
||||
newPort: availablePort,
|
||||
});
|
||||
|
||||
const portChange = {
|
||||
port: availablePort,
|
||||
homeUrl: homeUrl.replace(`:${port}`, `:${availablePort}`),
|
||||
lastUrl: lastUrl?.replace(`:${port}`, `:${availablePort}`) ?? null,
|
||||
};
|
||||
|
||||
await workspaceService.update(workspaceID, portChange, true);
|
||||
|
||||
// Update local variables with new port
|
||||
port = availablePort;
|
||||
homeUrl = portChange.homeUrl;
|
||||
lastUrl = portChange.lastUrl;
|
||||
}
|
||||
// wiki server is about to boot, but our webview is just start loading, wait for `view.webContents.on('did-stop-loading'` to set this to false
|
||||
await workspaceService.updateMetaData(workspaceID, { isLoading: true });
|
||||
if (tokenAuth && authToken) {
|
||||
|
|
@ -287,16 +323,26 @@ export class Wiki implements IWikiService {
|
|||
logger.info('Realigned view after plugin error', { workspaceID, function: 'startWiki' });
|
||||
}
|
||||
|
||||
// fix "message":"listen EADDRINUSE: address already in use 0.0.0.0:5212"
|
||||
// Port availability check should have prevented EADDRINUSE, but handle as fallback
|
||||
if (errorMessage.includes('EADDRINUSE')) {
|
||||
const portChange = {
|
||||
port: port + 1,
|
||||
homeUrl: homeUrl.replace(`:${port}`, `:${port + 1}`),
|
||||
|
||||
lastUrl: lastUrl?.replace(`:${port}`, `:${port + 1}`) ?? null,
|
||||
};
|
||||
await workspaceService.update(workspaceID, portChange, true);
|
||||
reject(new WikiRuntimeError(new Error(message.message), wikiFolderLocation, true, { ...workspace, ...portChange }));
|
||||
logger.warn('EADDRINUSE error despite pre-flight port check', {
|
||||
workspaceID,
|
||||
port,
|
||||
errorMessage,
|
||||
});
|
||||
// Try to find another available port as emergency fallback
|
||||
const emergencyPort = await findAvailablePort(port + 1);
|
||||
if (emergencyPort !== null) {
|
||||
const portChange = {
|
||||
port: emergencyPort,
|
||||
homeUrl: homeUrl.replace(`:${port}`, `:${emergencyPort}`),
|
||||
lastUrl: lastUrl?.replace(`:${port}`, `:${emergencyPort}`) ?? null,
|
||||
};
|
||||
await workspaceService.update(workspaceID, portChange, true);
|
||||
reject(new WikiRuntimeError(new Error(message.message), wikiFolderLocation, true, { ...workspace, ...portChange }));
|
||||
} else {
|
||||
reject(new WikiRuntimeError(new Error(message.message), wikiFolderLocation, false, { ...workspace }));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue