mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-05-27 00:55:16 -07:00
thread launcher config dir through app control and overlay calls
- startOverlay and isRunningAppControlServerAvailable accept explicit configDir to avoid re-resolving from env mid-flight - emit connection-change on reconnect when previously connected - handle errored client sockets in app control server with logWarn and destroy
This commit is contained in:
@@ -171,7 +171,11 @@ test('MpvIpcClient connect logs connect-request at debug level', () => {
|
||||
test('MpvIpcClient reconnect clears stale connected state and starts a fresh transport connect', () => {
|
||||
const client = new MpvIpcClient('/tmp/mpv.sock', makeDeps());
|
||||
const calls: string[] = [];
|
||||
const connectionChanges: boolean[] = [];
|
||||
const resolved: unknown[] = [];
|
||||
client.on('connection-change', ({ connected }) => {
|
||||
connectionChanges.push(connected);
|
||||
});
|
||||
(client as any).connected = true;
|
||||
(client as any).connecting = false;
|
||||
(client as any).socket = {};
|
||||
@@ -191,6 +195,7 @@ test('MpvIpcClient reconnect clears stale connected state and starts a fresh tra
|
||||
assert.equal(client.connected, false);
|
||||
assert.equal((client as any).connecting, true);
|
||||
assert.equal((client as any).socket, null);
|
||||
assert.deepEqual(connectionChanges, [false]);
|
||||
assert.deepEqual(resolved, [{ request_id: 10, error: 'disconnected' }]);
|
||||
});
|
||||
|
||||
|
||||
@@ -277,11 +277,15 @@ export class MpvIpcClient implements MpvClient {
|
||||
|
||||
reconnect(): void {
|
||||
logger.debug('MPV IPC reconnect requested.');
|
||||
const wasConnected = this.connected;
|
||||
this.transport.shutdown();
|
||||
this.connected = false;
|
||||
this.connecting = false;
|
||||
this.socket = null;
|
||||
this.playbackPaused = null;
|
||||
if (wasConnected) {
|
||||
this.emit('connection-change', { connected: false });
|
||||
}
|
||||
this.failPendingRequests();
|
||||
this.connect();
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { EventEmitter } from 'node:events';
|
||||
import fs from 'node:fs';
|
||||
import net from 'node:net';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
import test from 'node:test';
|
||||
@@ -13,9 +15,7 @@ async function waitForSocketPath(socketPath: string): Promise<void> {
|
||||
if (fs.existsSync(socketPath)) return;
|
||||
await new Promise<void>((resolve) => setTimeout(resolve, 10));
|
||||
}
|
||||
throw new Error(
|
||||
`Timed out waiting for control socket ${socketPath} after ${timeoutMs}ms`,
|
||||
);
|
||||
throw new Error(`Timed out waiting for control socket ${socketPath} after ${timeoutMs}ms`);
|
||||
}
|
||||
|
||||
test('app control server dispatches argv requests and replies ok', async () => {
|
||||
@@ -62,9 +62,12 @@ test('app control server rejects requests larger than 64KB by UTF-8 byte length'
|
||||
|
||||
try {
|
||||
await waitForSocketPath(socketPath);
|
||||
const result = await sendAppControlCommand(Array.from({ length: 4 }, () => 'あ'.repeat(6000)), {
|
||||
socketPath,
|
||||
});
|
||||
const result = await sendAppControlCommand(
|
||||
Array.from({ length: 4 }, () => 'あ'.repeat(6000)),
|
||||
{
|
||||
socketPath,
|
||||
},
|
||||
);
|
||||
|
||||
assert.deepEqual(result, { ok: false, error: 'App control request too large' });
|
||||
assert.deepEqual(received, []);
|
||||
@@ -73,3 +76,56 @@ test('app control server rejects requests larger than 64KB by UTF-8 byte length'
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test('app control server logs and closes errored client sockets', () => {
|
||||
const originalCreateServer = net.createServer;
|
||||
let socketHandler: ((socket: net.Socket) => void) | null = null;
|
||||
const fakeServer = new EventEmitter() as net.Server;
|
||||
fakeServer.listen = (() => fakeServer) as net.Server['listen'];
|
||||
fakeServer.close = ((callback?: (err?: Error) => void) => {
|
||||
callback?.();
|
||||
return fakeServer;
|
||||
}) as net.Server['close'];
|
||||
const received: string[][] = [];
|
||||
const warnings: Array<{ message: string; error?: unknown }> = [];
|
||||
|
||||
try {
|
||||
net.createServer = ((handler?: (socket: net.Socket) => void) => {
|
||||
socketHandler = handler ?? null;
|
||||
return fakeServer;
|
||||
}) as typeof net.createServer;
|
||||
|
||||
const server = startAppControlServer({
|
||||
socketPath: '\\\\.\\pipe\\subminer-test-control',
|
||||
platform: 'win32',
|
||||
handleArgv: (argv) => {
|
||||
received.push(argv);
|
||||
},
|
||||
logWarn: (message, error) => {
|
||||
warnings.push({ message, error });
|
||||
},
|
||||
});
|
||||
|
||||
const error = new Error('client reset');
|
||||
let destroyed = false;
|
||||
const socket = new EventEmitter() as net.Socket;
|
||||
socket.destroy = (() => {
|
||||
destroyed = true;
|
||||
return socket;
|
||||
}) as net.Socket['destroy'];
|
||||
|
||||
const handler = socketHandler as ((socket: net.Socket) => void) | null;
|
||||
assert.ok(handler);
|
||||
handler(socket);
|
||||
socket.emit('error', error);
|
||||
socket.emit('data', Buffer.from('{"argv":["--start"]}\n'));
|
||||
|
||||
assert.equal(destroyed, true);
|
||||
assert.deepEqual(received, []);
|
||||
assert.deepEqual(warnings, [{ message: 'App control client socket error.', error }]);
|
||||
|
||||
server.close();
|
||||
} finally {
|
||||
net.createServer = originalCreateServer;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -47,6 +47,13 @@ export function startAppControlServer(options: AppControlServerOptions): AppCont
|
||||
let byteCount = 0;
|
||||
let handled = false;
|
||||
|
||||
socket.on('error', (error) => {
|
||||
if (handled) return;
|
||||
handled = true;
|
||||
options.logWarn?.('App control client socket error.', error);
|
||||
socket.destroy();
|
||||
});
|
||||
|
||||
socket.on('data', (chunk) => {
|
||||
if (handled) return;
|
||||
byteCount += chunk.length;
|
||||
|
||||
Reference in New Issue
Block a user