From 3f57682a9ba28239946df83317620fe028576871 Mon Sep 17 00:00:00 2001 From: olcxja Date: Wed, 7 Oct 2026 01:46:26 +0200 Subject: [PATCH] try to improve link handling --- README.md | 2 +- development.md | 1 + src/linuxapp.js | 5 ----- src/linuxrunner.js | 29 ++++++++++++++++++++++------- src/sandbox.js | 7 ++++++- 5 files changed, 30 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index f18276c..f6ff87a 100644 --- a/README.md +++ b/README.md @@ -60,7 +60,7 @@ Per app you choose the **Isolation** policy: *Automatic* (bwrap when it works, p ## "Open with PLWL" -The desktop entry registers `.exe/.msi/.bat/.cmd/.lnk` in file managers. Opening one shows a popup to pick the **Wine/Proton build** and **prefix** for that run (defaults preselected, nothing imported). A second instance hands the path to the running one. App shortcuts work as **Open With** targets too: the app receives the file as the Windows path it would get on Windows — `C:\...` inside its prefix, `Z:\...` otherwise. A file handed to an already running app goes to that instance instead of a second tracked run. +The desktop entry registers `.exe/.msi/.bat/.cmd/.lnk` in file managers. Opening one shows a popup to pick the **Wine/Proton build** and **prefix** for that run (defaults preselected, nothing imported). A second instance hands the path to the running one. App shortcuts work as **Open With** targets too: the app receives the file as the Windows path it would get on Windows — `C:\...` inside its prefix, `Z:\...` otherwise. A file handed to an already running app goes to that instance instead of a second tracked run. A **Linux app shortcut** is an Open With target the same way: the app gets the plain absolute path, and that one file is mounted read-write inside its sandbox — the deliberate exception to "only its home and its app folder are writable", because it is the document you chose to hand over. ## LAN syncing diff --git a/development.md b/development.md index 45bb67d..c8c3127 100644 --- a/development.md +++ b/development.md @@ -85,6 +85,7 @@ Renderer never touches Node/Electron directly — everything goes through the `w - **Reset Linux config** on every prefix row removes exactly what PLWL seeded (DESKTOP_SEEDS, the pulse cookie, `home/.Xauthority`, `.seeded.json`) plus the folders that existed only for the seed, so the next start seeds clean. syncDesktopConfig can only follow a copy it can still prove is its own; the reset does not argue. App data (`data/`, `cache/`, `state/`, `config//`, the rest of `home/`) is never touched, and a running app from that prefix blocks it (the usual `inUse` guard). - The user's real `~` is deliberately invisible in the sandbox (HOME is bound straight to `{prefix}/linux_pfx/home`), so absolute paths an app wrote into its config before the import (Prism's instance `JavaPath`) do not resolve — that is what `isolation: 'off'` is for. - A clicked link belongs to the host desktop, so it leaves through `org.freedesktop.portal.OpenURI` — both routes, because two different things open a link. The `xdg-open` BINARY is replaced inside the sandbox (a written shim `--ro-bind` over `/usr/bin/xdg-open`, the host's own script parked at `/tmp/.plwl-host/xdg-open` as what the shim falls back to for anything that is not a URI): Qt execs it (measured — `QGenericUnixServices` picks `xdg-open` first, and reaches the portal itself only under `/.flatpak-info`/`SNAP`, which a bwrap profile does not have) and so does Electron. GIO resolves an URI IN-PROCESS, which no shim can reach: for that the prefix seeds `home/.plwl/open-url` (launcher-written, like the shim) plus a desktop entry and a `mimeapps.list` naming it the default for `x-scheme-handler/http|https` and `text/html`. Without the seed GIO falls back to the host's `/usr/share` list — measured: it offered `brave-origin.desktop`, and `gio open` answered "The specified location is not supported". Measured end to end (both backends): the portal launches the host's `helium.desktop` with the exact URI. `GIO_USE_PORTALS=1` was measured and REJECTED: it opened NOTHING and still reported success (GIO's portal path wants a sandbox marker this launcher does not fake), while on a machine WITHOUT a portal it fails hard (“Failed to create OpenURI proxy”). The whole wiring is therefore written only when a session bus and a portal are present (`portalReady`), and a KDE-native app resolving through KIO in-process (`kde-open`) stays uncovered — KDE ignores the seeded list (measured). +- A file handed over (a shortcut's `--file %f`, a drop on it) is the ONE path the sandbox cannot see by itself, and all three reasons are real: a file out of the user's home is buried under the prefix home, one out of `/tmp` sits behind the private tmpfs, and the rest of the machine is read-only. `handedFileBinds` returns it for `--bind` at the same path, placed AFTER the app data binds so it wins over the home mounted there, and writable — the way Wine hands a file over as `Z:\…` and may save it (parity with the Wine side is the point: same shortcut, same outcome). Prefix data is skipped, being the app's own writable home already; a folder and a path that is gone are skipped too, since a missing bind source makes bwrap refuse the launch. The argv is untouched: `linuxPathFor` still passes the host path to the app. Only a *new* run gets the mount — a handoff into an app already running in its own namespace cannot, and the app reports that itself. - Not built: a private or overlay `/etc` (`buildBwrapArgs` accepts `etcRoot`, nothing sets it; `--ro-bind / /` leaves `/etc` read-only, which is all we promise today), `--tmpfs /home` against OTHER users' homes (`hideHome` is plumbed, no UI) and `dataMode` — app data always lands in `{prefix}/linux_pfx`. - Measured, not assumed (Debian 13, kernel 6.12, `bwrap` 0.12.0): the functional probe passes here, no firejail/proot/fuse-overlayfs on the box, `/etc` is 16 MB and its `machine-id` is read-only even on the host. diff --git a/src/linuxapp.js b/src/linuxapp.js index 64f724c..2a03dd1 100644 --- a/src/linuxapp.js +++ b/src/linuxapp.js @@ -252,13 +252,8 @@ function findIconFile(spec, baseDir) { } module.exports = { - ELF_MAGIC, - isElfBuffer, - isElfSync, findLinuxExe, - parseDesktopEntry, readDesktopEntry, - desktopExecPath, desktopFileFor, findIconFile, pngOrSvg diff --git a/src/linuxrunner.js b/src/linuxrunner.js index daf1571..eb8db3b 100644 --- a/src/linuxrunner.js +++ b/src/linuxrunner.js @@ -450,6 +450,26 @@ function linuxPathFor(file) { return abs } +// The document a shortcut was handed. The sandbox cannot hand it over by +// itself: a file out of the user's home is buried under the prefix home, one +// out of /tmp sits behind the private tmpfs, and the rest of the machine is +// read-only. So it is mounted where the app was told it is, writable - the way +// a Wine app gets it as Z:\... and may save it. Prefix data needs nothing: it +// already is the app's own writable home. +function handedFileBinds({ openFile, root }) { + if (!openFile) return [] + let abs = '' + try { + abs = fs.realpathSync(String(openFile)) + if (!fs.statSync(abs).isFile()) return [] + } catch { + // a path that is gone must not become a bind bwrap would refuse + return [] + } + if (abs === root || abs.startsWith(root + path.sep)) return [] + return [abs] +} + function bwrapSource() { const devices = ['/dev/shm', '/dev/dri', '/dev/snd', '/dev/input'].filter(d => fs.existsSync(d)) const x11 = fs.existsSync('/tmp/.X11-unix') ? '/tmp/.X11-unix' : '' @@ -481,7 +501,7 @@ async function runLinuxGame({ game, sandbox, onExit, openFile, handoff = false } if (chosen.backend === 'bwrap') { const source = bwrapSource() cmd = 'bwrap' - args = [...buildBwrapArgs({ dataRoot: root, user: source.user, devices: source.devices, x11: source.x11, runUser: source.runUser, shares: source.shares, appDir: writableAppDir(game), urlOpener: routing }), '--', exe, ...argList] + args = [...buildBwrapArgs({ dataRoot: root, user: source.user, devices: source.devices, x11: source.x11, runUser: source.runUser, shares: source.shares, appDir: writableAppDir(game), openFiles: handedFileBinds({ openFile, root }), urlOpener: routing }), '--', exe, ...argList] header.push(`[plwl] HOME=${env.HOME}`) } @@ -489,12 +509,7 @@ async function runLinuxGame({ game, sandbox, onExit, openFile, handoff = false } } module.exports = { - DATA_DIRS, - XDG_LAYOUT, - SYSTEM_DIRS, DESKTOP_SEEDS, - DATA_SHARES, - X11_ENV, SHIM_REL, OPENER_REL, MIME_REL, @@ -509,10 +524,10 @@ module.exports = { ensureLinuxPfx, resetSeededConfig, dataShares, - syncDesktopConfig, buildLinuxEnv, pickBackend, isolationLine, linuxPathFor, + handedFileBinds, runLinuxGame } diff --git a/src/sandbox.js b/src/sandbox.js index bfc990e..2bce2c8 100644 --- a/src/sandbox.js +++ b/src/sandbox.js @@ -115,6 +115,7 @@ function buildBwrapArgs({ etcRoot = '', unshareNet = false, appDir = '', + openFiles = [], urlOpener = null }) { const xdgDir = sub => path.join(home, sub) @@ -152,6 +153,10 @@ function buildBwrapArgs({ '--bind', path.join(dataRoot, 'data'), xdgDir(path.join('.local', 'share')), '--bind', path.join(dataRoot, 'cache'), xdgDir('.cache'), '--bind', path.join(dataRoot, 'state'), xdgDir(path.join('.local', 'state')), + // a file handed over from the desktop, mounted where the app was told it + // is: the prefix home sits ON TOP of the user's real home and /tmp is + // emptied, so such a document would otherwise simply not be there + ...openFiles.flatMap(file => ['--bind', file, file]), // Wayland, PipeWire, PulseAudio and the session bus all live here ...(runUser ? ['--bind', runUser, runUser] : []), ...(etcRoot ? ['--bind', etcRoot, '/etc'] : []), @@ -164,4 +169,4 @@ function buildBwrapArgs({ ] } -module.exports = { createSandboxProbe, buildBwrapArgs, SHARE_ROOT, PROBE_TIMEOUT_MS } +module.exports = { createSandboxProbe, buildBwrapArgs, SHARE_ROOT }