This commit is contained in:
2026-02-17 22:50:57 -08:00
parent ffeef9c136
commit f20d019c11
315 changed files with 9876 additions and 12537 deletions

View File

@@ -1,33 +1,36 @@
import fs from "node:fs";
import path from "node:path";
import os from "node:os";
import net from "node:net";
import { spawn, spawnSync } from "node:child_process";
import type { LogLevel, Backend, Args, MpvTrack } from "./types.js";
import { DEFAULT_MPV_SUBMINER_ARGS, DEFAULT_YOUTUBE_YTDL_FORMAT } from "./types.js";
import { log, fail, getMpvLogPath } from "./log.js";
import fs from 'node:fs';
import path from 'node:path';
import os from 'node:os';
import net from 'node:net';
import { spawn, spawnSync } from 'node:child_process';
import type { LogLevel, Backend, Args, MpvTrack } from './types.js';
import { DEFAULT_MPV_SUBMINER_ARGS, DEFAULT_YOUTUBE_YTDL_FORMAT } from './types.js';
import { log, fail, getMpvLogPath } from './log.js';
import {
commandExists, isExecutable, resolveBinaryPathCandidate,
realpathMaybe, isYoutubeTarget, uniqueNormalizedLangCodes, sleep, normalizeLangCode,
} from "./util.js";
commandExists,
isExecutable,
resolveBinaryPathCandidate,
realpathMaybe,
isYoutubeTarget,
uniqueNormalizedLangCodes,
sleep,
normalizeLangCode,
} from './util.js';
export const state = {
overlayProc: null as ReturnType<typeof spawn> | null,
mpvProc: null as ReturnType<typeof spawn> | null,
youtubeSubgenChildren: new Set<ReturnType<typeof spawn>>(),
appPath: "" as string,
appPath: '' as string,
overlayManagedByLauncher: false,
stopRequested: false,
};
const DETACHED_IDLE_MPV_PID_FILE = path.join(
os.tmpdir(),
"subminer-idle-mpv.pid",
);
const DETACHED_IDLE_MPV_PID_FILE = path.join(os.tmpdir(), 'subminer-idle-mpv.pid');
function readTrackedDetachedMpvPid(): number | null {
try {
const raw = fs.readFileSync(DETACHED_IDLE_MPV_PID_FILE, "utf8").trim();
const raw = fs.readFileSync(DETACHED_IDLE_MPV_PID_FILE, 'utf8').trim();
const pid = Number.parseInt(raw, 10);
return Number.isInteger(pid) && pid > 0 ? pid : null;
} catch {
@@ -45,7 +48,7 @@ function clearTrackedDetachedMpvPid(): void {
function trackDetachedMpvPid(pid: number): void {
try {
fs.writeFileSync(DETACHED_IDLE_MPV_PID_FILE, String(pid), "utf8");
fs.writeFileSync(DETACHED_IDLE_MPV_PID_FILE, String(pid), 'utf8');
} catch {
// ignore
}
@@ -61,10 +64,10 @@ function isProcessAlive(pid: number): boolean {
}
function processLooksLikeMpv(pid: number): boolean {
if (process.platform !== "linux") return true;
if (process.platform !== 'linux') return true;
try {
const cmdline = fs.readFileSync(`/proc/${pid}/cmdline`, "utf8");
return cmdline.includes("mpv");
const cmdline = fs.readFileSync(`/proc/${pid}/cmdline`, 'utf8');
return cmdline.includes('mpv');
} catch {
return false;
}
@@ -83,7 +86,7 @@ async function terminateTrackedDetachedMpv(logLevel: LogLevel): Promise<void> {
}
try {
process.kill(pid, "SIGTERM");
process.kill(pid, 'SIGTERM');
} catch {
clearTrackedDetachedMpvPid();
return;
@@ -99,62 +102,62 @@ async function terminateTrackedDetachedMpv(logLevel: LogLevel): Promise<void> {
}
try {
process.kill(pid, "SIGKILL");
process.kill(pid, 'SIGKILL');
} catch {
// ignore
}
clearTrackedDetachedMpvPid();
log("debug", logLevel, `Terminated stale detached mpv pid=${pid}`);
log('debug', logLevel, `Terminated stale detached mpv pid=${pid}`);
}
export function makeTempDir(prefix: string): string {
return fs.mkdtempSync(path.join(os.tmpdir(), prefix));
}
export function detectBackend(backend: Backend): Exclude<Backend, "auto"> {
if (backend !== "auto") return backend;
if (process.platform === "darwin") return "macos";
const xdgCurrentDesktop = (process.env.XDG_CURRENT_DESKTOP || "").toLowerCase();
const xdgSessionDesktop = (process.env.XDG_SESSION_DESKTOP || "").toLowerCase();
const xdgSessionType = (process.env.XDG_SESSION_TYPE || "").toLowerCase();
const hasWayland = Boolean(process.env.WAYLAND_DISPLAY) || xdgSessionType === "wayland";
export function detectBackend(backend: Backend): Exclude<Backend, 'auto'> {
if (backend !== 'auto') return backend;
if (process.platform === 'darwin') return 'macos';
const xdgCurrentDesktop = (process.env.XDG_CURRENT_DESKTOP || '').toLowerCase();
const xdgSessionDesktop = (process.env.XDG_SESSION_DESKTOP || '').toLowerCase();
const xdgSessionType = (process.env.XDG_SESSION_TYPE || '').toLowerCase();
const hasWayland = Boolean(process.env.WAYLAND_DISPLAY) || xdgSessionType === 'wayland';
if (
process.env.HYPRLAND_INSTANCE_SIGNATURE ||
xdgCurrentDesktop.includes("hyprland") ||
xdgSessionDesktop.includes("hyprland")
xdgCurrentDesktop.includes('hyprland') ||
xdgSessionDesktop.includes('hyprland')
) {
return "hyprland";
return 'hyprland';
}
if (hasWayland && commandExists("hyprctl")) return "hyprland";
if (process.env.DISPLAY) return "x11";
fail("Could not detect display backend");
if (hasWayland && commandExists('hyprctl')) return 'hyprland';
if (process.env.DISPLAY) return 'x11';
fail('Could not detect display backend');
}
function resolveMacAppBinaryCandidate(candidate: string): string {
const direct = resolveBinaryPathCandidate(candidate);
if (!direct) return "";
if (!direct) return '';
if (process.platform !== "darwin") {
return isExecutable(direct) ? direct : "";
if (process.platform !== 'darwin') {
return isExecutable(direct) ? direct : '';
}
if (isExecutable(direct)) {
return direct;
}
const appIndex = direct.indexOf(".app/");
const appIndex = direct.indexOf('.app/');
const appPath =
direct.endsWith(".app") && direct.includes(".app")
direct.endsWith('.app') && direct.includes('.app')
? direct
: appIndex >= 0
? direct.slice(0, appIndex + ".app".length)
: "";
if (!appPath) return "";
? direct.slice(0, appIndex + '.app'.length)
: '';
if (!appPath) return '';
const candidates = [
path.join(appPath, "Contents", "MacOS", "SubMiner"),
path.join(appPath, "Contents", "MacOS", "subminer"),
path.join(appPath, 'Contents', 'MacOS', 'SubMiner'),
path.join(appPath, 'Contents', 'MacOS', 'subminer'),
];
for (const candidateBinary of candidates) {
@@ -163,14 +166,13 @@ function resolveMacAppBinaryCandidate(candidate: string): string {
}
}
return "";
return '';
}
export function findAppBinary(selfPath: string): string | null {
const envPaths = [
process.env.SUBMINER_APPIMAGE_PATH,
process.env.SUBMINER_BINARY_PATH,
].filter((candidate): candidate is string => Boolean(candidate));
const envPaths = [process.env.SUBMINER_APPIMAGE_PATH, process.env.SUBMINER_BINARY_PATH].filter(
(candidate): candidate is string => Boolean(candidate),
);
for (const envPath of envPaths) {
const resolved = resolveMacAppBinaryCandidate(envPath);
@@ -180,32 +182,22 @@ export function findAppBinary(selfPath: string): string | null {
}
const candidates: string[] = [];
if (process.platform === "darwin") {
candidates.push("/Applications/SubMiner.app/Contents/MacOS/SubMiner");
candidates.push("/Applications/SubMiner.app/Contents/MacOS/subminer");
candidates.push(
path.join(
os.homedir(),
"Applications/SubMiner.app/Contents/MacOS/SubMiner",
),
);
candidates.push(
path.join(
os.homedir(),
"Applications/SubMiner.app/Contents/MacOS/subminer",
),
);
if (process.platform === 'darwin') {
candidates.push('/Applications/SubMiner.app/Contents/MacOS/SubMiner');
candidates.push('/Applications/SubMiner.app/Contents/MacOS/subminer');
candidates.push(path.join(os.homedir(), 'Applications/SubMiner.app/Contents/MacOS/SubMiner'));
candidates.push(path.join(os.homedir(), 'Applications/SubMiner.app/Contents/MacOS/subminer'));
}
candidates.push(path.join(os.homedir(), ".local/bin/SubMiner.AppImage"));
candidates.push("/opt/SubMiner/SubMiner.AppImage");
candidates.push(path.join(os.homedir(), '.local/bin/SubMiner.AppImage'));
candidates.push('/opt/SubMiner/SubMiner.AppImage');
for (const candidate of candidates) {
if (isExecutable(candidate)) return candidate;
}
const fromPath = process.env.PATH?.split(path.delimiter)
.map((dir) => path.join(dir, "subminer"))
.map((dir) => path.join(dir, 'subminer'))
.find((candidate) => isExecutable(candidate));
if (fromPath) {
@@ -220,12 +212,12 @@ export function findAppBinary(selfPath: string): string | null {
export function sendMpvCommand(socketPath: string, command: unknown[]): Promise<void> {
return new Promise((resolve, reject) => {
const socket = net.createConnection(socketPath);
socket.once("connect", () => {
socket.once('connect', () => {
socket.write(`${JSON.stringify({ command })}\n`);
socket.end();
resolve();
});
socket.once("error", (error) => {
socket.once('error', (error) => {
reject(error);
});
});
@@ -245,7 +237,7 @@ export function sendMpvCommandWithResponse(
return new Promise((resolve, reject) => {
const requestId = Date.now() + Math.floor(Math.random() * 1000);
const socket = net.createConnection(socketPath);
let buffer = "";
let buffer = '';
const cleanup = (): void => {
try {
@@ -266,15 +258,15 @@ export function sendMpvCommandWithResponse(
resolve(value);
};
socket.once("connect", () => {
socket.once('connect', () => {
const message = JSON.stringify({ command, request_id: requestId });
socket.write(`${message}\n`);
});
socket.on("data", (chunk: Buffer) => {
socket.on('data', (chunk: Buffer) => {
buffer += chunk.toString();
const lines = buffer.split(/\r?\n/);
buffer = lines.pop() ?? "";
buffer = lines.pop() ?? '';
for (const line of lines) {
if (!line.trim()) continue;
let parsed: MpvResponseEnvelope;
@@ -284,7 +276,7 @@ export function sendMpvCommandWithResponse(
continue;
}
if (parsed.request_id !== requestId) continue;
if (parsed.error && parsed.error !== "success") {
if (parsed.error && parsed.error !== 'success') {
reject(new Error(`MPV error: ${parsed.error}`));
cleanup();
clearTimeout(timer);
@@ -295,7 +287,7 @@ export function sendMpvCommandWithResponse(
}
});
socket.once("error", (error) => {
socket.once('error', (error) => {
clearTimeout(timer);
cleanup();
reject(error);
@@ -306,32 +298,29 @@ export function sendMpvCommandWithResponse(
export async function getMpvTracks(socketPath: string): Promise<MpvTrack[]> {
const response = await sendMpvCommandWithResponse(
socketPath,
["get_property", "track-list"],
['get_property', 'track-list'],
8000,
);
if (!Array.isArray(response)) return [];
return response
.filter((track): track is MpvTrack => {
if (!track || typeof track !== "object") return false;
if (!track || typeof track !== 'object') return false;
const candidate = track as Record<string, unknown>;
return candidate.type === "sub";
return candidate.type === 'sub';
})
.map((track) => {
const candidate = track as Record<string, unknown>;
return {
type:
typeof candidate.type === "string" ? candidate.type : undefined,
type: typeof candidate.type === 'string' ? candidate.type : undefined,
id:
typeof candidate.id === "number"
typeof candidate.id === 'number'
? candidate.id
: typeof candidate.id === "string"
: typeof candidate.id === 'string'
? Number.parseInt(candidate.id, 10)
: undefined,
lang:
typeof candidate.lang === "string" ? candidate.lang : undefined,
title:
typeof candidate.title === "string" ? candidate.title : undefined,
lang: typeof candidate.lang === 'string' ? candidate.lang : undefined,
title: typeof candidate.title === 'string' ? candidate.title : undefined,
};
});
}
@@ -340,10 +329,10 @@ function isPreferredStreamLang(candidate: string, preferred: string[]): boolean
const normalized = normalizeLangCode(candidate);
if (!normalized) return false;
if (preferred.includes(normalized)) return true;
if (normalized === "ja" && preferred.includes("jpn")) return true;
if (normalized === "jpn" && preferred.includes("ja")) return true;
if (normalized === "en" && preferred.includes("eng")) return true;
if (normalized === "eng" && preferred.includes("en")) return true;
if (normalized === 'ja' && preferred.includes('jpn')) return true;
if (normalized === 'jpn' && preferred.includes('ja')) return true;
if (normalized === 'en' && preferred.includes('eng')) return true;
if (normalized === 'eng' && preferred.includes('en')) return true;
return false;
}
@@ -352,7 +341,7 @@ export function findPreferredSubtitleTrack(
preferredLanguages: string[],
): MpvTrack | null {
const normalizedPreferred = uniqueNormalizedLangCodes(preferredLanguages);
const subtitleTracks = tracks.filter((track) => track.type === "sub");
const subtitleTracks = tracks.filter((track) => track.type === 'sub');
if (normalizedPreferred.length === 0) return subtitleTracks[0] ?? null;
for (const lang of normalizedPreferred) {
@@ -374,11 +363,7 @@ export async function waitForSubtitleTrackList(
const tracks = await getMpvTracks(socketPath).catch(() => [] as MpvTrack[]);
if (tracks.length > 0) return tracks;
if (attempt % 10 === 0) {
log(
"debug",
logLevel,
`Waiting for mpv tracks (${attempt}/${maxAttempts})`,
);
log('debug', logLevel, `Waiting for mpv tracks (${attempt}/${maxAttempts})`);
}
await sleep(250);
}
@@ -403,7 +388,7 @@ export async function loadSubtitleIntoMpv(
if (!fs.existsSync(socketPath)) {
if (attempt % 20 === 0) {
log(
"debug",
'debug',
logLevel,
`Waiting for mpv socket before loading subtitle (${attempt} attempts): ${path.basename(subtitlePath)}`,
);
@@ -414,18 +399,14 @@ export async function loadSubtitleIntoMpv(
try {
await sendMpvCommand(
socketPath,
select ? ["sub-add", subtitlePath, "select"] : ["sub-add", subtitlePath],
);
log(
"info",
logLevel,
`Loaded generated subtitle into mpv: ${path.basename(subtitlePath)}`,
select ? ['sub-add', subtitlePath, 'select'] : ['sub-add', subtitlePath],
);
log('info', logLevel, `Loaded generated subtitle into mpv: ${path.basename(subtitlePath)}`);
return;
} catch {
if (attempt % 20 === 0) {
log(
"debug",
'debug',
logLevel,
`Retrying subtitle load into mpv (${attempt} attempts): ${path.basename(subtitlePath)}`,
);
@@ -435,10 +416,7 @@ export async function loadSubtitleIntoMpv(
}
}
export function waitForSocket(
socketPath: string,
timeoutMs = 10000,
): Promise<boolean> {
export function waitForSocket(socketPath: string, timeoutMs = 10000): Promise<boolean> {
const start = Date.now();
return new Promise((resolve) => {
const timer = setInterval(() => {
@@ -457,54 +435,48 @@ export function waitForSocket(
export function startMpv(
target: string,
targetKind: "file" | "url",
targetKind: 'file' | 'url',
args: Args,
socketPath: string,
appPath: string,
preloadedSubtitles?: { primaryPath?: string; secondaryPath?: string },
): void {
if (
targetKind === "file" &&
(!fs.existsSync(target) || !fs.statSync(target).isFile())
) {
if (targetKind === 'file' && (!fs.existsSync(target) || !fs.statSync(target).isFile())) {
fail(`Video file not found: ${target}`);
}
if (targetKind === "url") {
log("info", args.logLevel, `Playing URL: ${target}`);
if (targetKind === 'url') {
log('info', args.logLevel, `Playing URL: ${target}`);
} else {
log("info", args.logLevel, `Playing: ${path.basename(target)}`);
log('info', args.logLevel, `Playing: ${path.basename(target)}`);
}
const mpvArgs: string[] = [];
if (args.profile) mpvArgs.push(`--profile=${args.profile}`);
mpvArgs.push(...DEFAULT_MPV_SUBMINER_ARGS);
if (targetKind === "url" && isYoutubeTarget(target)) {
log("info", args.logLevel, "Applying URL playback options");
mpvArgs.push("--ytdl=yes", "--ytdl-raw-options=");
if (targetKind === 'url' && isYoutubeTarget(target)) {
log('info', args.logLevel, 'Applying URL playback options');
mpvArgs.push('--ytdl=yes', '--ytdl-raw-options=');
if (isYoutubeTarget(target)) {
const subtitleLangs = uniqueNormalizedLangCodes([
...args.youtubePrimarySubLangs,
...args.youtubeSecondarySubLangs,
]).join(",");
const audioLangs = uniqueNormalizedLangCodes(args.youtubeAudioLangs).join(",");
log("info", args.logLevel, "Applying YouTube playback options");
log("debug", args.logLevel, `YouTube subtitle langs: ${subtitleLangs}`);
log("debug", args.logLevel, `YouTube audio langs: ${audioLangs}`);
mpvArgs.push(
`--ytdl-format=${DEFAULT_YOUTUBE_YTDL_FORMAT}`,
`--alang=${audioLangs}`,
);
]).join(',');
const audioLangs = uniqueNormalizedLangCodes(args.youtubeAudioLangs).join(',');
log('info', args.logLevel, 'Applying YouTube playback options');
log('debug', args.logLevel, `YouTube subtitle langs: ${subtitleLangs}`);
log('debug', args.logLevel, `YouTube audio langs: ${audioLangs}`);
mpvArgs.push(`--ytdl-format=${DEFAULT_YOUTUBE_YTDL_FORMAT}`, `--alang=${audioLangs}`);
if (args.youtubeSubgenMode === "off") {
if (args.youtubeSubgenMode === 'off') {
mpvArgs.push(
"--sub-auto=fuzzy",
'--sub-auto=fuzzy',
`--slang=${subtitleLangs}`,
"--ytdl-raw-options-append=write-auto-subs=",
"--ytdl-raw-options-append=write-subs=",
"--ytdl-raw-options-append=sub-format=vtt/best",
'--ytdl-raw-options-append=write-auto-subs=',
'--ytdl-raw-options-append=write-subs=',
'--ytdl-raw-options-append=sub-format=vtt/best',
`--ytdl-raw-options-append=sub-langs=${subtitleLangs}`,
);
}
@@ -517,9 +489,7 @@ export function startMpv(
if (preloadedSubtitles?.secondaryPath) {
mpvArgs.push(`--sub-file=${preloadedSubtitles.secondaryPath}`);
}
mpvArgs.push(
`--script-opts=subminer-binary_path=${appPath},subminer-socket_path=${socketPath}`,
);
mpvArgs.push(`--script-opts=subminer-binary_path=${appPath},subminer-socket_path=${socketPath}`);
mpvArgs.push(`--log-file=${getMpvLogPath()}`);
try {
@@ -531,28 +501,19 @@ export function startMpv(
mpvArgs.push(`--input-ipc-server=${socketPath}`);
mpvArgs.push(target);
state.mpvProc = spawn("mpv", mpvArgs, { stdio: "inherit" });
state.mpvProc = spawn('mpv', mpvArgs, { stdio: 'inherit' });
}
export function startOverlay(
appPath: string,
args: Args,
socketPath: string,
): Promise<void> {
export function startOverlay(appPath: string, args: Args, socketPath: string): Promise<void> {
const backend = detectBackend(args.backend);
log(
"info",
args.logLevel,
`Starting SubMiner overlay (backend: ${backend})...`,
);
log('info', args.logLevel, `Starting SubMiner overlay (backend: ${backend})...`);
const overlayArgs = ["--start", "--backend", backend, "--socket", socketPath];
if (args.logLevel !== "info")
overlayArgs.push("--log-level", args.logLevel);
if (args.useTexthooker) overlayArgs.push("--texthooker");
const overlayArgs = ['--start', '--backend', backend, '--socket', socketPath];
if (args.logLevel !== 'info') overlayArgs.push('--log-level', args.logLevel);
if (args.useTexthooker) overlayArgs.push('--texthooker');
state.overlayProc = spawn(appPath, overlayArgs, {
stdio: "inherit",
stdio: 'inherit',
env: { ...process.env, SUBMINER_MPV_LOG: getMpvLogPath() },
});
state.overlayManagedByLauncher = true;
@@ -563,12 +524,11 @@ export function startOverlay(
}
export function launchTexthookerOnly(appPath: string, args: Args): never {
const overlayArgs = ["--texthooker"];
if (args.logLevel !== "info")
overlayArgs.push("--log-level", args.logLevel);
const overlayArgs = ['--texthooker'];
if (args.logLevel !== 'info') overlayArgs.push('--log-level', args.logLevel);
log("info", args.logLevel, "Launching texthooker mode...");
const result = spawnSync(appPath, overlayArgs, { stdio: "inherit" });
log('info', args.logLevel, 'Launching texthooker mode...');
const result = spawnSync(appPath, overlayArgs, { stdio: 'inherit' });
process.exit(result.status ?? 0);
}
@@ -577,17 +537,16 @@ export function stopOverlay(args: Args): void {
state.stopRequested = true;
if (state.overlayManagedByLauncher && state.appPath) {
log("info", args.logLevel, "Stopping SubMiner overlay...");
log('info', args.logLevel, 'Stopping SubMiner overlay...');
const stopArgs = ["--stop"];
if (args.logLevel !== "info")
stopArgs.push("--log-level", args.logLevel);
const stopArgs = ['--stop'];
if (args.logLevel !== 'info') stopArgs.push('--log-level', args.logLevel);
spawnSync(state.appPath, stopArgs, { stdio: "ignore" });
spawnSync(state.appPath, stopArgs, { stdio: 'ignore' });
if (state.overlayProc && !state.overlayProc.killed) {
try {
state.overlayProc.kill("SIGTERM");
state.overlayProc.kill('SIGTERM');
} catch {
// ignore
}
@@ -596,7 +555,7 @@ export function stopOverlay(args: Args): void {
if (state.mpvProc && !state.mpvProc.killed) {
try {
state.mpvProc.kill("SIGTERM");
state.mpvProc.kill('SIGTERM');
} catch {
// ignore
}
@@ -605,7 +564,7 @@ export function stopOverlay(args: Args): void {
for (const child of state.youtubeSubgenChildren) {
if (!child.killed) {
try {
child.kill("SIGTERM");
child.kill('SIGTERM');
} catch {
// ignore
}
@@ -617,15 +576,18 @@ export function stopOverlay(args: Args): void {
}
function buildAppEnv(): NodeJS.ProcessEnv {
const env: Record<string, string | undefined> = { ...process.env, SUBMINER_MPV_LOG: getMpvLogPath() };
const env: Record<string, string | undefined> = {
...process.env,
SUBMINER_MPV_LOG: getMpvLogPath(),
};
const layers = env.VK_INSTANCE_LAYERS;
if (typeof layers === "string" && layers.trim().length > 0) {
if (typeof layers === 'string' && layers.trim().length > 0) {
const filtered = layers
.split(":")
.split(':')
.map((part) => part.trim())
.filter((part) => part.length > 0 && !/lsfg/i.test(part));
if (filtered.length > 0) {
env.VK_INSTANCE_LAYERS = filtered.join(":");
env.VK_INSTANCE_LAYERS = filtered.join(':');
} else {
delete env.VK_INSTANCE_LAYERS;
}
@@ -633,12 +595,9 @@ function buildAppEnv(): NodeJS.ProcessEnv {
return env;
}
export function runAppCommandWithInherit(
appPath: string,
appArgs: string[],
): never {
export function runAppCommandWithInherit(appPath: string, appArgs: string[]): never {
const result = spawnSync(appPath, appArgs, {
stdio: "inherit",
stdio: 'inherit',
env: buildAppEnv(),
});
if (result.error) {
@@ -653,27 +612,23 @@ export function runAppCommandWithInheritLogged(
logLevel: LogLevel,
label: string,
): never {
log("debug", logLevel, `${label}: launching app with args: ${appArgs.join(" ")}`);
log('debug', logLevel, `${label}: launching app with args: ${appArgs.join(' ')}`);
const result = spawnSync(appPath, appArgs, {
stdio: "inherit",
stdio: 'inherit',
env: buildAppEnv(),
});
if (result.error) {
fail(`Failed to run app command: ${result.error.message}`);
}
log(
"debug",
logLevel,
`${label}: app command exited with status ${result.status ?? 0}`,
);
log('debug', logLevel, `${label}: app command exited with status ${result.status ?? 0}`);
process.exit(result.status ?? 0);
}
export function launchAppStartDetached(appPath: string, logLevel: LogLevel): void {
const startArgs = ["--start"];
if (logLevel !== "info") startArgs.push("--log-level", logLevel);
const startArgs = ['--start'];
if (logLevel !== 'info') startArgs.push('--log-level', logLevel);
const proc = spawn(appPath, startArgs, {
stdio: "ignore",
stdio: 'ignore',
detached: true,
env: buildAppEnv(),
});
@@ -693,23 +648,23 @@ export function launchMpvIdleDetached(
// ignore
}
const mpvArgs: string[] = [];
if (args.profile) mpvArgs.push(`--profile=${args.profile}`);
mpvArgs.push(...DEFAULT_MPV_SUBMINER_ARGS);
mpvArgs.push("--idle=yes");
mpvArgs.push(
`--script-opts=subminer-binary_path=${appPath},subminer-socket_path=${socketPath}`,
);
mpvArgs.push(`--log-file=${getMpvLogPath()}`);
mpvArgs.push(`--input-ipc-server=${socketPath}`);
const proc = spawn("mpv", mpvArgs, {
stdio: "ignore",
detached: true,
});
if (typeof proc.pid === "number" && proc.pid > 0) {
trackDetachedMpvPid(proc.pid);
}
proc.unref();
const mpvArgs: string[] = [];
if (args.profile) mpvArgs.push(`--profile=${args.profile}`);
mpvArgs.push(...DEFAULT_MPV_SUBMINER_ARGS);
mpvArgs.push('--idle=yes');
mpvArgs.push(
`--script-opts=subminer-binary_path=${appPath},subminer-socket_path=${socketPath}`,
);
mpvArgs.push(`--log-file=${getMpvLogPath()}`);
mpvArgs.push(`--input-ipc-server=${socketPath}`);
const proc = spawn('mpv', mpvArgs, {
stdio: 'ignore',
detached: true,
});
if (typeof proc.pid === 'number' && proc.pid > 0) {
trackDetachedMpvPid(proc.pid);
}
proc.unref();
})();
}
@@ -717,10 +672,7 @@ async function sleepMs(ms: number): Promise<void> {
await new Promise<void>((resolve) => setTimeout(resolve, ms));
}
async function waitForPathExists(
filePath: string,
timeoutMs: number,
): Promise<boolean> {
async function waitForPathExists(filePath: string, timeoutMs: number): Promise<boolean> {
const deadline = Date.now() + timeoutMs;
while (Date.now() < deadline) {
try {
@@ -749,8 +701,8 @@ async function canConnectUnixSocket(socketPath: string): Promise<boolean> {
resolve(value);
};
socket.once("connect", () => finish(true));
socket.once("error", () => finish(false));
socket.once('connect', () => finish(true));
socket.once('error', () => finish(false));
socket.setTimeout(400, () => finish(false));
});
}