mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-05-26 00:55:16 -07:00
Launch macOS app background-detached when no args passed
- Add `launchAppBackgroundDetached` that spawns with `--start --background` and `SUBMINER_BACKGROUND_CHILD=1` - On darwin with empty appArgs, use detached background launch instead of inherited process - Add `extraEnv` param to `launchAppCommandDetached` for env injection - Inject deps into `runAppPassthroughCommand` for testability - Bump vendor/subminer-yomitan submodule
This commit is contained in:
@@ -1,19 +1,45 @@
|
||||
import { launchTexthookerOnly, runAppCommandWithInherit } from '../mpv.js';
|
||||
import {
|
||||
launchAppBackgroundDetached,
|
||||
launchTexthookerOnly,
|
||||
runAppCommandWithInherit,
|
||||
} from '../mpv.js';
|
||||
import type { LauncherCommandContext } from './context.js';
|
||||
|
||||
export function runAppPassthroughCommand(context: LauncherCommandContext): boolean {
|
||||
type AppCommandDeps = {
|
||||
platform: () => NodeJS.Platform;
|
||||
runAppCommandWithInherit: (appPath: string, appArgs: string[]) => void;
|
||||
launchAppBackgroundDetached: (
|
||||
appPath: string,
|
||||
logLevel: LauncherCommandContext['args']['logLevel'],
|
||||
) => void;
|
||||
};
|
||||
|
||||
const defaultAppCommandDeps: AppCommandDeps = {
|
||||
platform: () => process.platform,
|
||||
runAppCommandWithInherit,
|
||||
launchAppBackgroundDetached,
|
||||
};
|
||||
|
||||
export function runAppPassthroughCommand(
|
||||
context: LauncherCommandContext,
|
||||
deps: AppCommandDeps = defaultAppCommandDeps,
|
||||
): boolean {
|
||||
const { args, appPath } = context;
|
||||
if (!appPath) {
|
||||
return false;
|
||||
}
|
||||
if (args.settings) {
|
||||
runAppCommandWithInherit(appPath, ['--settings']);
|
||||
deps.runAppCommandWithInherit(appPath, ['--settings']);
|
||||
return true;
|
||||
}
|
||||
if (!args.appPassthrough) {
|
||||
return false;
|
||||
}
|
||||
runAppCommandWithInherit(appPath, args.appArgs);
|
||||
if (deps.platform() === 'darwin' && args.appArgs.length === 0) {
|
||||
deps.launchAppBackgroundDetached(appPath, args.logLevel);
|
||||
return true;
|
||||
}
|
||||
deps.runAppCommandWithInherit(appPath, args.appArgs);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import { runConfigCommand } from './config-command.js';
|
||||
import { runDictionaryCommand } from './dictionary-command.js';
|
||||
import { runDoctorCommand } from './doctor-command.js';
|
||||
import { runMpvPreAppCommand } from './mpv-command.js';
|
||||
import { runAppPassthroughCommand } from './app-command.js';
|
||||
import { runStatsCommand } from './stats-command.js';
|
||||
import { runUpdateCommand } from './update-command.js';
|
||||
|
||||
@@ -168,6 +169,48 @@ test('doctor command forwards refresh-known-words to app binary', () => {
|
||||
assert.deepEqual(forwarded, [['--refresh-known-words']]);
|
||||
});
|
||||
|
||||
test('app command starts default macOS background app detached from launcher', () => {
|
||||
const context = createContext();
|
||||
context.args.appPassthrough = true;
|
||||
context.args.appArgs = [];
|
||||
const calls: string[] = [];
|
||||
|
||||
const handled = runAppPassthroughCommand(context, {
|
||||
platform: () => 'darwin',
|
||||
runAppCommandWithInherit: () => {
|
||||
calls.push('attached');
|
||||
},
|
||||
launchAppBackgroundDetached: (appPath, logLevel) => {
|
||||
calls.push(`detached:${appPath}:${logLevel}`);
|
||||
},
|
||||
});
|
||||
|
||||
assert.equal(handled, true);
|
||||
assert.deepEqual(calls, ['detached:/tmp/subminer.app:info']);
|
||||
});
|
||||
|
||||
test('app command keeps explicit passthrough args attached', () => {
|
||||
const context = createContext();
|
||||
context.args.appPassthrough = true;
|
||||
context.args.appArgs = ['--settings'];
|
||||
const forwarded: string[][] = [];
|
||||
const detached: string[] = [];
|
||||
|
||||
const handled = runAppPassthroughCommand(context, {
|
||||
platform: () => 'darwin',
|
||||
runAppCommandWithInherit: (_appPath, appArgs) => {
|
||||
forwarded.push(appArgs);
|
||||
},
|
||||
launchAppBackgroundDetached: () => {
|
||||
detached.push('detached');
|
||||
},
|
||||
});
|
||||
|
||||
assert.equal(handled, true);
|
||||
assert.deepEqual(forwarded, [['--settings']]);
|
||||
assert.deepEqual(detached, []);
|
||||
});
|
||||
|
||||
test('mpv pre-app command exits non-zero when socket is not ready', async () => {
|
||||
const context = createContext();
|
||||
context.args.mpvStatus = true;
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
buildMpvEnv,
|
||||
cleanupPlaybackSession,
|
||||
detectBackend,
|
||||
launchAppBackgroundDetached,
|
||||
findAppBinary,
|
||||
launchAppCommandDetached,
|
||||
launchTexthookerOnly,
|
||||
@@ -425,6 +426,34 @@ test('launchAppCommandDetached handles child process spawn errors', async () =>
|
||||
}
|
||||
});
|
||||
|
||||
test('launchAppBackgroundDetached starts background child directly', async () => {
|
||||
const { dir } = createTempSocketPath();
|
||||
const appPath = path.join(dir, 'fake-subminer.sh');
|
||||
const argsPath = path.join(dir, 'args.txt');
|
||||
const envPath = path.join(dir, 'env.txt');
|
||||
fs.writeFileSync(
|
||||
appPath,
|
||||
[
|
||||
'#!/bin/sh',
|
||||
`printf '%s\\n' "$@" > ${JSON.stringify(argsPath)}`,
|
||||
`printf '%s\\n' "$SUBMINER_BACKGROUND_CHILD" > ${JSON.stringify(envPath)}`,
|
||||
'',
|
||||
].join('\n'),
|
||||
);
|
||||
fs.chmodSync(appPath, 0o755);
|
||||
|
||||
launchAppBackgroundDetached(appPath, 'info');
|
||||
|
||||
const deadline = Date.now() + 1000;
|
||||
while ((!fs.existsSync(argsPath) || !fs.existsSync(envPath)) && Date.now() < deadline) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 20));
|
||||
}
|
||||
|
||||
assert.equal(fs.readFileSync(argsPath, 'utf8').trim(), '--start\n--background');
|
||||
assert.equal(fs.readFileSync(envPath, 'utf8').trim(), '1');
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
test('stopOverlay logs a warning when stop command cannot be spawned', () => {
|
||||
const originalWrite = process.stdout.write;
|
||||
const writes: string[] = [];
|
||||
|
||||
+11
-1
@@ -57,6 +57,7 @@ const OVERLAY_START_SOCKET_READY_TIMEOUT_MS = 900;
|
||||
const OVERLAY_START_COMMAND_SETTLE_TIMEOUT_MS = 700;
|
||||
const TRANSPORTED_APP_ARGC_ENV = 'SUBMINER_APP_ARGC';
|
||||
const TRANSPORTED_APP_ARG_PREFIX = 'SUBMINER_APP_ARG_';
|
||||
const BACKGROUND_CHILD_ENV = 'SUBMINER_BACKGROUND_CHILD';
|
||||
|
||||
export interface LauncherRuntimePluginPlan {
|
||||
scriptPath: string | null;
|
||||
@@ -1589,11 +1590,20 @@ export function launchAppStartDetached(appPath: string, logLevel: LogLevel): voi
|
||||
launchAppCommandDetached(appPath, startArgs, logLevel, 'start');
|
||||
}
|
||||
|
||||
export function launchAppBackgroundDetached(appPath: string, logLevel: LogLevel): void {
|
||||
const startArgs = ['--start', '--background'];
|
||||
if (logLevel !== 'info') startArgs.push('--log-level', logLevel);
|
||||
launchAppCommandDetached(appPath, startArgs, logLevel, 'app', {
|
||||
[BACKGROUND_CHILD_ENV]: '1',
|
||||
});
|
||||
}
|
||||
|
||||
export function launchAppCommandDetached(
|
||||
appPath: string,
|
||||
appArgs: string[],
|
||||
logLevel: LogLevel,
|
||||
label: string,
|
||||
extraEnv: NodeJS.ProcessEnv = {},
|
||||
): void {
|
||||
if (maybeCaptureAppArgs(appArgs)) {
|
||||
return;
|
||||
@@ -1612,7 +1622,7 @@ export function launchAppCommandDetached(
|
||||
const proc = spawn(target.command, target.args, {
|
||||
stdio: ['ignore', stdoutFd, stderrFd],
|
||||
detached: true,
|
||||
env: buildAppEnv(process.env, target.env),
|
||||
env: buildAppEnv(process.env, { ...target.env, ...extraEnv }),
|
||||
});
|
||||
proc.once('error', (error) => {
|
||||
log('warn', logLevel, `${label}: failed to launch detached app: ${error.message}`);
|
||||
|
||||
Reference in New Issue
Block a user