import fs from 'node:fs'; import os from 'node:os'; import path from 'node:path'; import { log as launcherLog } from './log.js'; import { runAppCommandCaptureOutput, resolveLauncherRuntimePluginPath } from './mpv.js'; import { nowMs } from './time.js'; import { sleep } from './util.js'; import { detectInstalledMpvPlugin } from '../src/main/runtime/first-run-setup-plugin.js'; import { resolveManagedLinuxRuntimePluginPaths, type EnsureLinuxRuntimePluginAssetsResult, } from '../src/main/runtime/linux-runtime-plugin-assets.js'; const RESPONSE_TIMEOUT_MS = 30_000; type PreflightLog = ( level: 'debug' | 'info' | 'warn' | 'error', configured: 'debug' | 'info' | 'warn' | 'error', message: string, ) => void; type EnsureLinuxRuntimePluginAvailableOptions = { appPath?: string; scriptPath?: string; logLevel?: 'debug' | 'info' | 'warn' | 'error'; platform?: NodeJS.Platform; homeDir?: string; xdgConfigHome?: string; xdgDataHome?: string; appDataDir?: string; detectInstalledPlugin?: () => boolean; resolveRuntimePluginPath?: () => string | null; isManagedThemeAvailable?: () => boolean; installManagedPluginAssets?: () => Promise; log?: PreflightLog; }; type RuntimePluginPreflightResponse = { ok: boolean; status: 'installed' | 'already-present' | 'failed'; path?: string; error?: string; }; function resolveConfiguredLogLevel( logLevel: EnsureLinuxRuntimePluginAvailableOptions['logLevel'], ): 'debug' | 'info' | 'warn' | 'error' { return logLevel ?? 'warn'; } async function waitForInstallResponse( responsePath: string, ): Promise { const deadline = nowMs() + RESPONSE_TIMEOUT_MS; while (nowMs() < deadline) { try { if (fs.existsSync(responsePath)) { return JSON.parse(fs.readFileSync(responsePath, 'utf8')) as RuntimePluginPreflightResponse; } } catch { // retry until timeout } await sleep(100); } return null; } type InstallManagedPluginAssetsViaAppDeps = { runAppCommandCaptureOutput?: typeof runAppCommandCaptureOutput; waitForInstallResponse?: typeof waitForInstallResponse; }; export async function installManagedPluginAssetsViaApp( options: { appPath: string; logLevel?: 'debug' | 'info' | 'warn' | 'error'; }, deps: InstallManagedPluginAssetsViaAppDeps = {}, ): Promise { const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'subminer-runtime-plugin-')); const responsePath = path.join(tempDir, 'response.json'); const runAppCommand = deps.runAppCommandCaptureOutput ?? runAppCommandCaptureOutput; const waitForResponse = deps.waitForInstallResponse ?? waitForInstallResponse; try { const appArgs = [ '--ensure-linux-runtime-plugin-assets', '--ensure-linux-runtime-plugin-assets-response-path', responsePath, ]; const result = runAppCommand(options.appPath, appArgs); if (result.error) { return { ok: false, status: 'failed', error: result.error.message, }; } if (result.status !== 0) { const stderr = result.stderr.trim(); const stdout = result.stdout.trim(); return { ok: false, status: 'failed', error: stderr || stdout || `Linux runtime plugin asset install command exited with status ${result.status}.`, }; } const response = await waitForResponse(responsePath); if (response) { return response; } const stderr = result.stderr.trim(); const stdout = result.stdout.trim(); return { ok: false, status: 'failed', error: stderr || stdout || `Timed out waiting for Linux runtime plugin asset response after app exit status ${result.status}.`, }; } finally { try { fs.rmSync(tempDir, { recursive: true, force: true }); } catch { // Avoid hiding the install failure or success result behind temp cleanup errors. } } } export async function ensureLinuxRuntimePluginAvailable( options: EnsureLinuxRuntimePluginAvailableOptions, ): Promise { const platform = options.platform ?? process.platform; if (platform !== 'linux') { return; } const configuredLogLevel = resolveConfiguredLogLevel(options.logLevel); const log = options.log ?? launcherLog; const homeDir = options.homeDir ?? os.homedir(); const detectInstalledPlugin = options.detectInstalledPlugin ?? (() => detectInstalledMpvPlugin({ platform, homeDir, xdgConfigHome: options.xdgConfigHome ?? process.env.XDG_CONFIG_HOME, appDataDir: options.appDataDir ?? process.env.APPDATA, }).installed); const installedPluginAvailable = detectInstalledPlugin(); const managedPaths = resolveManagedLinuxRuntimePluginPaths({ homeDir, xdgDataHome: options.xdgDataHome ?? process.env.XDG_DATA_HOME, }); const resolveRuntimePluginPath = options.resolveRuntimePluginPath ?? (() => { if (!options.appPath) return null; return resolveLauncherRuntimePluginPath({ appPath: options.appPath, scriptPath: options.scriptPath, platform, homeDir, env: process.env, }); }); const isManagedThemeAvailable = options.isManagedThemeAvailable ?? (() => fs.existsSync(managedPaths.themePath)); const runtimePluginAvailable = installedPluginAvailable || Boolean(resolveRuntimePluginPath()); if (runtimePluginAvailable && isManagedThemeAvailable()) { return; } log( 'info', configuredLogLevel, 'Linux runtime support assets missing; installing managed plugin/theme assets.', ); const installManagedPluginAssets = options.installManagedPluginAssets ?? (() => { if (!options.appPath) { throw new Error( 'Linux managed runtime plugin assets could not be installed. Launch aborted before starting mpv.', ); } return installManagedPluginAssetsViaApp({ appPath: options.appPath, logLevel: options.logLevel, }); }); const installResult = await installManagedPluginAssets(); if (!installResult.ok) { const message = installResult.error || 'Unknown Linux runtime plugin asset install failure.'; log( 'warn', configuredLogLevel, `Managed Linux runtime support asset install failed: ${message}`, ); throw new Error(message); } log( 'info', configuredLogLevel, `Managed Linux runtime support assets installed: plugin=${installResult.path ?? 'unknown path'} theme=${managedPaths.themePath}`, ); const runtimePluginPath = resolveRuntimePluginPath(); if (runtimePluginPath) { return; } const message = `Linux managed runtime plugin assets could not be installed. ` + `Checked path: ${managedPaths.pluginEntrypointPath}. ` + 'Launch aborted before starting mpv.'; log('warn', configuredLogLevel, message); throw new Error(message); }