mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-05-26 00:55:16 -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
|
## 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 |
|
| Dependency | Status | What it does |
|
||||||
| -------------------- | ----------- | ---------------------------------------- |
|
| -------------------- | ----------- | ---------------------------------------- |
|
||||||
|
|||||||
@@ -151,6 +151,7 @@ test('plugin auto-start playback leaves app lifetime to managed-playback owner',
|
|||||||
...context.args,
|
...context.args,
|
||||||
target: '/tmp/movie.mkv',
|
target: '/tmp/movie.mkv',
|
||||||
targetKind: 'file',
|
targetKind: 'file',
|
||||||
|
useTexthooker: true,
|
||||||
};
|
};
|
||||||
context.pluginRuntimeConfig = {
|
context.pluginRuntimeConfig = {
|
||||||
socketPath: '/tmp/subminer.sock',
|
socketPath: '/tmp/subminer.sock',
|
||||||
@@ -213,6 +214,7 @@ test('plugin auto-start playback attaches a warm background app through the laun
|
|||||||
...context.args,
|
...context.args,
|
||||||
target: '/tmp/movie.mkv',
|
target: '/tmp/movie.mkv',
|
||||||
targetKind: 'file',
|
targetKind: 'file',
|
||||||
|
useTexthooker: true,
|
||||||
};
|
};
|
||||||
context.pluginRuntimeConfig = {
|
context.pluginRuntimeConfig = {
|
||||||
socketPath: '/tmp/subminer.sock',
|
socketPath: '/tmp/subminer.sock',
|
||||||
@@ -268,3 +270,47 @@ test('plugin auto-start playback attaches a warm background app through the laun
|
|||||||
false,
|
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
|
pluginRuntimeConfig.autoStartVisibleOverlay
|
||||||
? '--show-visible-overlay'
|
? '--show-visible-overlay'
|
||||||
: '--hide-visible-overlay',
|
: '--hide-visible-overlay',
|
||||||
...(pluginRuntimeConfig.texthookerEnabled ? ['--texthooker'] : []),
|
...(args.useTexthooker && effectivePluginRuntimeConfig.texthookerEnabled
|
||||||
|
? ['--texthooker']
|
||||||
|
: []),
|
||||||
]
|
]
|
||||||
: [];
|
: [];
|
||||||
await deps.startOverlay(appPath, args, mpvSocketPath, extraAppArgs);
|
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 = '';
|
let buffer = '';
|
||||||
socket.on('data', (chunk) => {
|
socket.on('data', (chunk) => {
|
||||||
buffer += chunk.toString('utf8');
|
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;
|
if (!line) return;
|
||||||
const payload = JSON.parse(line) as { argv?: unknown };
|
const payload = JSON.parse(line) as { argv?: unknown };
|
||||||
if (Array.isArray(payload.argv)) {
|
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 () => {
|
test('startOverlay keeps lifecycle ownership for its already-managed app', async () => {
|
||||||
const { dir, socketPath } = createTempSocketPath();
|
const { dir, socketPath } = createTempSocketPath();
|
||||||
const appPath = path.join(dir, 'fake-subminer.sh');
|
const appPath = path.join(dir, 'fake-subminer.sh');
|
||||||
|
|||||||
@@ -1058,7 +1058,6 @@ export async function startOverlay(
|
|||||||
clearOverlayManagedByLauncher();
|
clearOverlayManagedByLauncher();
|
||||||
state.overlayProc = null;
|
state.overlayProc = null;
|
||||||
}
|
}
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const appAlreadyRunning = isAppAlreadyRunning(appPath, args.logLevel);
|
const appAlreadyRunning = isAppAlreadyRunning(appPath, args.logLevel);
|
||||||
|
|||||||
+5
-1
@@ -3077,11 +3077,15 @@ const openFirstRunSetupWindowHandler = createOpenFirstRunSetupWindowHandler({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (submission.action === 'open-config-settings') {
|
if (submission.action === 'open-config-settings') {
|
||||||
firstRunSetupMessage = openConfigSettingsWindow()
|
const opened = openConfigSettingsWindow();
|
||||||
|
firstRunSetupMessage = opened
|
||||||
? 'Opened SubMiner settings.'
|
? 'Opened SubMiner settings.'
|
||||||
: 'SubMiner settings are unavailable.';
|
: 'SubMiner settings are unavailable.';
|
||||||
|
if (opened) {
|
||||||
return { skipRender: true };
|
return { skipRender: true };
|
||||||
}
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (submission.action === 'refresh') {
|
if (submission.action === 'refresh') {
|
||||||
const snapshot = await firstRunSetupService.refreshStatus('Status refreshed.');
|
const snapshot = await firstRunSetupService.refreshStatus('Status refreshed.');
|
||||||
firstRunSetupMessage = snapshot.message;
|
firstRunSetupMessage = snapshot.message;
|
||||||
|
|||||||
@@ -45,3 +45,31 @@ test('app control server dispatches argv requests and replies ok', async () => {
|
|||||||
fs.rmSync(dir, { recursive: true, force: true });
|
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) => {
|
const server = net.createServer((socket) => {
|
||||||
let buffer = '';
|
let buffer = '';
|
||||||
|
let byteCount = 0;
|
||||||
let handled = false;
|
let handled = false;
|
||||||
|
|
||||||
socket.on('data', (chunk) => {
|
socket.on('data', (chunk) => {
|
||||||
if (handled) return;
|
if (handled) return;
|
||||||
|
byteCount += chunk.length;
|
||||||
buffer += chunk.toString('utf8');
|
buffer += chunk.toString('utf8');
|
||||||
if (buffer.length > 65536) {
|
if (byteCount > 65536) {
|
||||||
handled = true;
|
handled = true;
|
||||||
writeResponse(socket, { ok: false, error: 'App control request too large' });
|
writeResponse(socket, { ok: false, error: 'App control request too large' });
|
||||||
return;
|
return;
|
||||||
|
|||||||
Reference in New Issue
Block a user