fix(anki,appimage): handle proxy port conflicts and stray GPU child

- AnkiConnect proxy now detects EADDRINUSE, logs a warning, and surfaces an overlay notification instead of crashing video startup
- Background AppImage bootstrap runs Chromium with in-process-gpu so no GPU child survives app.exit() and outlives the FUSE mount, fixing the remaining DrKonqi "Service Crash" case
- Add changelog fragment for the proxy fix; update the AppImage quit fragment with the GPU child root cause
This commit is contained in:
2026-07-17 23:04:56 -07:00
parent 2398f5c030
commit 44959ed282
9 changed files with 210 additions and 2 deletions
@@ -543,3 +543,41 @@ test('proxy detects self-referential loop configuration', () => {
assert.equal(result, true);
});
test('proxy continues without a local listener when its address is already bound', async () => {
const occupiedServer = http.createServer();
occupiedServer.listen(0, '127.0.0.1');
await once(occupiedServer, 'listening');
const occupiedAddress = occupiedServer.address();
assert.ok(occupiedAddress && typeof occupiedAddress === 'object');
const info: string[] = [];
const warnings: string[] = [];
const proxy = new AnkiConnectProxyServer({
shouldAutoUpdateNewCards: () => true,
processNewCard: async () => undefined,
logInfo: (message) => info.push(message),
logWarn: (message, ...args) => warnings.push([message, ...args].join(' ')),
logError: () => undefined,
});
try {
proxy.start({
host: '127.0.0.1',
port: occupiedAddress.port,
upstreamUrl: 'http://127.0.0.1:8765',
});
await proxy.waitUntilReady();
assert.equal(proxy.isRunning, false);
assert.deepEqual(warnings, [
`[anki-proxy] Local proxy unavailable because http://127.0.0.1:${occupiedAddress.port} is already in use; continuing without it. Change ankiConnect.proxy.port or stop the process using that address.`,
]);
proxy.stop();
assert.deepEqual(info, []);
} finally {
proxy.stop();
occupiedServer.close();
await once(occupiedServer, 'close');
}
});
@@ -27,6 +27,7 @@ export interface AnkiConnectProxyServerDeps {
logInfo: (message: string, ...args: unknown[]) => void;
logWarn: (message: string, ...args: unknown[]) => void;
logError: (message: string, ...args: unknown[]) => void;
notifyUnavailable?: (message: string) => void;
}
export class AnkiConnectProxyServer {
@@ -78,7 +79,23 @@ export class AnkiConnectProxyServer {
void this.handleRequest(req, res, options.upstreamUrl);
});
const server = this.server;
this.server.on('error', (error) => {
if ((error as NodeJS.ErrnoException).code === 'EADDRINUSE') {
this.resolveReady?.();
this.resolveReady = null;
this.rejectReady = null;
if (this.server === server) {
this.server = null;
}
this.deps.logWarn(
`[anki-proxy] Local proxy unavailable because http://${options.host}:${options.port} is already in use; continuing without it. Change ankiConnect.proxy.port or stop the process using that address.`,
);
this.deps.notifyUnavailable?.(
`AnkiConnect proxy unavailable because http://${options.host}:${options.port} is already in use. Change ankiConnect.proxy.port or stop the process using that address.`,
);
return;
}
this.rejectReady?.(error as Error);
this.resolveReady = null;
this.rejectReady = null;