mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-07-27 16:49:51 -07:00
fix(sync): settle launcher client only after terminal NDJSON on exit
- `runSyncLauncher`: defer exit settlement until result event parsed; handle exit-before-stdout race - AppImage sync commands now run in Node-only mode (`ELECTRON_RUN_AS_NODE=1 -e ...`) so remote sync needs no display server - Update docs and changelog to reflect both changes
This commit is contained in:
@@ -89,7 +89,7 @@ test('runSyncLauncher falls back to stderr when no result event arrives', async
|
||||
assert.match(result.error ?? '', /boom/);
|
||||
});
|
||||
|
||||
test('runSyncLauncher settles when the child exits before its stdio pipes close', async () => {
|
||||
test('runSyncLauncher settles on exit when the terminal event is already parsed', async () => {
|
||||
const { spawn, children } = makeSpawn();
|
||||
const handle = runSyncLauncher({
|
||||
command: ['subminer'],
|
||||
@@ -97,6 +97,10 @@ test('runSyncLauncher settles when the child exits before its stdio pipes close'
|
||||
onEvent: () => {},
|
||||
spawn,
|
||||
});
|
||||
children[0]!.stdout.emit(
|
||||
'data',
|
||||
Buffer.from('{"type":"result","ok":true,"error":null}\n'),
|
||||
);
|
||||
children[0]!.emit('exit', 0, null);
|
||||
|
||||
const result = await Promise.race([
|
||||
@@ -106,6 +110,31 @@ test('runSyncLauncher settles when the child exits before its stdio pipes close'
|
||||
assert.deepEqual(result, { ok: true, error: null });
|
||||
});
|
||||
|
||||
test('runSyncLauncher waits for terminal NDJSON when exit precedes final stdout data', async () => {
|
||||
const { spawn, children } = makeSpawn();
|
||||
const events: SyncProgressEvent[] = [];
|
||||
const handle = runSyncLauncher({
|
||||
command: ['subminer'],
|
||||
args: ['sync', 'media-box', '--json'],
|
||||
onEvent: (event) => events.push(event),
|
||||
spawn,
|
||||
});
|
||||
const child = children[0]!;
|
||||
let settled = false;
|
||||
void handle.done.then(() => {
|
||||
settled = true;
|
||||
});
|
||||
|
||||
child.emit('exit', 0, null);
|
||||
await Promise.resolve();
|
||||
assert.equal(settled, false);
|
||||
|
||||
child.stdout.emit('data', Buffer.from('{"type":"result","ok":true,"error":null}\n'));
|
||||
|
||||
assert.deepEqual(await handle.done, { ok: true, error: null });
|
||||
assert.deepEqual(events.at(-1), { type: 'result', ok: true, error: null });
|
||||
});
|
||||
|
||||
test('runSyncLauncher cancel kills the child and resolves as cancelled', async () => {
|
||||
const { spawn, children } = makeSpawn();
|
||||
const handle = runSyncLauncher({
|
||||
|
||||
@@ -73,6 +73,7 @@ export function runSyncLauncher(options: {
|
||||
let stderrTail = '';
|
||||
let resultEvent: Extract<SyncProgressEvent, { type: 'result' }> | null = null;
|
||||
let terminationError: string | null = null;
|
||||
let settleAfterTerminalEvent: (() => void) | null = null;
|
||||
|
||||
child.stdout?.on('data', (chunk) => {
|
||||
stdoutBuffer += chunk.toString();
|
||||
@@ -82,7 +83,10 @@ export function runSyncLauncher(options: {
|
||||
stdoutBuffer = stdoutBuffer.slice(newlineIndex + 1);
|
||||
const event = parseSyncProgressLine(line);
|
||||
if (event) {
|
||||
if (event.type === 'result') resultEvent = event;
|
||||
if (event.type === 'result') {
|
||||
resultEvent = event;
|
||||
settleAfterTerminalEvent?.();
|
||||
}
|
||||
options.onEvent(event);
|
||||
}
|
||||
newlineIndex = stdoutBuffer.indexOf('\n');
|
||||
@@ -105,6 +109,8 @@ export function runSyncLauncher(options: {
|
||||
|
||||
const done = new Promise<SyncLauncherRunResult>((resolve) => {
|
||||
let settled = false;
|
||||
let exitObserved = false;
|
||||
let exitCode: number | null = null;
|
||||
const settle = (result: SyncLauncherRunResult): void => {
|
||||
if (settled) return;
|
||||
settled = true;
|
||||
@@ -127,10 +133,17 @@ export function runSyncLauncher(options: {
|
||||
resultEvent?.error ?? (stderrTail.trim() || `Launcher exited with code ${code ?? 'null'}.`);
|
||||
settle({ ok: false, error });
|
||||
};
|
||||
settleAfterTerminalEvent = () => {
|
||||
if (exitObserved) settleFromExit(exitCode);
|
||||
};
|
||||
// Electron descendants can retain inherited stdio pipes after the main
|
||||
// child exits, delaying `close` indefinitely. `exit` is authoritative for
|
||||
// process completion; keep `close` as a fallback for test/process shims.
|
||||
child.on('exit', settleFromExit);
|
||||
// child exits, delaying `close` indefinitely. Once terminal NDJSON has
|
||||
// arrived, `exit` is authoritative; keep `close` as the fallback.
|
||||
child.on('exit', (code) => {
|
||||
exitObserved = true;
|
||||
exitCode = code;
|
||||
if (resultEvent) settleFromExit(code);
|
||||
});
|
||||
child.on('close', settleFromExit);
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user