From 4d24e22bb5d49943324e9065563f805adaa50ff8 Mon Sep 17 00:00:00 2001 From: sudacode Date: Sun, 5 Apr 2026 15:32:45 -0700 Subject: [PATCH] fix: force X11 mpv fallback for launcher-managed playback (#47) --- ...-supported-Wayland-tracker-is-available.md | 59 ++++ ...m-leaking-backslash-temp-files-on-POSIX.md | 62 ++++ ...GPU-context-when-X11-fallback-is-active.md | 58 ++++ ...lback-to-mpv-vogpu-with-OpenGL-on-Linux.md | 56 ++++ ...nt-for-launcher-X11-MPV-fallback-change.md | 32 ++ changes/2026.04.05-mpv-x11-fallback.md | 4 + launcher/mpv.test.ts | 290 ++++++++++++++---- launcher/mpv.ts | 96 +++++- 8 files changed, 585 insertions(+), 72 deletions(-) create mode 100644 backlog/tasks/task-280 - Force-launcher-spawned-mpv-onto-X11-when-backend-resolves-to-x11-or-no-supported-Wayland-tracker-is-available.md create mode 100644 backlog/tasks/task-281 - Prevent-Windows-launcher-tests-from-leaking-backslash-temp-files-on-POSIX.md create mode 100644 backlog/tasks/task-282 - Force-launcher-managed-mpv-to-an-explicit-X11-GPU-context-when-X11-fallback-is-active.md create mode 100644 backlog/tasks/task-283 - Force-launcher-managed-X11-fallback-to-mpv-vogpu-with-OpenGL-on-Linux.md create mode 100644 backlog/tasks/task-284 - Fix-CI-changelog-fragment-requirement-for-launcher-X11-MPV-fallback-change.md create mode 100644 changes/2026.04.05-mpv-x11-fallback.md diff --git a/backlog/tasks/task-280 - Force-launcher-spawned-mpv-onto-X11-when-backend-resolves-to-x11-or-no-supported-Wayland-tracker-is-available.md b/backlog/tasks/task-280 - Force-launcher-spawned-mpv-onto-X11-when-backend-resolves-to-x11-or-no-supported-Wayland-tracker-is-available.md new file mode 100644 index 00000000..d7502337 --- /dev/null +++ b/backlog/tasks/task-280 - Force-launcher-spawned-mpv-onto-X11-when-backend-resolves-to-x11-or-no-supported-Wayland-tracker-is-available.md @@ -0,0 +1,59 @@ +--- +id: TASK-280 +title: >- + Force launcher-spawned mpv onto X11 when backend resolves to x11 or no + supported Wayland tracker is available +status: Done +assignee: + - codex +created_date: '2026-04-05 21:01' +updated_date: '2026-04-05 21:05' +labels: + - bug + - linux + - launcher + - overlay +dependencies: [] +priority: high +--- + +## Description + + +On Linux Plasma Wayland and similar sessions, `subminer --backend=x11` currently only changes SubMiner's window-tracker override. The launcher still spawns mpv without forcing an X11/XWayland backend, so the X11 tracker cannot find the mpv window and the overlay remains hidden. Update launcher-side mpv spawn behavior so launcher-managed mpv runs under X11 when backend resolves to `x11`, and also when auto detection cannot resolve to a supported Wayland tracker. Preserve existing Hyprland/Sway behavior. + + +## Acceptance Criteria + +- [x] #1 Launcher-managed mpv is spawned with X11/XWayland-forcing environment/config when backend resolves to `x11`. +- [x] #2 Linux auto mode falls back to X11/XWayland-forced mpv when no supported Wayland tracker backend is detected. +- [x] #3 Hyprland and Sway launcher flows do not regress to forced X11 mpv. +- [x] #4 Regression tests cover launcher env/backend selection for these Linux cases. + + +## Implementation Plan + + +1. Add focused launcher tests that assert the mpv spawn environment forces X11 when backend resolves to `x11`, and when Linux auto mode cannot use a supported Wayland tracker. +2. Refactor launcher mpv spawn code to compute an mpv-specific environment without changing existing Hyprland/Sway flows. +3. Route all launcher-managed mpv spawns through the new environment helper. +4. Run focused launcher tests, then summarize behavior and any remaining verification gaps. + + +## Implementation Notes + + +User approved scope: force launcher-managed mpv to X11 for explicit `--backend=x11` and for unsupported Linux Wayland auto-detect fallback; preserve Hyprland/Sway behavior. + +Implemented launcher-side `buildMpvEnv` to strip Wayland hints and force X11/XWayland for launcher-managed mpv when `--backend=x11`, and for Linux auto mode on unsupported Wayland desktops with an X11 display available. Wired both normal mpv launches and idle detached mpv launches through the helper. + +Verification: `bun test launcher/mpv.test.ts --test-name-pattern "buildMpvEnv"` passed; `bun run tsc --noEmit` passed. A broader `bun test launcher/mpv.test.ts` run still hits a pre-existing sandbox-specific failure in `launchAppCommandDetached handles child process spawn errors` because this environment cannot write the default app log path under `/home/sudacode/.config/SubMiner/logs`. + + +## Final Summary + + +Updated the launcher so mpv gets an X11/XWayland-oriented spawn environment whenever the user explicitly requests `--backend=x11`, and when Linux auto mode is running under an unsupported Wayland desktop that still exposes an X11 display. The new helper reuses the launcher child-process base environment, strips Wayland-specific hints (`WAYLAND_DISPLAY`, Hyprland/Sway markers), and flips `XDG_SESSION_TYPE` to `x11` only for those fallback cases. Both foreground mpv launches and detached idle mpv launches now use the same helper so overlay-tracked playback stays consistent. + +Added focused regression coverage in `launcher/mpv.test.ts` for three cases: explicit `x11` forcing, unsupported Wayland auto fallback (for example KDE Plasma Wayland), and preserving native Wayland env for supported Hyprland/Sway auto backends. Verification completed with `bun test launcher/mpv.test.ts --test-name-pattern "buildMpvEnv"` and `bun run tsc --noEmit`. A broader launcher mpv test run still shows an unrelated sandbox write failure for the default app log path in this environment. + diff --git a/backlog/tasks/task-281 - Prevent-Windows-launcher-tests-from-leaking-backslash-temp-files-on-POSIX.md b/backlog/tasks/task-281 - Prevent-Windows-launcher-tests-from-leaking-backslash-temp-files-on-POSIX.md new file mode 100644 index 00000000..6810b777 --- /dev/null +++ b/backlog/tasks/task-281 - Prevent-Windows-launcher-tests-from-leaking-backslash-temp-files-on-POSIX.md @@ -0,0 +1,62 @@ +--- +id: TASK-281 +title: Prevent Windows launcher tests from leaking backslash temp files on POSIX +status: Done +assignee: + - codex +created_date: '2026-04-05 21:13' +updated_date: '2026-04-05 21:20' +labels: + - tests + - launcher + - bug +dependencies: [] +documentation: + - /home/sudacode/github/SubMiner2/AGENTS.md +priority: medium +--- + +## Description + + +Windows-specific launcher tests in launcher/mpv.test.ts currently create real filesystem entries using path.win32.join(...) with a POSIX mkdtemp base. On Linux/macOS this produces literal backslash-named paths like \\tmp\\subminer-test-win-dir-* inside the repo/worktree, and the existing cleanup only removes the POSIX /tmp base directory. Fix the tests so they still cover Windows path resolution behavior without leaking stray files into the working tree. + + +## Acceptance Criteria + +- [x] #1 Running the Windows findAppBinary tests on a POSIX host does not create new untracked \\tmp\\subminer-test-win-* files in the repository root. +- [x] #2 The Windows launcher tests still validate PATH and install-directory resolution behavior. +- [x] #3 Relevant launcher tests pass after the change. + + +## Implementation Plan + + +1. Add a regression test in launcher/mpv.test.ts that exercises the Windows findAppBinary cases on a POSIX host and asserts they do not leave new backslash-named temp artifacts in the repository root. +2. Refactor the Windows launcher tests to avoid creating real filesystem paths from path.win32.join(...) on POSIX; keep Windows path assertions via stubs and only create real files with native POSIX paths where needed. +3. Run the targeted launcher tests and confirm no new \\tmp\\subminer-test-win-* artifacts appear in git status. + + +## Implementation Notes + + +Investigation: reproduced the leak locally. The source is launcher/mpv.test.ts Windows findAppBinary tests that combine a POSIX mkdtemp base with path.win32.join(...), creating literal backslash-named entries like \\tmp\\subminer-test-win-dir-* in the repo root. Existing cleanup only removes the POSIX /tmp base directory. + +User approved implementation plan on 2026-04-05. + +Implemented in launcher/mpv.test.ts by replacing the leaky Windows PATH/install-directory helpers with pure fs stubs (access/exists/stat) and fixed Windows path strings instead of creating real path.win32 filesystem entries on POSIX. Added a regression test that snapshots repo-root \\tmp\\subminer-test-win-* artifacts before/after running the Windows cases and asserts no new entries are created. + +Verification: `bun test launcher/mpv.test.ts --test-name-pattern 'findAppBinary Windows cases do not leak backslash temp artifacts on POSIX|findAppBinary resolves SubMiner.exe on PATH on Windows|findAppBinary resolves a Windows install directory to SubMiner.exe'` passed (3/3). `bun test launcher/mpv.test.ts` still has one unrelated pre-existing sandbox failure in `launchAppCommandDetached handles child process spawn errors` because the test opens `~/.config/SubMiner/logs/app-2026-04-05.log` and hits `EROFS` in this environment. + + +## Final Summary + + +Reworked the Windows `findAppBinary` tests in `launcher/mpv.test.ts` so they no longer create real backslash-named temp files on POSIX hosts. The PATH and install-directory cases now use synthetic Windows path strings plus `fs.accessSync` / `fs.existsSync` / `fs.statSync` stubs to exercise the same resolver behavior without writing `\\tmp\\subminer-test-win-*` entries into the repository root. + +Added a POSIX regression test that snapshots existing repo-root `\\tmp\\subminer-test-win-*` artifacts, runs the Windows path-resolution cases, and asserts the artifact set is unchanged. This catches future regressions where a Windows-path test accidentally writes literal backslash paths on Linux/macOS. + +Tests run: +- `bun test launcher/mpv.test.ts --test-name-pattern 'findAppBinary Windows cases do not leak backslash temp artifacts on POSIX|findAppBinary resolves SubMiner.exe on PATH on Windows|findAppBinary resolves a Windows install directory to SubMiner.exe'` +- `bun test launcher/mpv.test.ts` (all relevant `findAppBinary` tests passed; one unrelated existing sandbox failure remains in `launchAppCommandDetached handles child process spawn errors` due `EROFS` opening `~/.config/SubMiner/logs/...`) + diff --git a/backlog/tasks/task-282 - Force-launcher-managed-mpv-to-an-explicit-X11-GPU-context-when-X11-fallback-is-active.md b/backlog/tasks/task-282 - Force-launcher-managed-mpv-to-an-explicit-X11-GPU-context-when-X11-fallback-is-active.md new file mode 100644 index 00000000..9e0261e7 --- /dev/null +++ b/backlog/tasks/task-282 - Force-launcher-managed-mpv-to-an-explicit-X11-GPU-context-when-X11-fallback-is-active.md @@ -0,0 +1,58 @@ +--- +id: TASK-282 +title: >- + Force launcher-managed mpv to an explicit X11 GPU context when X11 fallback is + active +status: Done +assignee: + - codex +created_date: '2026-04-05 21:14' +updated_date: '2026-04-05 21:15' +labels: + - bug + - linux + - launcher + - mpv + - overlay +dependencies: [] +priority: high +--- + +## Description + + +Follow-up to the launcher X11 fallback work: on Plasma Wayland, stripping Wayland env vars alone is not sufficient for launcher-managed mpv. mpv can still fail GPU initialization under the default video output path (`vo/gpu-next`) unless an explicit X11 GPU context is selected. Update launcher-managed mpv startup so X11 fallback mode also appends explicit mpv options for an X11 GPU context, while preserving supported Hyprland/Sway Wayland flows. + + +## Acceptance Criteria + +- [x] #1 Launcher-managed mpv appends explicit X11 GPU context args when explicit `--backend=x11` is used on Linux. +- [x] #2 Launcher-managed mpv appends the same explicit X11 GPU context args for Linux auto-mode fallback on unsupported Wayland desktops. +- [x] #3 Supported Hyprland/Sway Wayland flows do not receive the forced X11 GPU context args. +- [x] #4 Regression tests cover the forced-X11 mpv arg selection. + + +## Implementation Plan + + +1. Add focused launcher tests for explicit X11 GPU-context arg selection, unsupported Wayland auto fallback, and Hyprland/Sway no-regression cases. +2. Introduce a shared launcher helper that decides when mpv should be forced onto X11 fallback mode. +3. Use that helper to append explicit mpv X11 GPU-context args in both normal and detached idle mpv launch paths. +4. Run focused launcher tests plus TypeScript verification, then record remaining runtime follow-up guidance. + + +## Implementation Notes + + +User runtime log showed `vo/gpu-next` failing with `Failed initializing any suitable GPU context!` under forced-X11 playback, which indicates env forcing alone was insufficient. Selected `--gpu-context=x11egl,x11` as the explicit mpv fallback: prefer X11/EGL, with GLX as a compatibility fallback. + +Verification: `bun test launcher/mpv.test.ts --test-name-pattern "buildMpv(Env|BackendArgs)"` passed. `bun run tsc --noEmit` passed. + + +## Final Summary + + +Added explicit mpv backend args for launcher-managed X11 fallback mode. The launcher now uses a shared `shouldForceX11MpvBackend` decision for both env rewriting and mpv arg selection, so explicit `--backend=x11` and unsupported Linux Wayland auto fallback both append `--gpu-context=x11egl,x11` while still stripping Wayland env hints. This preserves supported Hyprland/Sway native Wayland flows and makes the X11 fallback more explicit for mpv's GPU initialization path. + +Wired the new X11 GPU-context args into both the normal playback launch path and the detached idle mpv launch path. Added focused regression coverage for explicit `x11`, Plasma-style unsupported Wayland auto fallback, and Hyprland/Sway no-regression behavior. Verification completed with `bun test launcher/mpv.test.ts --test-name-pattern "buildMpv(Env|BackendArgs)"` and `bun run tsc --noEmit`. + diff --git a/backlog/tasks/task-283 - Force-launcher-managed-X11-fallback-to-mpv-vogpu-with-OpenGL-on-Linux.md b/backlog/tasks/task-283 - Force-launcher-managed-X11-fallback-to-mpv-vogpu-with-OpenGL-on-Linux.md new file mode 100644 index 00000000..a2867f09 --- /dev/null +++ b/backlog/tasks/task-283 - Force-launcher-managed-X11-fallback-to-mpv-vogpu-with-OpenGL-on-Linux.md @@ -0,0 +1,56 @@ +--- +id: TASK-283 +title: Force launcher-managed X11 fallback to mpv vo=gpu with OpenGL on Linux +status: Done +assignee: + - codex +created_date: '2026-04-05 21:19' +updated_date: '2026-04-05 21:20' +labels: + - bug + - linux + - launcher + - mpv + - overlay +dependencies: [] +priority: high +--- + +## Description + + +Follow-up to explicit X11 gpu-context forcing: Plasma Wayland runtime logs still show mpv using `vo/gpu-next` and failing to initialize any suitable GPU context under launcher-managed X11 fallback. Update launcher-managed mpv X11 fallback mode to force a more compatible renderer stack: `--vo=gpu`, `--gpu-api=opengl`, and `--gpu-context=x11egl,x11`, while preserving supported native Wayland flows and allowing explicit user mpv args to override later on the command line. + + +## Acceptance Criteria + +- [x] #1 Launcher-managed mpv appends `--vo=gpu`, `--gpu-api=opengl`, and `--gpu-context=x11egl,x11` when explicit `--backend=x11` is used on Linux. +- [x] #2 Launcher-managed mpv appends the same renderer args for Linux auto-mode fallback on unsupported Wayland desktops. +- [x] #3 Supported Hyprland/Sway Wayland flows do not receive the forced X11 renderer args. +- [x] #4 Regression tests cover the forced-X11 renderer arg selection. + + +## Implementation Plan + + +1. Tighten launcher tests so forced-X11 renderer args require `--vo=gpu`, `--gpu-api=opengl`, and `--gpu-context=x11egl,x11`. +2. Update the shared launcher X11-fallback helper to return the full renderer arg stack for explicit `x11` and unsupported Wayland auto fallback. +3. Re-run focused launcher env/backend tests and TypeScript verification. +4. Hand back with retry instructions and next debugging branch if runtime still fails. + + +## Implementation Notes + + +Runtime log still showed `[vo/gpu-next] Failed initializing any suitable GPU context!`, which meant forcing only the context was not enough. Updated the fallback to force the classic OpenGL renderer path too: `--vo=gpu --gpu-api=opengl --gpu-context=x11egl,x11`. + +Verification: `bun test launcher/mpv.test.ts --test-name-pattern "buildMpv(Env|BackendArgs)"` passed. `bun run tsc --noEmit` passed. + + +## Final Summary + + +Updated launcher-managed X11 fallback mode to force a more compatible mpv renderer stack on Linux: `--vo=gpu`, `--gpu-api=opengl`, and `--gpu-context=x11egl,x11`. This applies both to explicit `--backend=x11` and to unsupported Wayland auto fallback, while supported Hyprland/Sway Wayland sessions still keep their native path. The renderer args are still inserted before user-supplied `--args`, so an explicit user override can win later on the command line if needed. + +Adjusted regression coverage to require the full renderer stack for forced-X11 mode and verified the helper behavior with focused launcher tests plus TypeScript compilation. Verification completed with `bun test launcher/mpv.test.ts --test-name-pattern "buildMpv(Env|BackendArgs)"` and `bun run tsc --noEmit`. + diff --git a/backlog/tasks/task-284 - Fix-CI-changelog-fragment-requirement-for-launcher-X11-MPV-fallback-change.md b/backlog/tasks/task-284 - Fix-CI-changelog-fragment-requirement-for-launcher-X11-MPV-fallback-change.md new file mode 100644 index 00000000..d502887f --- /dev/null +++ b/backlog/tasks/task-284 - Fix-CI-changelog-fragment-requirement-for-launcher-X11-MPV-fallback-change.md @@ -0,0 +1,32 @@ +--- +id: TASK-284 +title: Fix CI changelog fragment requirement for launcher X11 MPV fallback change +status: Done +assignee: [] +created_date: '2026-04-05 22:21' +updated_date: '2026-04-05 22:22' +labels: + - ci + - changelog +dependencies: [] +priority: high +--- + +## Description + + +Current CI is failing in `changelog:pr-check` because PR changes release-relevant files but does not include a required entry under `changes/` and lacks `skip-changelog` label. Add a release fragment describing the behavioral change and verify the CI gate passes. + + +## Acceptance Criteria + +- [x] #1 Add a correctly formatted changelog fragment under `changes/` for the current change +- [x] #2 Run the local changelog PR check (or equivalent) with passing result +- [x] #3 Run required CI gate commands after change and confirm no regressions + + +## Final Summary + + +Added `changes/2026.04.05-mpv-x11-fallback.md` with launcher release-note metadata so `changelog:pr-check` can pass. Verified local CI gate commands: `bun run typecheck`, `bun run test:fast`, `bun run test:env`, `bun run build`, `bun run test:smoke:dist` all passed. Ran manual PR changelog verification by invoking `verifyPullRequestChangelog` with current git diff plus the new fragment and confirmed it passes. + diff --git a/changes/2026.04.05-mpv-x11-fallback.md b/changes/2026.04.05-mpv-x11-fallback.md new file mode 100644 index 00000000..da01479b --- /dev/null +++ b/changes/2026.04.05-mpv-x11-fallback.md @@ -0,0 +1,4 @@ +type: fixed +area: launcher + +- Fixed launcher-managed mpv spawning to force an explicit X11 GPU path when Wayland trackers are unavailable. diff --git a/launcher/mpv.test.ts b/launcher/mpv.test.ts index 9ca35aaa..24855f8f 100644 --- a/launcher/mpv.test.ts +++ b/launcher/mpv.test.ts @@ -7,6 +7,8 @@ import net from 'node:net'; import { EventEmitter } from 'node:events'; import type { Args } from './types'; import { + buildMpvBackendArgs, + buildMpvEnv, cleanupPlaybackSession, detectBackend, findAppBinary, @@ -125,6 +127,113 @@ test('detectBackend resolves windows on win32 auto mode', () => { }); }); +test('buildMpvEnv forces X11 by dropping Wayland hints when backend resolves to x11', () => { + withPlatform('linux', () => { + const env = buildMpvEnv(makeArgs({ backend: 'x11' }), { + DISPLAY: ':1', + WAYLAND_DISPLAY: 'wayland-0', + XDG_SESSION_TYPE: 'wayland', + HYPRLAND_INSTANCE_SIGNATURE: 'hypr', + SWAYSOCK: '/tmp/sway.sock', + }); + + assert.equal(env.DISPLAY, ':1'); + assert.equal(env.WAYLAND_DISPLAY, undefined); + assert.equal(env.XDG_SESSION_TYPE, 'x11'); + assert.equal(env.HYPRLAND_INSTANCE_SIGNATURE, undefined); + assert.equal(env.SWAYSOCK, undefined); + }); +}); + +test('buildMpvEnv auto mode falls back to X11 when no supported Wayland tracker backend is detected', () => { + withPlatform('linux', () => { + const env = buildMpvEnv(makeArgs({ backend: 'auto' }), { + DISPLAY: ':1', + WAYLAND_DISPLAY: 'wayland-0', + XDG_SESSION_TYPE: 'wayland', + XDG_CURRENT_DESKTOP: 'KDE', + XDG_SESSION_DESKTOP: 'plasma', + }); + + assert.equal(env.DISPLAY, ':1'); + assert.equal(env.WAYLAND_DISPLAY, undefined); + assert.equal(env.XDG_SESSION_TYPE, 'x11'); + }); +}); + +test('buildMpvEnv preserves native Wayland env for supported Hyprland and Sway auto backends', () => { + withPlatform('linux', () => { + const hyprEnv = buildMpvEnv(makeArgs({ backend: 'auto' }), { + DISPLAY: ':1', + WAYLAND_DISPLAY: 'wayland-0', + XDG_SESSION_TYPE: 'wayland', + HYPRLAND_INSTANCE_SIGNATURE: 'hypr', + }); + assert.equal(hyprEnv.WAYLAND_DISPLAY, 'wayland-0'); + assert.equal(hyprEnv.XDG_SESSION_TYPE, 'wayland'); + + const swayEnv = buildMpvEnv(makeArgs({ backend: 'auto' }), { + DISPLAY: ':1', + WAYLAND_DISPLAY: 'wayland-0', + XDG_SESSION_TYPE: 'wayland', + SWAYSOCK: '/tmp/sway.sock', + }); + assert.equal(swayEnv.WAYLAND_DISPLAY, 'wayland-0'); + assert.equal(swayEnv.XDG_SESSION_TYPE, 'wayland'); + }); +}); + +test('buildMpvBackendArgs forces an explicit X11 renderer stack when backend resolves to x11', () => { + withPlatform('linux', () => { + assert.deepEqual( + buildMpvBackendArgs(makeArgs({ backend: 'x11' }), { + DISPLAY: ':1', + WAYLAND_DISPLAY: 'wayland-0', + XDG_SESSION_TYPE: 'wayland', + }), + ['--vo=gpu', '--gpu-api=opengl', '--gpu-context=x11egl,x11'], + ); + }); +}); + +test('buildMpvBackendArgs forces the same X11 renderer stack for unsupported Wayland auto fallback', () => { + withPlatform('linux', () => { + assert.deepEqual( + buildMpvBackendArgs(makeArgs({ backend: 'auto' }), { + DISPLAY: ':1', + WAYLAND_DISPLAY: 'wayland-0', + XDG_SESSION_TYPE: 'wayland', + XDG_CURRENT_DESKTOP: 'KDE', + XDG_SESSION_DESKTOP: 'plasma', + }), + ['--vo=gpu', '--gpu-api=opengl', '--gpu-context=x11egl,x11'], + ); + }); +}); + +test('buildMpvBackendArgs keeps supported Hyprland and Sway auto backends unchanged', () => { + withPlatform('linux', () => { + assert.deepEqual( + buildMpvBackendArgs(makeArgs({ backend: 'auto' }), { + DISPLAY: ':1', + WAYLAND_DISPLAY: 'wayland-0', + XDG_SESSION_TYPE: 'wayland', + HYPRLAND_INSTANCE_SIGNATURE: 'hypr', + }), + [], + ); + assert.deepEqual( + buildMpvBackendArgs(makeArgs({ backend: 'auto' }), { + DISPLAY: ':1', + WAYLAND_DISPLAY: 'wayland-0', + XDG_SESSION_TYPE: 'wayland', + SWAYSOCK: '/tmp/sway.sock', + }), + [], + ); + }); +}); + test('launchTexthookerOnly exits non-zero when app binary cannot be spawned', () => { const error = withProcessExitIntercept(() => { launchTexthookerOnly('/definitely-missing-subminer-binary', makeArgs()); @@ -485,6 +594,40 @@ function withAccessSyncStub( } } +function withExistsAndStatSyncStubs( + options: { + existingPaths?: string[]; + directoryPaths?: string[]; + }, + run: () => void, +): void { + const existingPaths = new Set(options.existingPaths ?? []); + const directoryPaths = new Set(options.directoryPaths ?? []); + const originalExistsSync = fs.existsSync; + const originalStatSync = fs.statSync; + try { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (fs as any).existsSync = (filePath: string): boolean => + existingPaths.has(filePath) || directoryPaths.has(filePath); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (fs as any).statSync = (filePath: string) => { + if (directoryPaths.has(filePath)) { + return { isDirectory: () => true }; + } + if (existingPaths.has(filePath)) { + return { isDirectory: () => false }; + } + throw Object.assign(new Error(`ENOENT: ${filePath}`), { code: 'ENOENT' }); + }; + run(); + } finally { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (fs as any).existsSync = originalExistsSync; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (fs as any).statSync = originalStatSync; + } +} + function withRealpathSyncStub(resolvePath: (filePath: string) => string, run: () => void): void { const originalRealpathSync = fs.realpathSync; try { @@ -497,6 +640,75 @@ function withRealpathSyncStub(resolvePath: (filePath: string) => string, run: () } } +function listRepoRootWindowsTempArtifacts(): string[] { + return fs + .readdirSync(process.cwd()) + .filter((entry) => /^\\tmp\\subminer-test-win-/.test(entry)) + .sort(); +} + +function runFindAppBinaryWindowsPathCase(): void { + const baseDir = 'C:\\Users\\tester\\subminer-test-win-path'; + const originalHomedir = os.homedir; + const originalPath = process.env.PATH; + try { + os.homedir = () => baseDir; + const binDir = path.win32.join(baseDir, 'bin'); + const wrapperPath = path.win32.join(binDir, 'SubMiner.exe'); + process.env.PATH = `${binDir}${path.win32.delimiter}${originalPath ?? ''}`; + + withFindAppBinaryPlatformSandbox('win32', (pathModule) => { + withAccessSyncStub( + (filePath) => filePath === wrapperPath, + () => { + const result = findAppBinary( + pathModule.join(baseDir, 'launcher', 'SubMiner.exe'), + pathModule, + ); + assert.equal(result, wrapperPath); + }, + ); + }); + } finally { + os.homedir = originalHomedir; + process.env.PATH = originalPath; + } +} + +function runFindAppBinaryWindowsInstallDirCase(): void { + const baseDir = 'C:\\Users\\tester\\subminer-test-win-dir'; + const originalHomedir = os.homedir; + const originalSubminerBinaryPath = process.env.SUBMINER_BINARY_PATH; + try { + os.homedir = () => baseDir; + const installDir = path.win32.join(baseDir, 'Programs', 'SubMiner'); + const appExe = path.win32.join(installDir, 'SubMiner.exe'); + process.env.SUBMINER_BINARY_PATH = installDir; + + withPlatform('win32', () => { + withExistsAndStatSyncStubs( + { existingPaths: [appExe], directoryPaths: [installDir] }, + () => { + withAccessSyncStub( + (filePath) => filePath === appExe, + () => { + const result = findAppBinary(path.win32.join(baseDir, 'launcher', 'SubMiner.exe'), path.win32); + assert.equal(result, appExe); + }, + ); + }, + ); + }); + } finally { + os.homedir = originalHomedir; + if (originalSubminerBinaryPath === undefined) { + delete process.env.SUBMINER_BINARY_PATH; + } else { + process.env.SUBMINER_BINARY_PATH = originalSubminerBinaryPath; + } + } +} + test( 'findAppBinary resolves ~/.local/bin/SubMiner.AppImage when it exists', { concurrency: false }, @@ -657,71 +869,31 @@ test('findAppBinary resolves Windows install paths when present', { concurrency: } }); -test('findAppBinary resolves SubMiner.exe on PATH on Windows', { concurrency: false }, () => { - const baseDir = fs.mkdtempSync(path.join(os.tmpdir(), 'subminer-test-win-path-')); - const originalHomedir = os.homedir; - const originalPath = process.env.PATH; - try { - os.homedir = () => baseDir; - const binDir = path.win32.join(baseDir, 'bin'); - const wrapperPath = path.win32.join(binDir, 'SubMiner.exe'); - makeExecutable(wrapperPath); - process.env.PATH = `${binDir}${path.win32.delimiter}${originalPath ?? ''}`; +test( + 'findAppBinary Windows cases do not leak backslash temp artifacts on POSIX', + { concurrency: false }, + () => { + if (path.sep === '\\') { + return; + } - withFindAppBinaryPlatformSandbox('win32', (pathModule) => { - withAccessSyncStub( - (filePath) => filePath === wrapperPath, - () => { - const result = findAppBinary( - pathModule.join(baseDir, 'launcher', 'SubMiner.exe'), - pathModule, - ); - assert.equal(result, wrapperPath); - }, - ); - }); - } finally { - os.homedir = originalHomedir; - process.env.PATH = originalPath; - fs.rmSync(baseDir, { recursive: true, force: true }); - } + const before = listRepoRootWindowsTempArtifacts(); + runFindAppBinaryWindowsPathCase(); + runFindAppBinaryWindowsInstallDirCase(); + const after = listRepoRootWindowsTempArtifacts(); + + assert.deepEqual(after, before); + }, +); + +test('findAppBinary resolves SubMiner.exe on PATH on Windows', { concurrency: false }, () => { + runFindAppBinaryWindowsPathCase(); }); test( 'findAppBinary resolves a Windows install directory to SubMiner.exe', { concurrency: false }, () => { - const baseDir = fs.mkdtempSync(path.join(os.tmpdir(), 'subminer-test-win-dir-')); - const originalHomedir = os.homedir; - const originalSubminerBinaryPath = process.env.SUBMINER_BINARY_PATH; - try { - os.homedir = () => baseDir; - const installDir = path.win32.join(baseDir, 'Programs', 'SubMiner'); - const appExe = path.win32.join(installDir, 'SubMiner.exe'); - process.env.SUBMINER_BINARY_PATH = installDir; - fs.mkdirSync(installDir, { recursive: true }); - fs.writeFileSync(appExe, '#!/bin/sh\nexit 0\n'); - fs.chmodSync(appExe, 0o755); - - const originalPlatform = process.platform; - try { - Object.defineProperty(process, 'platform', { value: 'win32', configurable: true }); - const result = findAppBinary( - path.win32.join(baseDir, 'launcher', 'SubMiner.exe'), - path.win32, - ); - assert.equal(result, appExe); - } finally { - Object.defineProperty(process, 'platform', { value: originalPlatform, configurable: true }); - } - } finally { - os.homedir = originalHomedir; - if (originalSubminerBinaryPath === undefined) { - delete process.env.SUBMINER_BINARY_PATH; - } else { - process.env.SUBMINER_BINARY_PATH = originalSubminerBinaryPath; - } - fs.rmSync(baseDir, { recursive: true, force: true }); - } + runFindAppBinaryWindowsInstallDirCase(); }, ); diff --git a/launcher/mpv.ts b/launcher/mpv.ts index a366b9f5..7cb72dbf 100644 --- a/launcher/mpv.ts +++ b/launcher/mpv.ts @@ -225,27 +225,65 @@ export function makeTempDir(prefix: string): string { return fs.mkdtempSync(path.join(os.tmpdir(), prefix)); } -export function detectBackend(backend: Backend): Exclude { +export function detectBackend( + backend: Backend, + env: NodeJS.ProcessEnv = process.env, +): Exclude { if (backend !== 'auto') return backend; if (process.platform === 'win32') return 'windows'; 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'; + const linuxDesktopEnv = getLinuxDesktopEnv(env); if ( - process.env.HYPRLAND_INSTANCE_SIGNATURE || - xdgCurrentDesktop.includes('hyprland') || - xdgSessionDesktop.includes('hyprland') + env.HYPRLAND_INSTANCE_SIGNATURE || + linuxDesktopEnv.xdgCurrentDesktop.includes('hyprland') || + linuxDesktopEnv.xdgSessionDesktop.includes('hyprland') ) { return 'hyprland'; } - if (hasWayland && commandExists('hyprctl')) return 'hyprland'; - if (process.env.DISPLAY) return 'x11'; + if (linuxDesktopEnv.hasWayland && commandExists('hyprctl')) return 'hyprland'; + if (env.DISPLAY) return 'x11'; fail('Could not detect display backend'); } +type LinuxDesktopEnv = { + xdgCurrentDesktop: string; + xdgSessionDesktop: string; + hasWayland: boolean; +}; + +function getLinuxDesktopEnv(env: NodeJS.ProcessEnv): LinuxDesktopEnv { + const xdgCurrentDesktop = (env.XDG_CURRENT_DESKTOP || '').toLowerCase(); + const xdgSessionDesktop = (env.XDG_SESSION_DESKTOP || '').toLowerCase(); + const xdgSessionType = (env.XDG_SESSION_TYPE || '').toLowerCase(); + return { + xdgCurrentDesktop, + xdgSessionDesktop, + hasWayland: Boolean(env.WAYLAND_DISPLAY) || xdgSessionType === 'wayland', + }; +} + +function shouldForceX11MpvBackend( + args: Pick, + env: NodeJS.ProcessEnv, +): boolean { + if (process.platform !== 'linux' || !env.DISPLAY?.trim()) { + return false; + } + + const linuxDesktopEnv = getLinuxDesktopEnv(env); + const supportedWaylandBackend = + Boolean(env.HYPRLAND_INSTANCE_SIGNATURE || env.SWAYSOCK) || + linuxDesktopEnv.xdgCurrentDesktop.includes('hyprland') || + linuxDesktopEnv.xdgCurrentDesktop.includes('sway') || + linuxDesktopEnv.xdgSessionDesktop.includes('hyprland') || + linuxDesktopEnv.xdgSessionDesktop.includes('sway'); + return ( + args.backend === 'x11' || + (args.backend === 'auto' && linuxDesktopEnv.hasWayland && !supportedWaylandBackend) + ); +} + function resolveAppBinaryCandidate(candidate: string, pathModule: PathModule = path): string { const direct = resolveBinaryPathCandidate(candidate); if (!direct) return ''; @@ -637,6 +675,7 @@ export async function startMpv( const mpvArgs: string[] = []; if (args.profile) mpvArgs.push(`--profile=${args.profile}`); mpvArgs.push(...DEFAULT_MPV_SUBMINER_ARGS); + mpvArgs.push(...buildMpvBackendArgs(args)); if (targetKind === 'url' && isYoutubeTarget(target)) { log('info', args.logLevel, 'Applying URL playback options'); mpvArgs.push('--ytdl=yes'); @@ -712,7 +751,10 @@ export async function startMpv( const mpvTarget = resolveCommandInvocation('mpv', mpvArgs, { normalizeWindowsShellArgs: false, }); - state.mpvProc = spawn(mpvTarget.command, mpvTarget.args, { stdio: 'inherit' }); + state.mpvProc = spawn(mpvTarget.command, mpvTarget.args, { + stdio: 'inherit', + env: buildMpvEnv(args), + }); } async function waitForOverlayStartCommandSettled( @@ -889,9 +931,9 @@ function stopManagedOverlayApp(args: Args): void { } } -function buildAppEnv(): NodeJS.ProcessEnv { +function buildAppEnv(baseEnv: NodeJS.ProcessEnv = process.env): NodeJS.ProcessEnv { const env: Record = { - ...process.env, + ...baseEnv, SUBMINER_APP_LOG: getAppLogPath(), SUBMINER_MPV_LOG: getMpvLogPath(), }; @@ -911,6 +953,32 @@ function buildAppEnv(): NodeJS.ProcessEnv { return env; } +export function buildMpvEnv( + args: Pick, + baseEnv: NodeJS.ProcessEnv = process.env, +): NodeJS.ProcessEnv { + const env = buildAppEnv(baseEnv); + if (!shouldForceX11MpvBackend(args, env)) { + return env; + } + + delete env.WAYLAND_DISPLAY; + delete env.HYPRLAND_INSTANCE_SIGNATURE; + delete env.SWAYSOCK; + env.XDG_SESSION_TYPE = 'x11'; + return env; +} + +export function buildMpvBackendArgs( + args: Pick, + baseEnv: NodeJS.ProcessEnv = process.env, +): string[] { + if (!shouldForceX11MpvBackend(args, baseEnv)) { + return []; + } + return ['--vo=gpu', '--gpu-api=opengl', '--gpu-context=x11egl,x11']; +} + function appendCapturedAppOutput(kind: 'STDOUT' | 'STDERR', chunk: string): void { const normalized = chunk.replace(/\r\n/g, '\n'); for (const line of normalized.split('\n')) { @@ -1144,6 +1212,7 @@ export function launchMpvIdleDetached( const mpvArgs: string[] = []; if (args.profile) mpvArgs.push(`--profile=${args.profile}`); mpvArgs.push(...DEFAULT_MPV_SUBMINER_ARGS); + mpvArgs.push(...buildMpvBackendArgs(args)); if (args.mpvArgs) { mpvArgs.push(...parseMpvArgString(args.mpvArgs)); } @@ -1159,6 +1228,7 @@ export function launchMpvIdleDetached( const proc = spawn(mpvTarget.command, mpvTarget.args, { stdio: 'ignore', detached: true, + env: buildMpvEnv(args), }); if (typeof proc.pid === 'number' && proc.pid > 0) { trackDetachedMpvPid(proc.pid);