try to improve link handling
This commit is contained in:
parent
2ff8920225
commit
3f57682a9b
5 changed files with 30 additions and 14 deletions
|
|
@ -60,7 +60,7 @@ Per app you choose the **Isolation** policy: *Automatic* (bwrap when it works, p
|
||||||
|
|
||||||
## "Open with PLWL"
|
## "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
|
## LAN syncing
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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/<app>/`, the rest of `home/`) is never touched, and a running app from that prefix blocks it (the usual `inUse` guard).
|
- **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/<app>/`, 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.
|
- 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 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`.
|
- 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.
|
- 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.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -252,13 +252,8 @@ function findIconFile(spec, baseDir) {
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
ELF_MAGIC,
|
|
||||||
isElfBuffer,
|
|
||||||
isElfSync,
|
|
||||||
findLinuxExe,
|
findLinuxExe,
|
||||||
parseDesktopEntry,
|
|
||||||
readDesktopEntry,
|
readDesktopEntry,
|
||||||
desktopExecPath,
|
|
||||||
desktopFileFor,
|
desktopFileFor,
|
||||||
findIconFile,
|
findIconFile,
|
||||||
pngOrSvg
|
pngOrSvg
|
||||||
|
|
|
||||||
|
|
@ -450,6 +450,26 @@ function linuxPathFor(file) {
|
||||||
return abs
|
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() {
|
function bwrapSource() {
|
||||||
const devices = ['/dev/shm', '/dev/dri', '/dev/snd', '/dev/input'].filter(d => fs.existsSync(d))
|
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' : ''
|
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') {
|
if (chosen.backend === 'bwrap') {
|
||||||
const source = bwrapSource()
|
const source = bwrapSource()
|
||||||
cmd = 'bwrap'
|
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}`)
|
header.push(`[plwl] HOME=${env.HOME}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -489,12 +509,7 @@ async function runLinuxGame({ game, sandbox, onExit, openFile, handoff = false }
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
DATA_DIRS,
|
|
||||||
XDG_LAYOUT,
|
|
||||||
SYSTEM_DIRS,
|
|
||||||
DESKTOP_SEEDS,
|
DESKTOP_SEEDS,
|
||||||
DATA_SHARES,
|
|
||||||
X11_ENV,
|
|
||||||
SHIM_REL,
|
SHIM_REL,
|
||||||
OPENER_REL,
|
OPENER_REL,
|
||||||
MIME_REL,
|
MIME_REL,
|
||||||
|
|
@ -509,10 +524,10 @@ module.exports = {
|
||||||
ensureLinuxPfx,
|
ensureLinuxPfx,
|
||||||
resetSeededConfig,
|
resetSeededConfig,
|
||||||
dataShares,
|
dataShares,
|
||||||
syncDesktopConfig,
|
|
||||||
buildLinuxEnv,
|
buildLinuxEnv,
|
||||||
pickBackend,
|
pickBackend,
|
||||||
isolationLine,
|
isolationLine,
|
||||||
linuxPathFor,
|
linuxPathFor,
|
||||||
|
handedFileBinds,
|
||||||
runLinuxGame
|
runLinuxGame
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -115,6 +115,7 @@ function buildBwrapArgs({
|
||||||
etcRoot = '',
|
etcRoot = '',
|
||||||
unshareNet = false,
|
unshareNet = false,
|
||||||
appDir = '',
|
appDir = '',
|
||||||
|
openFiles = [],
|
||||||
urlOpener = null
|
urlOpener = null
|
||||||
}) {
|
}) {
|
||||||
const xdgDir = sub => path.join(home, sub)
|
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, 'data'), xdgDir(path.join('.local', 'share')),
|
||||||
'--bind', path.join(dataRoot, 'cache'), xdgDir('.cache'),
|
'--bind', path.join(dataRoot, 'cache'), xdgDir('.cache'),
|
||||||
'--bind', path.join(dataRoot, 'state'), xdgDir(path.join('.local', 'state')),
|
'--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
|
// Wayland, PipeWire, PulseAudio and the session bus all live here
|
||||||
...(runUser ? ['--bind', runUser, runUser] : []),
|
...(runUser ? ['--bind', runUser, runUser] : []),
|
||||||
...(etcRoot ? ['--bind', etcRoot, '/etc'] : []),
|
...(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 }
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue