From dbdec9468570b8621c25f8d05c09c7da782a322c Mon Sep 17 00:00:00 2001 From: 4gray Date: Sat, 3 Oct 2026 09:20:51 +0200 Subject: [PATCH] fix(electron): skip the login shell PATH wait for Flatpak host launches In Flatpak, players start through `flatpak-spawn --host`, which resolves the name with the host's PATH; the sandbox's login shell lookup cannot change it. The launch handlers now decide from the same launch context the player uses: no wait in flatpak-host mode, otherwise only for a bare player name. Co-Authored-By: Claude Opus 5.5 --- .../src/app/events/player.events.spec.ts | 39 +++++++++++++++++++ .../src/app/events/player.events.ts | 27 +++++++------ 2 files changed, 52 insertions(+), 14 deletions(-) diff --git a/apps/electron-backend/src/app/events/player.events.spec.ts b/apps/electron-backend/src/app/events/player.events.spec.ts index eeef68397..5250b813c 100644 --- a/apps/electron-backend/src/app/events/player.events.spec.ts +++ b/apps/electron-backend/src/app/events/player.events.spec.ts @@ -4,6 +4,11 @@ jest.mock('electron', () => ({ }, })); +jest.mock('fs', () => { + const actual = jest.requireActual('fs'); + return { ...actual, existsSync: jest.fn(actual.existsSync) }; +}); + jest.mock('child_process', () => ({ spawn: jest.fn(), })); @@ -37,6 +42,7 @@ jest.mock('../services/stalker-playback-context.service', () => ({ import { ipcMain } from 'electron'; import { spawn, type ChildProcess } from 'child_process'; import { EventEmitter } from 'events'; +import * as fs from 'fs'; import { MPV_PLAYER_PATH, store, @@ -528,6 +534,39 @@ describe('external player launch handlers', () => { } ); + it.each(['OPEN_MPV_PLAYER', 'OPEN_VLC_PLAYER'])( + '%s does not wait for a Flatpak host launch of a bare name', + async (channel) => { + const originalPlatform = process.platform; + const existsSync = fs.existsSync as unknown as jest.Mock; + existsSync.mockImplementation( + (candidate: unknown) => String(candidate) === '/.flatpak-info' + ); + Object.defineProperty(process, 'platform', { value: 'linux' }); + configurePlayerPath(channel, 'player-on-host-path'); + try { + await launchUntilSpawn( + channel, + new Promise(() => undefined), + async () => { + await waitForSpawnCallCount(1); + expect(waitForLoginShellPath).not.toHaveBeenCalled(); + expect( + (spawn as unknown as jest.Mock).mock.calls[0][0] + ).toBe('flatpak-spawn'); + } + ); + } finally { + existsSync.mockImplementation( + jest.requireActual('fs').existsSync + ); + Object.defineProperty(process, 'platform', { + value: originalPlatform, + }); + } + } + ); + it.each(['OPEN_MPV_PLAYER', 'OPEN_VLC_PLAYER'])( '%s starts a configured executable path without waiting', async (channel) => { diff --git a/apps/electron-backend/src/app/events/player.events.ts b/apps/electron-backend/src/app/events/player.events.ts index 99236ca12..1dd8f0653 100644 --- a/apps/electron-backend/src/app/events/player.events.ts +++ b/apps/electron-backend/src/app/events/player.events.ts @@ -1,6 +1,7 @@ import { ipcMain } from 'electron'; import { CLOSE_EXTERNAL_PLAYER_SESSION, + type ExternalPlayerName, PLAYBACK_SET_KEEP_AWAKE, PlayerContentInfo, } from '@iptvnator/shared/interfaces'; @@ -12,10 +13,8 @@ import { VLC_PLAYER_PATH, } from '../services/store.service'; import { - getDefaultMpvPath, - getDefaultVlcPath, - normalizeCustomPlayerPath, normalizePlayerPathForStore, + resolveExternalPlayerLaunchContext as resolveLaunchContext, } from './external-player-launch-context'; import { externalPlayerSessions, @@ -43,10 +42,16 @@ export { /** * A player resolved to a bare name (no configured path, no well-known * install found) is looked up through PATH, so it waits for the login shell - * PATH; a path to an executable starts right away. + * PATH; a path to an executable starts right away. So does a Flatpak host + * launch: `flatpak-spawn --host` resolves the name with the host's PATH, + * which the sandbox's login shell lookup cannot change. */ -async function waitForPathIfBareName(playerPath: string): Promise { - if (!/[\\/]/.test(playerPath)) { +async function waitForPathIfBareName( + player: ExternalPlayerName, + configuredPath: string | undefined +): Promise { + const context = resolveLaunchContext(player, configuredPath); + if (context.mode !== 'flatpak-host' && !/[\\/]/.test(context.playerPath)) { await waitForLoginShellPath(); } } @@ -71,10 +76,7 @@ ipcMain.handle( startTime?: number, headers?: Record ) => { - await waitForPathIfBareName( - normalizeCustomPlayerPath(store.get(MPV_PLAYER_PATH)) ?? - getDefaultMpvPath() - ); + await waitForPathIfBareName('mpv', store.get(MPV_PLAYER_PATH)); return openMpvPlayer({ url, title, @@ -118,10 +120,7 @@ ipcMain.handle( startTime?: number, headers?: Record ) => { - await waitForPathIfBareName( - normalizeCustomPlayerPath(store.get(VLC_PLAYER_PATH)) ?? - getDefaultVlcPath() - ); + await waitForPathIfBareName('vlc', store.get(VLC_PLAYER_PATH)); return openVlcPlayer({ url, title,