fix(decky): drive a native client install, not only the flatpak
Every headless call went through `flatpak run io.unom.Punktfunk`, and the launch wrapper exec'd the same. On a Deck whose client came from a sysext, a .deb/rpm, an AUR build or a nix profile there is no such app: discovery worked (it is mDNS), and everything that needed the client — pairing, the library fetch, the host store, the stream launch itself — failed. Resolve the client once. The flatpak still wins when it is actually installed, so an existing Deck is byte-for-byte unchanged, and a native `punktfunk-client` is the fallback; `PF_DECKY_CLIENT` forces one on a machine with both. Both kinds share ~/.config/punktfunk — the flatpak's sandbox HOME is the real home — so identity, known-hosts and settings need no divergence. The wrapper takes the resolved binary as PF_CLIENT_BIN and execs it in place of `flatpak run`; kill_stream sends a name-matched SIGTERM where there is no flatpak instance to kill; and the client-update check, which reads the flatpak's tracked remote, simply reports nothing for a native install, whose updates belong to whatever installed it. Installed-ness is now decided by the app's exported directory rather than by the presence of the `flatpak` binary, so a machine that has flatpak but not this app no longer resolves to a client that cannot run. Closes #11 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -20,6 +20,9 @@
|
|||||||
# firing Wake-on-LAN so the connect survives the host's resume)
|
# firing Wake-on-LAN so the connect survives the host's resume)
|
||||||
# PF_APPID flatpak app id (default io.unom.Punktfunk)
|
# PF_APPID flatpak app id (default io.unom.Punktfunk)
|
||||||
# PF_FLATPAK override the flatpak binary path (default: `flatpak` on PATH)
|
# PF_FLATPAK override the flatpak binary path (default: `flatpak` on PATH)
|
||||||
|
# PF_CLIENT_BIN absolute path of a NATIVE client (optional; set by the plugin when it
|
||||||
|
# resolved a non-flatpak install — then the client is exec'd directly and
|
||||||
|
# PF_APPID/PF_FLATPAK are unused)
|
||||||
#
|
#
|
||||||
# Values are plain tokens (the plugin validates launch ids to space/quote-free ASCII before
|
# Values are plain tokens (the plugin validates launch ids to space/quote-free ASCII before
|
||||||
# they ever reach Steam launch options). An older flatpak without --launch/--browse ignores
|
# they ever reach Steam launch options). An older flatpak without --launch/--browse ignores
|
||||||
@@ -38,8 +41,23 @@ set -u
|
|||||||
APPID="${PF_APPID:-io.unom.Punktfunk}"
|
APPID="${PF_APPID:-io.unom.Punktfunk}"
|
||||||
FLATPAK="${PF_FLATPAK:-flatpak}"
|
FLATPAK="${PF_FLATPAK:-flatpak}"
|
||||||
|
|
||||||
# exec so the flatpak client IS the game process — when it exits, Steam ends the "game" and
|
# The client is not always the flatpak: a sysext, a .deb/.rpm, an AUR build or a nix profile
|
||||||
# Gaming Mode reclaims focus automatically (no manual refocus needed).
|
# installs a native `punktfunk-client`, and the plugin passes its absolute path here when that
|
||||||
|
# is what it resolved. Both kinds take the same argv and share ~/.config/punktfunk, so the only
|
||||||
|
# difference is the prefix in front of it.
|
||||||
|
#
|
||||||
|
# exec so the client IS the game process — when it exits, Steam ends the "game" and Gaming Mode
|
||||||
|
# reclaims focus automatically (no manual refocus needed).
|
||||||
|
run_client() {
|
||||||
|
if [ -n "${PF_CLIENT_BIN:-}" ]; then
|
||||||
|
exec "$PF_CLIENT_BIN" "$@"
|
||||||
|
fi
|
||||||
|
exec "$FLATPAK" run --arch=x86_64 "$APPID" "$@"
|
||||||
|
}
|
||||||
|
|
||||||
|
# What we are about to run, for the log line each branch prints.
|
||||||
|
CLIENT_LABEL="${PF_CLIENT_BIN:-$APPID}"
|
||||||
|
|
||||||
# --fullscreen: present the stream chrome-less and fullscreen (the client also auto-detects the
|
# --fullscreen: present the stream chrome-less and fullscreen (the client also auto-detects the
|
||||||
# Deck/gamescope env, and ignores the flag harmlessly on older builds that predate it).
|
# Deck/gamescope env, and ignores the flag harmlessly on older builds that predate it).
|
||||||
if [ -n "${PF_BROWSE:-}" ]; then
|
if [ -n "${PF_BROWSE:-}" ]; then
|
||||||
@@ -48,14 +66,14 @@ if [ -n "${PF_BROWSE:-}" ]; then
|
|||||||
# library shortcut launches. `--browse <host>` opens straight into that host's library (the
|
# library shortcut launches. `--browse <host>` opens straight into that host's library (the
|
||||||
# per-host "open on screen" action). A streams a game, session end returns here, B quits.
|
# per-host "open on screen" action). A streams a game, session end returns here, B quits.
|
||||||
if [ -z "${PF_HOST:-}" ]; then
|
if [ -z "${PF_HOST:-}" ]; then
|
||||||
echo "punktfunkrun: gamepad UI $APPID --browse (console home)" >&2
|
echo "punktfunkrun: gamepad UI $CLIENT_LABEL --browse (console home)" >&2
|
||||||
exec "$FLATPAK" run --arch=x86_64 "$APPID" --browse --fullscreen
|
run_client --browse --fullscreen
|
||||||
fi
|
fi
|
||||||
echo "punktfunkrun: library $APPID --browse $PF_HOST" >&2
|
echo "punktfunkrun: library $CLIENT_LABEL --browse $PF_HOST" >&2
|
||||||
if [ -n "${PF_MGMT:-}" ]; then
|
if [ -n "${PF_MGMT:-}" ]; then
|
||||||
exec "$FLATPAK" run --arch=x86_64 "$APPID" --browse "$PF_HOST" --mgmt "$PF_MGMT" --fullscreen
|
run_client --browse "$PF_HOST" --mgmt "$PF_MGMT" --fullscreen
|
||||||
fi
|
fi
|
||||||
exec "$FLATPAK" run --arch=x86_64 "$APPID" --browse "$PF_HOST" --fullscreen
|
run_client --browse "$PF_HOST" --fullscreen
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Streaming modes need a host (browse above is the only host-less path).
|
# Streaming modes need a host (browse above is the only host-less path).
|
||||||
@@ -72,8 +90,8 @@ if [ -n "${PF_CONNECT_TIMEOUT:-}" ]; then
|
|||||||
fi
|
fi
|
||||||
if [ -n "${PF_LAUNCH:-}" ]; then
|
if [ -n "${PF_LAUNCH:-}" ]; then
|
||||||
# A pinned game: the id rides the session Hello and the host launches that title.
|
# A pinned game: the id rides the session Hello and the host launches that title.
|
||||||
echo "punktfunkrun: streaming $APPID --connect $PF_HOST --launch $PF_LAUNCH" >&2
|
echo "punktfunkrun: streaming $CLIENT_LABEL --connect $PF_HOST --launch $PF_LAUNCH" >&2
|
||||||
exec "$FLATPAK" run --arch=x86_64 "$APPID" --connect "$PF_HOST" --launch "$PF_LAUNCH" "$@"
|
run_client --connect "$PF_HOST" --launch "$PF_LAUNCH" "$@"
|
||||||
fi
|
fi
|
||||||
echo "punktfunkrun: streaming $APPID --connect $PF_HOST" >&2
|
echo "punktfunkrun: streaming $CLIENT_LABEL --connect $PF_HOST" >&2
|
||||||
exec "$FLATPAK" run --arch=x86_64 "$APPID" --connect "$PF_HOST" "$@"
|
run_client --connect "$PF_HOST" "$@"
|
||||||
|
|||||||
+112
-24
@@ -328,10 +328,75 @@ def _flatpak() -> str | None:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# --- which client is installed -------------------------------------------------------------
|
||||||
|
#
|
||||||
|
# The flatpak is the Deck's usual client, but it is not the only one: a sysext, a .deb/.rpm, an
|
||||||
|
# AUR build, a nix profile and a hand-built binary all install a NATIVE `punktfunk-client`, and
|
||||||
|
# on those the plugin used to be dead in the water — every headless call went through
|
||||||
|
# `flatpak run io.unom.Punktfunk` and simply failed. Both kinds keep identity, known-hosts and
|
||||||
|
# settings in the same ~/.config/punktfunk (the flatpak's sandbox HOME resolves to the real
|
||||||
|
# home), so nothing else in this file has to care which one answered.
|
||||||
|
NATIVE_BIN = "punktfunk-client"
|
||||||
|
|
||||||
|
# Prefixes to try when PATH doesn't have it. The Decky backend runs with a minimal PATH, and
|
||||||
|
# SteamOS's read-only /usr pushes native installs into a sysext or the user's own prefix.
|
||||||
|
_NATIVE_PREFIXES = (
|
||||||
|
"/usr/bin",
|
||||||
|
"/usr/local/bin",
|
||||||
|
"/run/host/usr/bin",
|
||||||
|
"/var/lib/extensions/punktfunk/usr/bin",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _native_client() -> str | None:
|
||||||
|
"""Absolute path of a native (non-flatpak) client binary, or None."""
|
||||||
|
found = shutil.which(NATIVE_BIN, path=os.environ.get("PATH", "") + ":" + ":".join(_NATIVE_PREFIXES))
|
||||||
|
if found:
|
||||||
|
return found
|
||||||
|
for prefix in (str(Path(decky.DECKY_USER_HOME) / ".local" / "bin"),):
|
||||||
|
candidate = Path(prefix) / NATIVE_BIN
|
||||||
|
if candidate.exists():
|
||||||
|
return str(candidate)
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _flatpak_installed() -> bool:
|
||||||
|
"""True when the flatpak APP is actually installed — not merely that `flatpak` exists.
|
||||||
|
|
||||||
|
Checked by the app's own exported directory rather than by shelling out to `flatpak info`,
|
||||||
|
because this is on the path of every headless call and a subprocess per call would be absurd.
|
||||||
|
Both scopes count: the Deck installs --user, a distro image may ship it system-wide.
|
||||||
|
"""
|
||||||
|
if not _flatpak():
|
||||||
|
return False
|
||||||
|
user = Path(decky.DECKY_USER_HOME) / ".local" / "share" / "flatpak" / "app" / APP_ID
|
||||||
|
return user.exists() or Path("/var/lib/flatpak/app", APP_ID).exists()
|
||||||
|
|
||||||
|
|
||||||
|
def _client_argv() -> list[str] | None:
|
||||||
|
"""The argv PREFIX that runs the client headlessly, or None when no client is installed.
|
||||||
|
|
||||||
|
Flatpak wins when it is installed: it is the tested Deck path, so an existing install keeps
|
||||||
|
behaving exactly as it did. A native binary is the fallback — and on a machine with no
|
||||||
|
flatpak client, the thing that makes the plugin work at all. `PF_DECKY_CLIENT=native|flatpak`
|
||||||
|
forces one when a machine has both.
|
||||||
|
"""
|
||||||
|
forced = os.environ.get("PF_DECKY_CLIENT", "").strip().lower()
|
||||||
|
native = _native_client()
|
||||||
|
if forced == "native":
|
||||||
|
return [native] if native else None
|
||||||
|
if forced != "flatpak" and not _flatpak_installed() and native:
|
||||||
|
return [native]
|
||||||
|
if _flatpak_installed():
|
||||||
|
return [_flatpak(), "run", "--arch=x86_64", APP_ID]
|
||||||
|
return [native] if native else None
|
||||||
|
|
||||||
|
|
||||||
def _flatpak_env() -> dict:
|
def _flatpak_env() -> dict:
|
||||||
"""Environment for a headless ``flatpak run`` from the backend (no display needed for
|
"""Environment for a headless client run from the backend (no display needed for pairing).
|
||||||
pairing). Reconstruct the user-session bits flatpak wants; the backend may not inherit
|
Reconstruct the user-session bits flatpak wants; the backend may not inherit them. Harmless
|
||||||
them. Harmless if some are already set."""
|
if some are already set — and correct for a NATIVE client too, which needs the same HOME and
|
||||||
|
the same LD_LIBRARY_PATH repair below."""
|
||||||
env = dict(os.environ)
|
env = dict(os.environ)
|
||||||
# Decky Loader is a PyInstaller binary: it prepends its bundled libs (an older libssl) to
|
# Decky Loader is a PyInstaller binary: it prepends its bundled libs (an older libssl) to
|
||||||
# LD_LIBRARY_PATH (its /tmp/_MEI* unpack dir), and that env leaks into our subprocess. The
|
# LD_LIBRARY_PATH (its /tmp/_MEI* unpack dir), and that env leaks into our subprocess. The
|
||||||
@@ -388,17 +453,19 @@ async def _flatpak_capture(args: list[str], timeout: float = 20.0) -> tuple[int,
|
|||||||
|
|
||||||
|
|
||||||
async def _run_client(client_args: list[str], timeout: float = 20.0) -> tuple[int, str, str]:
|
async def _run_client(client_args: list[str], timeout: float = 20.0) -> tuple[int, str, str]:
|
||||||
"""Run the flatpak CLIENT headlessly (``flatpak run … io.unom.Punktfunk <client_args>``) with
|
"""Run the CLIENT headlessly with the user-session env, returning ``(returncode, stdout,
|
||||||
the user-session env, returning ``(returncode, stdout, stderr)`` with SEPARATE pipes so a JSON
|
stderr)`` with SEPARATE pipes so a JSON payload on stdout stays clean of the client's log
|
||||||
payload on stdout stays clean of the client's log lines on stderr. ``(-1, "", "")`` when
|
lines on stderr. ``(-1, "", "")`` when no client is installed or the call errors/times out.
|
||||||
flatpak is missing or the call errors/times out. This is the single entry point for the
|
|
||||||
headless host-store modes (``--list-hosts`` / ``--add-host`` / ``--set-host`` /
|
Whether that client is the flatpak or a native install is [_client_argv]'s business; both
|
||||||
``--forget-host`` / ``--reset`` / ``--reachable``), which mutate the SAME
|
read and write the SAME ``client-known-hosts.json`` the desktop client uses. This is the
|
||||||
``client-known-hosts.json`` the desktop client reads — so state is shared, not duplicated."""
|
single entry point for the headless host-store modes (``--list-hosts`` / ``--add-host`` /
|
||||||
flatpak = _flatpak()
|
``--set-host`` / ``--forget-host`` / ``--reset`` / ``--reachable``), so state is shared, not
|
||||||
if not flatpak:
|
duplicated."""
|
||||||
|
prefix = _client_argv()
|
||||||
|
if not prefix:
|
||||||
return -1, "", ""
|
return -1, "", ""
|
||||||
argv = [flatpak, "run", "--arch=x86_64", APP_ID, *client_args]
|
argv = [*prefix, *client_args]
|
||||||
proc = None
|
proc = None
|
||||||
try:
|
try:
|
||||||
proc = await asyncio.create_subprocess_exec(
|
proc = await asyncio.create_subprocess_exec(
|
||||||
@@ -885,9 +952,22 @@ class Plugin:
|
|||||||
"""The wrapper-script path + flatpak app id the frontend needs to create the Steam
|
"""The wrapper-script path + flatpak app id the frontend needs to create the Steam
|
||||||
shortcut. The shortcut invokes the script through ``/bin/sh`` (see steam.ts), so no
|
shortcut. The shortcut invokes the script through ``/bin/sh`` (see steam.ts), so no
|
||||||
exec bit is needed — Decky's zip extraction drops it, and the root-owned plugins dir
|
exec bit is needed — Decky's zip extraction drops it, and the root-owned plugins dir
|
||||||
means this unprivileged backend couldn't chmod it back on anyway."""
|
means this unprivileged backend couldn't chmod it back on anyway.
|
||||||
|
|
||||||
|
``client_bin`` is set only when the resolved client is a NATIVE install; the frontend
|
||||||
|
passes it to the wrapper as ``PF_CLIENT_BIN`` so the launch execs the binary instead of
|
||||||
|
``flatpak run``. Absent = the wrapper's flatpak default, i.e. every existing Deck
|
||||||
|
install is unaffected."""
|
||||||
path = _runner_path()
|
path = _runner_path()
|
||||||
return {"runner": path, "app_id": APP_ID, "exists": Path(path).exists()}
|
prefix = _client_argv()
|
||||||
|
native = bool(prefix) and prefix[0] != _flatpak()
|
||||||
|
return {
|
||||||
|
"runner": path,
|
||||||
|
"app_id": APP_ID,
|
||||||
|
"exists": Path(path).exists(),
|
||||||
|
"client_kind": "native" if native else ("flatpak" if prefix else "none"),
|
||||||
|
"client_bin": prefix[0] if native else "",
|
||||||
|
}
|
||||||
|
|
||||||
async def get_settings(self) -> dict:
|
async def get_settings(self) -> dict:
|
||||||
"""Read the flatpak client's stream settings (resolution/bitrate/gamepad…)."""
|
"""Read the flatpak client's stream settings (resolution/bitrate/gamepad…)."""
|
||||||
@@ -1032,28 +1112,36 @@ class Plugin:
|
|||||||
}
|
}
|
||||||
|
|
||||||
async def kill_stream(self) -> dict:
|
async def kill_stream(self) -> dict:
|
||||||
"""Force-stop a wedged stream client (``flatpak kill``)."""
|
"""Force-stop a wedged stream client — ``flatpak kill`` for the sandboxed one, a plain
|
||||||
flatpak = _flatpak()
|
SIGTERM by name for a native install (which has no flatpak instance to kill)."""
|
||||||
if not flatpak:
|
prefix = _client_argv()
|
||||||
return {"ok": False, "error": "flatpak-not-found"}
|
if not prefix:
|
||||||
|
return {"ok": False, "error": "client-not-found"}
|
||||||
|
if prefix[0] == _flatpak():
|
||||||
|
argv = [prefix[0], "kill", APP_ID]
|
||||||
|
else:
|
||||||
|
# -x: whole-name match, so this can only ever hit the client itself.
|
||||||
|
killer = shutil.which("pkill") or "/usr/bin/pkill"
|
||||||
|
argv = [killer, "-x", NATIVE_BIN]
|
||||||
try:
|
try:
|
||||||
proc = await asyncio.create_subprocess_exec(
|
proc = await asyncio.create_subprocess_exec(
|
||||||
flatpak, "kill", APP_ID,
|
*argv,
|
||||||
stdout=asyncio.subprocess.DEVNULL, stderr=asyncio.subprocess.DEVNULL,
|
stdout=asyncio.subprocess.DEVNULL, stderr=asyncio.subprocess.DEVNULL,
|
||||||
env=_flatpak_env(),
|
env=_flatpak_env(),
|
||||||
)
|
)
|
||||||
await asyncio.wait_for(proc.wait(), timeout=10.0)
|
await asyncio.wait_for(proc.wait(), timeout=10.0)
|
||||||
except Exception: # noqa: BLE001
|
except Exception: # noqa: BLE001
|
||||||
decky.logger.exception("flatpak kill failed")
|
decky.logger.exception("kill_stream (%s) failed", argv[0])
|
||||||
return {"ok": False}
|
return {"ok": False}
|
||||||
return {"ok": True}
|
return {"ok": True}
|
||||||
|
|
||||||
async def update_client(self) -> dict:
|
async def update_client(self) -> dict:
|
||||||
"""Update the flatpak **client** (io.unom.Punktfunk) in the USER installation — the scope a
|
"""Update the flatpak **client** (io.unom.Punktfunk) in the USER installation — the scope a
|
||||||
Steam Deck install lives in, which ``sudo flatpak update`` (system-scope) never reaches.
|
Steam Deck install lives in, which ``sudo flatpak update`` (system-scope) never reaches.
|
||||||
Returns whether a new commit was actually pulled. Best-effort; non-fatal."""
|
Returns whether a new commit was actually pulled. Best-effort; non-fatal. A NATIVE client
|
||||||
flatpak = _flatpak()
|
is updated by whatever installed it (distro package manager, sysext, nix), never here —
|
||||||
if not flatpak:
|
`check_update` reports no client update for one, so the UI never offers this."""
|
||||||
|
if not _flatpak_installed():
|
||||||
return {"ok": False, "updated": False, "error": "flatpak-not-found"}
|
return {"ok": False, "updated": False, "error": "flatpak-not-found"}
|
||||||
_, before = await _flatpak_capture(["info", "--user", APP_ID], timeout=10.0)
|
_, before = await _flatpak_capture(["info", "--user", APP_ID], timeout=10.0)
|
||||||
before_commit = _field_from(before, "Commit")
|
before_commit = _field_from(before, "Commit")
|
||||||
|
|||||||
@@ -90,6 +90,11 @@ export interface RunnerInfo {
|
|||||||
runner: string; // absolute path to bin/punktfunkrun.sh
|
runner: string; // absolute path to bin/punktfunkrun.sh
|
||||||
app_id: string; // flatpak app id
|
app_id: string; // flatpak app id
|
||||||
exists: boolean;
|
exists: boolean;
|
||||||
|
// Which client the backend resolved: the flatpak, a native install (.deb/rpm/sysext/AUR/nix),
|
||||||
|
// or none at all. Older backends send neither field — hence optional.
|
||||||
|
client_kind?: "flatpak" | "native" | "none";
|
||||||
|
// Absolute path of the native binary; "" for flatpak. Passed to the wrapper as PF_CLIENT_BIN.
|
||||||
|
client_bin?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
// The slice of the flatpak client's settings JSON this UI surfaces. The file can hold more
|
// The slice of the flatpak client's settings JSON this UI surfaces. The file can hold more
|
||||||
|
|||||||
@@ -214,7 +214,7 @@ async function ensureControllerConfig(): Promise<void> {
|
|||||||
* the current runner path. Reuses/repoints the remembered shortcut (the plugin dir can change
|
* the current runner path. Reuses/repoints the remembered shortcut (the plugin dir can change
|
||||||
* across reinstalls, and pre-two-shortcut installs had this one visible).
|
* across reinstalls, and pre-two-shortcut installs had this one visible).
|
||||||
*/
|
*/
|
||||||
async function ensureStreamShortcut(): Promise<{ appId: number; runner: string }> {
|
async function ensureStreamShortcut(): Promise<{ appId: number; runner: string; clientBin: string }> {
|
||||||
const info = await runnerInfo();
|
const info = await runnerInfo();
|
||||||
if (!info.exists) {
|
if (!info.exists) {
|
||||||
throw new Error(`launch wrapper missing at ${info.runner}`);
|
throw new Error(`launch wrapper missing at ${info.runner}`);
|
||||||
@@ -231,7 +231,7 @@ async function ensureStreamShortcut(): Promise<{ appId: number; runner: string }
|
|||||||
SteamClient.Apps.SetShortcutName(remembered, SHORTCUT_NAME);
|
SteamClient.Apps.SetShortcutName(remembered, SHORTCUT_NAME);
|
||||||
setShortcutHidden(remembered, true); // migrate pre-two-shortcut installs (were visible)
|
setShortcutHidden(remembered, true); // migrate pre-two-shortcut installs (were visible)
|
||||||
void applyArtwork(remembered);
|
void applyArtwork(remembered);
|
||||||
return { appId: remembered, runner: info.runner };
|
return { appId: remembered, runner: info.runner, clientBin: info.client_bin ?? "" };
|
||||||
}
|
}
|
||||||
|
|
||||||
const appId = await SteamClient.Apps.AddShortcut(SHORTCUT_NAME, SHELL, startDir, "");
|
const appId = await SteamClient.Apps.AddShortcut(SHORTCUT_NAME, SHELL, startDir, "");
|
||||||
@@ -239,7 +239,7 @@ async function ensureStreamShortcut(): Promise<{ appId: number; runner: string }
|
|||||||
setShortcutHidden(appId, true);
|
setShortcutHidden(appId, true);
|
||||||
void applyArtwork(appId);
|
void applyArtwork(appId);
|
||||||
remember(STORAGE_KEY_STREAM, appId);
|
remember(STORAGE_KEY_STREAM, appId);
|
||||||
return { appId, runner: info.runner };
|
return { appId, runner: info.runner, clientBin: info.client_bin ?? "" };
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -259,7 +259,10 @@ export async function ensureGamepadUiShortcut(): Promise<number | null> {
|
|||||||
void ensureControllerConfig();
|
void ensureControllerConfig();
|
||||||
// Bare browse: PF_BROWSE with no PF_HOST → the wrapper runs `--browse --fullscreen` (console
|
// Bare browse: PF_BROWSE with no PF_HOST → the wrapper runs `--browse --fullscreen` (console
|
||||||
// home). %command% expands to the shortcut exe (/bin/sh); the wrapper rides behind as an arg.
|
// home). %command% expands to the shortcut exe (/bin/sh); the wrapper rides behind as an arg.
|
||||||
const launchOpts = `PF_BROWSE=1 %command% "${info.runner}"`;
|
// PF_CLIENT_BIN only when the backend resolved a NATIVE client — else the wrapper's flatpak
|
||||||
|
// default stands and this shortcut is exactly what it always was.
|
||||||
|
const clientBin = info.client_bin ? `PF_CLIENT_BIN=${info.client_bin} ` : "";
|
||||||
|
const launchOpts = `${clientBin}PF_BROWSE=1 %command% "${info.runner}"`;
|
||||||
|
|
||||||
// Reuse the remembered entry only if it still exists; a stale appId (deleted shortcut whose
|
// Reuse the remembered entry only if it still exists; a stale appId (deleted shortcut whose
|
||||||
// localStorage key survived a plugin reinstall) falls through to AddShortcut so the visible
|
// localStorage key survived a plugin reinstall) falls through to AddShortcut so the visible
|
||||||
@@ -357,9 +360,14 @@ export async function launchStream(
|
|||||||
// Best-effort — the flatpak client's --wake looks up the host's learned MAC (a no-op if none is
|
// Best-effort — the flatpak client's --wake looks up the host's learned MAC (a no-op if none is
|
||||||
// known), and the connect that follows has its own retry window, so a failure never blocks launch.
|
// known), and the connect that follows has its own retry window, so a failure never blocks launch.
|
||||||
const waking = wake(host, port).catch(() => ({ ok: false }));
|
const waking = wake(host, port).catch(() => ({ ok: false }));
|
||||||
const [{ appId, runner }, woke] = await Promise.all([ensureStreamShortcut(), waking]);
|
const [{ appId, runner, clientBin }, woke] = await Promise.all([ensureStreamShortcut(), waking]);
|
||||||
const target = port && port !== 9777 ? `${host}:${port}` : host;
|
const target = port && port !== 9777 ? `${host}:${port}` : host;
|
||||||
const env = [`PF_HOST=${target}`];
|
const env = [`PF_HOST=${target}`];
|
||||||
|
// Set only for a NATIVE client install; absent, the wrapper takes its flatpak default, so every
|
||||||
|
// existing Deck install produces byte-identical launch options to before.
|
||||||
|
if (clientBin) {
|
||||||
|
env.push(`PF_CLIENT_BIN=${clientBin}`);
|
||||||
|
}
|
||||||
// A magic packet actually went out (a MAC was known), so the host may be mid-resume from
|
// A magic packet actually went out (a MAC was known), so the host may be mid-resume from
|
||||||
// suspend — that takes far longer than the client's default 15 s connect budget. Stretch the
|
// suspend — that takes far longer than the client's default 15 s connect budget. Stretch the
|
||||||
// budget so the client's wake-tolerant dial keeps retrying across the resume; against an
|
// budget so the client's wake-tolerant dial keeps retrying across the resume; against an
|
||||||
|
|||||||
Reference in New Issue
Block a user