fix: keep config dashboard alive when interface detection fails

This commit is contained in:
Tam Nhu Tran
2026-04-28 11:39:40 -04:00
parent 0867378644
commit 119aa3be98
3 changed files with 58 additions and 6 deletions
+16 -3
View File
@@ -45,7 +45,7 @@ export function normalizeDashboardHost(host: string | undefined): string | undef
export function resolveDashboardUrls(
host: string | undefined,
port: number,
networkInterfaces: NetworkInterfacesMap = os.networkInterfaces()
networkInterfaces?: NetworkInterfacesMap
): DashboardUrls {
const bindHost = normalizeDashboardHost(host);
if (!bindHost) {
@@ -58,7 +58,7 @@ export function resolveDashboardUrls(
return {
bindHost,
browserUrl: `http://localhost:${port}`,
networkUrls: getExternalIpv4Urls(port, networkInterfaces),
networkUrls: getExternalIpv4Urls(port, networkInterfaces ?? getNetworkInterfacesSafe()),
};
}
@@ -70,8 +70,12 @@ export function resolveDashboardUrls(
function getExternalIpv4Urls(
port: number,
networkInterfaces: NetworkInterfacesMap
networkInterfaces: NetworkInterfacesMap | undefined
): string[] | undefined {
if (!networkInterfaces) {
return undefined;
}
const urls: string[] = [];
const seen = new Set<string>();
@@ -97,6 +101,15 @@ function getExternalIpv4Urls(
return urls.length > 0 ? urls : undefined;
}
function getNetworkInterfacesSafe(): NetworkInterfacesMap | undefined {
try {
// Some constrained environments (for example proot) throw here; keep the local dashboard URL.
return os.networkInterfaces();
} catch {
return undefined;
}
}
function formatHostForUrl(host: string): string {
if (host.includes(':') && !host.startsWith('[') && !host.endsWith(']')) {
return `[${host}]`;
+24 -2
View File
@@ -1,4 +1,5 @@
import { afterEach, beforeEach, describe, expect, it } from 'bun:test';
import { afterEach, beforeEach, describe, expect, it, spyOn } from 'bun:test';
import * as os from 'os';
import { handleConfigCommand } from '../../../src/commands/config-command';
import { resolveNamedCommand } from '../../../src/commands/named-command-router';
@@ -104,7 +105,9 @@ describe('config command dashboard startup', () => {
throw new Error(`process.exit(${code ?? 0})`);
}) as typeof process.exit;
await expect(handleConfigCommand(['help'], createTestDeps())).rejects.toThrow('process.exit(0)');
await expect(handleConfigCommand(['help'], createTestDeps())).rejects.toThrow(
'process.exit(0)'
);
expect(startServerCalls).toHaveLength(0);
expect(logLines.join('\n')).toContain('Usage: ccs config [command] [options]');
@@ -191,6 +194,25 @@ describe('config command dashboard startup', () => {
expect(rendered).toContain('Dashboard will work but Control Panel/Stats may be limited');
});
it('still opens the dashboard when wildcard network detection is unavailable', async () => {
const networkInterfacesSpy = spyOn(os, 'networkInterfaces').mockImplementation(() => {
throw new Error('uv_interface_addresses returned Unknown system error 13');
});
try {
await handleConfigCommand([], createTestDeps());
expect(startServerCalls).toHaveLength(1);
const rendered = logLines.join('\n');
expect(rendered).toContain('Dashboard: http://localhost:3000');
expect(rendered).toContain('Bind host: ::');
expect(errorLines).toHaveLength(0);
} finally {
networkInterfacesSpy.mockRestore();
}
});
it('fails cleanly when the server cannot bind the requested host', async () => {
startServerError = new Error(
'Unable to bind 192.0.2.123:4100; the address may be unavailable or the port may already be in use'
@@ -1,4 +1,5 @@
import { describe, expect, it } from 'bun:test';
import { describe, expect, it, spyOn } from 'bun:test';
import * as os from 'os';
import {
isLoopbackHost,
@@ -66,4 +67,20 @@ describe('config dashboard host helpers', () => {
expect(urls.bindHost).toBe('::1');
expect(urls.browserUrl).toBe('http://[::1]:3000');
});
it('falls back to localhost-only URLs when interface enumeration fails', () => {
const networkInterfacesSpy = spyOn(os, 'networkInterfaces').mockImplementation(() => {
throw new Error('uv_interface_addresses returned Unknown system error 13');
});
try {
const urls = resolveDashboardUrls('::', 3000);
expect(urls.bindHost).toBe('::');
expect(urls.browserUrl).toBe('http://localhost:3000');
expect(urls.networkUrls).toBeUndefined();
} finally {
networkInterfacesSpy.mockRestore();
}
});
});