mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-05-25 12:55:18 -07:00
fix texthooker gate, overlay fallback, and control server byte limit
- gate --texthooker flag on both CLI useTexthooker arg and plugin texthookerEnabled - remove erroneous return that blocked legacy app startup fallback after control command failure - fix open-config-settings to only skipRender when window actually opened - track raw byte count for accurate 64KB limit in app control server
This commit is contained in:
@@ -110,7 +110,7 @@ Browse sibling episode files and the active mpv queue in one overlay modal. Open
|
||||
|
||||
## Requirements
|
||||
|
||||
Only **mpv** and Anki+AnkiConnect is required. Everything else is optional but enhances the experience
|
||||
Only **mpv** and Anki+AnkiConnect are required. Everything else is optional but enhances the experience.
|
||||
|
||||
| Dependency | Status | What it does |
|
||||
| -------------------- | ----------- | ---------------------------------------- |
|
||||
|
||||
@@ -151,6 +151,7 @@ test('plugin auto-start playback leaves app lifetime to managed-playback owner',
|
||||
...context.args,
|
||||
target: '/tmp/movie.mkv',
|
||||
targetKind: 'file',
|
||||
useTexthooker: true,
|
||||
};
|
||||
context.pluginRuntimeConfig = {
|
||||
socketPath: '/tmp/subminer.sock',
|
||||
@@ -213,6 +214,7 @@ test('plugin auto-start playback attaches a warm background app through the laun
|
||||
...context.args,
|
||||
target: '/tmp/movie.mkv',
|
||||
targetKind: 'file',
|
||||
useTexthooker: true,
|
||||
};
|
||||
context.pluginRuntimeConfig = {
|
||||
socketPath: '/tmp/subminer.sock',
|
||||
@@ -268,3 +270,47 @@ test('plugin auto-start playback attaches a warm background app through the laun
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
test('plugin auto-start attach mode omits texthooker flag when CLI texthooker is disabled', async () => {
|
||||
const context = createContext();
|
||||
context.args = {
|
||||
...context.args,
|
||||
target: '/tmp/movie.mkv',
|
||||
targetKind: 'file',
|
||||
};
|
||||
context.pluginRuntimeConfig = {
|
||||
socketPath: '/tmp/subminer.sock',
|
||||
binaryPath: '',
|
||||
backend: 'auto',
|
||||
autoStart: true,
|
||||
autoStartVisibleOverlay: true,
|
||||
autoStartPauseUntilReady: true,
|
||||
texthookerEnabled: true,
|
||||
aniskipEnabled: true,
|
||||
aniskipButtonKey: 'TAB',
|
||||
};
|
||||
const calls: string[] = [];
|
||||
|
||||
await runPlaybackCommandWithDeps(context, {
|
||||
ensurePlaybackSetupReady: async () => {},
|
||||
chooseTarget: async () => ({ target: context.args.target, kind: 'file' }),
|
||||
checkDependencies: () => {},
|
||||
registerCleanup: () => {},
|
||||
startMpv: async () => {
|
||||
calls.push('startMpv');
|
||||
},
|
||||
waitForUnixSocketReady: async () => true,
|
||||
startOverlay: async (_appPath, _args, _socketPath, extraAppArgs = []) => {
|
||||
calls.push(`startOverlay:${extraAppArgs.join(' ')}`);
|
||||
},
|
||||
launchAppCommandDetached: () => {},
|
||||
log: () => {},
|
||||
cleanupPlaybackSession: async () => {},
|
||||
getMpvProc: () => null,
|
||||
isAppControlServerAvailable: async () => true,
|
||||
} as Parameters<typeof runPlaybackCommandWithDeps>[1] & {
|
||||
isAppControlServerAvailable: () => Promise<boolean>;
|
||||
});
|
||||
|
||||
assert.deepEqual(calls, ['startMpv', 'startOverlay:--show-visible-overlay']);
|
||||
});
|
||||
|
||||
@@ -282,7 +282,9 @@ export async function runPlaybackCommandWithDeps(
|
||||
pluginRuntimeConfig.autoStartVisibleOverlay
|
||||
? '--show-visible-overlay'
|
||||
: '--hide-visible-overlay',
|
||||
...(pluginRuntimeConfig.texthookerEnabled ? ['--texthooker'] : []),
|
||||
...(args.useTexthooker && effectivePluginRuntimeConfig.texthookerEnabled
|
||||
? ['--texthooker']
|
||||
: []),
|
||||
]
|
||||
: [];
|
||||
await deps.startOverlay(appPath, args, mpvSocketPath, extraAppArgs);
|
||||
|
||||
+57
-1
@@ -767,7 +767,10 @@ test('startOverlay attaches through the running app control socket without spawn
|
||||
let buffer = '';
|
||||
socket.on('data', (chunk) => {
|
||||
buffer += chunk.toString('utf8');
|
||||
const line = buffer.split(/\r?\n/, 1)[0];
|
||||
const newlineMatch = buffer.match(/\r?\n/);
|
||||
if (!newlineMatch || newlineMatch.index === undefined) return;
|
||||
const line = buffer.slice(0, newlineMatch.index).trim();
|
||||
buffer = buffer.slice(newlineMatch.index + newlineMatch[0].length);
|
||||
if (!line) return;
|
||||
const payload = JSON.parse(line) as { argv?: unknown };
|
||||
if (Array.isArray(payload.argv)) {
|
||||
@@ -823,6 +826,59 @@ test('startOverlay attaches through the running app control socket without spawn
|
||||
}
|
||||
});
|
||||
|
||||
test('startOverlay falls back to legacy app startup when control command fails', async () => {
|
||||
if (process.platform === 'win32') return;
|
||||
|
||||
const { dir, socketPath } = createTempSocketPath();
|
||||
const controlSocketPath = path.join(dir, 'control.sock');
|
||||
const appPath = path.join(dir, 'fake-subminer.sh');
|
||||
const appInvocationsPath = path.join(dir, 'app-invocations.log');
|
||||
const originalControlSocket = process.env.SUBMINER_APP_CONTROL_SOCKET;
|
||||
|
||||
fs.writeFileSync(
|
||||
appPath,
|
||||
[
|
||||
'#!/bin/sh',
|
||||
`printf '%s\\n' "$@" >> ${JSON.stringify(appInvocationsPath)}`,
|
||||
'if [ "$1" = "--app-ping" ]; then exit 0; fi',
|
||||
'exit 0',
|
||||
'',
|
||||
].join('\n'),
|
||||
);
|
||||
fs.chmodSync(appPath, 0o755);
|
||||
|
||||
const controlServer = net.createServer((socket) => {
|
||||
socket.on('data', () => {
|
||||
socket.end(JSON.stringify({ ok: false, error: 'boom' }) + '\n');
|
||||
});
|
||||
});
|
||||
|
||||
try {
|
||||
process.env.SUBMINER_APP_CONTROL_SOCKET = controlSocketPath;
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
controlServer.once('error', reject);
|
||||
controlServer.listen(controlSocketPath, resolve);
|
||||
});
|
||||
|
||||
await startOverlay(appPath, makeArgs(), socketPath);
|
||||
|
||||
const invocationText = fs.readFileSync(appInvocationsPath, 'utf8');
|
||||
assert.match(invocationText, /--app-ping/);
|
||||
assert.match(invocationText, /--start/);
|
||||
} finally {
|
||||
if (originalControlSocket === undefined) {
|
||||
delete process.env.SUBMINER_APP_CONTROL_SOCKET;
|
||||
} else {
|
||||
process.env.SUBMINER_APP_CONTROL_SOCKET = originalControlSocket;
|
||||
}
|
||||
await new Promise<void>((resolve) => controlServer.close(() => resolve()));
|
||||
state.overlayProc = null;
|
||||
state.overlayManagedByLauncher = false;
|
||||
state.appPath = '';
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test('startOverlay keeps lifecycle ownership for its already-managed app', async () => {
|
||||
const { dir, socketPath } = createTempSocketPath();
|
||||
const appPath = path.join(dir, 'fake-subminer.sh');
|
||||
|
||||
@@ -1058,7 +1058,6 @@ export async function startOverlay(
|
||||
clearOverlayManagedByLauncher();
|
||||
state.overlayProc = null;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const appAlreadyRunning = isAppAlreadyRunning(appPath, args.logLevel);
|
||||
|
||||
+6
-2
@@ -3077,10 +3077,14 @@ const openFirstRunSetupWindowHandler = createOpenFirstRunSetupWindowHandler({
|
||||
return;
|
||||
}
|
||||
if (submission.action === 'open-config-settings') {
|
||||
firstRunSetupMessage = openConfigSettingsWindow()
|
||||
const opened = openConfigSettingsWindow();
|
||||
firstRunSetupMessage = opened
|
||||
? 'Opened SubMiner settings.'
|
||||
: 'SubMiner settings are unavailable.';
|
||||
return { skipRender: true };
|
||||
if (opened) {
|
||||
return { skipRender: true };
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (submission.action === 'refresh') {
|
||||
const snapshot = await firstRunSetupService.refreshStatus('Status refreshed.');
|
||||
|
||||
@@ -45,3 +45,31 @@ test('app control server dispatches argv requests and replies ok', async () => {
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test('app control server rejects requests larger than 64KB by UTF-8 byte length', async () => {
|
||||
if (process.platform === 'win32') return;
|
||||
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'subminer-control-test-'));
|
||||
const socketPath = path.join(dir, 'control.sock');
|
||||
const received: string[][] = [];
|
||||
const server = startAppControlServer({
|
||||
socketPath,
|
||||
platform: 'linux',
|
||||
handleArgv: (argv) => {
|
||||
received.push(argv);
|
||||
},
|
||||
});
|
||||
|
||||
try {
|
||||
await waitForSocketPath(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, []);
|
||||
} finally {
|
||||
server.close();
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
@@ -44,12 +44,14 @@ export function startAppControlServer(options: AppControlServerOptions): AppCont
|
||||
|
||||
const server = net.createServer((socket) => {
|
||||
let buffer = '';
|
||||
let byteCount = 0;
|
||||
let handled = false;
|
||||
|
||||
socket.on('data', (chunk) => {
|
||||
if (handled) return;
|
||||
byteCount += chunk.length;
|
||||
buffer += chunk.toString('utf8');
|
||||
if (buffer.length > 65536) {
|
||||
if (byteCount > 65536) {
|
||||
handled = true;
|
||||
writeResponse(socket, { ok: false, error: 'App control request too large' });
|
||||
return;
|
||||
|
||||
Reference in New Issue
Block a user