improve native isolation
This commit is contained in:
parent
3f57682a9b
commit
9748b98163
15 changed files with 341 additions and 95 deletions
|
|
@ -199,6 +199,49 @@ async function chmodTree(root) {
|
|||
return { files, dirs }
|
||||
}
|
||||
|
||||
// One file, streamed, with byte progress: copyFile says nothing until a
|
||||
// multi-gigabyte copy is over. The bytes land on a .part name and are renamed
|
||||
// at the end, so nothing can ever read a half-written document.
|
||||
async function copyFileProgress(src, dst, onProgress) {
|
||||
const stat = await fsp.stat(src)
|
||||
if (!stat.isFile()) throw new Error('not a file: ' + src)
|
||||
const gate = onProgress ? progressGate(onProgress) : null
|
||||
await fsp.mkdir(path.dirname(dst), { recursive: true })
|
||||
const part = dst + '.part'
|
||||
let bytes = 0
|
||||
try {
|
||||
await new Promise((resolve, reject) => {
|
||||
const input = fs.createReadStream(src)
|
||||
const output = fs.createWriteStream(part)
|
||||
const fail = err => {
|
||||
input.destroy()
|
||||
output.destroy()
|
||||
reject(err)
|
||||
}
|
||||
input.on('error', fail)
|
||||
output.on('error', fail)
|
||||
input.on('data', chunk => {
|
||||
bytes += chunk.length
|
||||
if (gate) gate({ done: 0, bytes }, false)
|
||||
})
|
||||
output.on('finish', resolve)
|
||||
input.pipe(output)
|
||||
})
|
||||
await fsp.chmod(part, stat.mode & 0o777)
|
||||
await fsp.rename(part, dst)
|
||||
} catch (err) {
|
||||
try {
|
||||
await fsp.rm(part, { force: true })
|
||||
} catch {}
|
||||
throw err
|
||||
}
|
||||
if (gate) {
|
||||
gate({ done: 1, bytes: stat.size }, true)
|
||||
gate.done()
|
||||
}
|
||||
return { files: 1, bytes: stat.size }
|
||||
}
|
||||
|
||||
// Delete deepest-first with progress through onProgress; symlinks are
|
||||
// unlinked, never followed.
|
||||
async function removeTree(root, onProgress) {
|
||||
|
|
@ -266,5 +309,5 @@ async function removeTree(root, onProgress) {
|
|||
return { files, dirs, bytes }
|
||||
}
|
||||
|
||||
module.exports = { pathExists, dedupeDir, copyTree, treeSize, removeTree, chmodTree, progressGate, mapLimit }
|
||||
module.exports = { pathExists, dedupeDir, copyTree, copyFileProgress, treeSize, removeTree, chmodTree, progressGate, mapLimit }
|
||||
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ function sendToWindow(win, channel, data) {
|
|||
// onGamesChanged fires in the main process when the running set changes.
|
||||
const STARTUP_GRACE_MS = 60000
|
||||
|
||||
function createGamesService({ getWindow, onGamesChanged, sandbox }) {
|
||||
function createGamesService({ getWindow, onGamesChanged, sandbox, onHandoffProgress }) {
|
||||
const running = new Map()
|
||||
const startedAt = new Map()
|
||||
const killRequests = new Set()
|
||||
|
|
@ -98,6 +98,15 @@ function createGamesService({ getWindow, onGamesChanged, sandbox }) {
|
|||
return payload => runLinuxGame({ game, sandbox, ...payload })
|
||||
}
|
||||
|
||||
// An isolated hand-off copies the document before the app starts, which is
|
||||
// long enough to be worth reporting. The window may not exist yet (a
|
||||
// shortcut boots headless), so the main process gets the event first and
|
||||
// decides whether to put a window in front of the user.
|
||||
function handoffProgress(data) {
|
||||
if (typeof onHandoffProgress === 'function') onHandoffProgress(data)
|
||||
sendToWindow(getWindow(), 'handoff:progress', data)
|
||||
}
|
||||
|
||||
async function start(win, id, openFile) {
|
||||
const game = library.getGame(id)
|
||||
if (!game) return { ok: false, error: 'game-not-found' }
|
||||
|
|
@ -116,7 +125,7 @@ function createGamesService({ getWindow, onGamesChanged, sandbox }) {
|
|||
// a document into a running app is a handoff, not a tracked run
|
||||
if (!openFile) return { ok: false, error: 'already-running' }
|
||||
try {
|
||||
await run({ openFile, handoff: true })
|
||||
await run({ openFile, handoff: true, onProgress: handoffProgress })
|
||||
return { ok: true, pid: null, logFile: null }
|
||||
} catch (err) {
|
||||
return { ok: false, error: String((err && err.message) || err) }
|
||||
|
|
@ -125,7 +134,7 @@ function createGamesService({ getWindow, onGamesChanged, sandbox }) {
|
|||
running.set(id, { pid: null, prefix: library.prefixNameForGame(game), wine: wine ? wine.id : '' })
|
||||
startedAt.set(id, Date.now())
|
||||
try {
|
||||
const res = await run({ onExit: () => handleExit(id), openFile })
|
||||
const res = await run({ onExit: () => handleExit(id), openFile, onProgress: handoffProgress })
|
||||
// stop requested while still starting: kill and clear
|
||||
if (killRequests.has(id)) {
|
||||
killRequests.delete(id)
|
||||
|
|
|
|||
|
|
@ -51,10 +51,10 @@ function extractGameIcon(id, exePath, kind) {
|
|||
|
||||
// --- registration -----------------------------------------------------------
|
||||
|
||||
function registerIpc({ getWindow, onGamesChanged }) {
|
||||
function registerIpc({ getWindow, onGamesChanged, onHandoffProgress }) {
|
||||
// one probe per launcher, cached: it decides bwrap vs plain run
|
||||
const sandbox = createSandboxProbe()
|
||||
const games = createGamesService({ getWindow, onGamesChanged, sandbox })
|
||||
const games = createGamesService({ getWindow, onGamesChanged, sandbox, onHandoffProgress })
|
||||
|
||||
// sandbox --------------------------------------------------------------------
|
||||
ipcMain.handle('sandbox:status', async () => sandbox.status() || (await sandbox.probe()))
|
||||
|
|
|
|||
|
|
@ -38,6 +38,8 @@ function loadGames() {
|
|||
g[k] = usePrefix && isPrefixRel(g[k]) ? path.join(usePrefix, g[k]) : resolvePortablePath(g[k])
|
||||
}
|
||||
if (g.iconSource) g.iconSource = resolvePortablePath(g.iconSource)
|
||||
// an entry written by an older launcher keeps its meaning
|
||||
if (g.isolation !== undefined) g.isolation = isolationMode(g.isolation)
|
||||
const ps = readPlaySeconds(g.id)
|
||||
if (ps != null) g.playSeconds = ps
|
||||
}
|
||||
|
|
@ -114,17 +116,26 @@ function isLinuxGame(game) {
|
|||
return !!game && game.kind === 'linux'
|
||||
}
|
||||
|
||||
const ISOLATION_MODES = ['auto', 'off', 'require']
|
||||
// none = plain run, relaxed = bwrap + the whole host filesystem mounted rw,
|
||||
// isolated = bwrap + handed documents copied in, auto = relaxed where bwrap
|
||||
// works and none where it does not.
|
||||
const ISOLATION_MODES = ['auto', 'none', 'relaxed', 'isolated']
|
||||
// Modes written by launcher 0.5.x: `off` was the plain run, `require` was
|
||||
// "bwrap or refuse" - which is exactly what the strictest level means now.
|
||||
const LEGACY_ISOLATION_MODES = { off: 'none', require: 'isolated' }
|
||||
|
||||
// One known mode, always: an unknown value would otherwise reach the runner
|
||||
// and pick something nobody asked for.
|
||||
function isolationMode(value) {
|
||||
const mode = LEGACY_ISOLATION_MODES[value] || value
|
||||
return ISOLATION_MODES.includes(mode) ? mode : 'auto'
|
||||
}
|
||||
|
||||
// Only known values reach the registry; anything else falls back to the safe
|
||||
// default (a typo must not silently disable the sandbox).
|
||||
function sanitizeLinuxPatch(patch) {
|
||||
if (!patch) return patch
|
||||
const out = { ...patch }
|
||||
if (out.kind !== undefined) out.kind = out.kind === 'linux' ? 'linux' : 'windows'
|
||||
if (out.isolation !== undefined) {
|
||||
out.isolation = ISOLATION_MODES.includes(out.isolation) ? out.isolation : 'auto'
|
||||
}
|
||||
if (out.isolation !== undefined) out.isolation = isolationMode(out.isolation)
|
||||
if (out.forceX11 !== undefined) out.forceX11 = !!out.forceX11
|
||||
return out
|
||||
}
|
||||
|
|
@ -211,7 +222,7 @@ function newGame({ id, name, exePath, workdir, mode, kind }) {
|
|||
dllOverrides: [],
|
||||
// Proton-only: UMU by default, or the build's steam.exe launcher
|
||||
disableUmu: false,
|
||||
// Linux-only: how hard to try the bwrap sandbox before falling back
|
||||
// Linux-only: none, relaxed, isolated, or auto (relaxed where bwrap works)
|
||||
isolation: 'auto',
|
||||
// Linux-only: force XWayland, where a window can still carry its icon
|
||||
forceX11: false,
|
||||
|
|
|
|||
|
|
@ -13,7 +13,8 @@ const { prefixPath, prefixLinuxRoot } = require('./prefixes')
|
|||
const { prefixNameForGame, loadSettings } = require('./library')
|
||||
const i18n = require('./i18n')
|
||||
const { spawnTracked, parseArgs } = require('./runner')
|
||||
const { buildBwrapArgs, SHARE_ROOT } = require('./sandbox')
|
||||
const { buildBwrapArgs, SHARE_ROOT, FS_ROOT } = require('./sandbox')
|
||||
const { copyFileProgress } = require('./fsutil')
|
||||
|
||||
const DATA_DIRS = ['home', 'config', 'data', 'cache', 'state']
|
||||
|
||||
|
|
@ -424,21 +425,58 @@ function buildLinuxEnv({ game, backend = 'env' }) {
|
|||
return { env, root, home, prefixName, prefixDir: prefixPath(prefixName), routing: urlRouting({ root, home, sandboxed }) }
|
||||
}
|
||||
|
||||
// bwrap when it works, plain run otherwise - unless the app demands a sandbox.
|
||||
function pickBackend({ game, sandbox }) {
|
||||
const policy = (game && game.isolation) || 'auto'
|
||||
const status = sandbox && typeof sandbox.status === 'function' ? sandbox.status() : null
|
||||
const available = !!status && status.backend === 'bwrap'
|
||||
if (policy === 'off') return { ok: true, backend: 'env', policy, status }
|
||||
if (available) return { ok: true, backend: 'bwrap', policy, status }
|
||||
if (policy === 'require') return { ok: false, error: 'sandbox-required', policy, status }
|
||||
return { ok: true, backend: 'env', policy, status }
|
||||
// Where a handed document goes inside the app's own home. The prefix home is
|
||||
// a live bind of a host folder, so a copy written here is visible to an app
|
||||
// that is already running - the one route a running namespace can be reached
|
||||
// through at all.
|
||||
const HANDOFF_DIR = 'plwl-handoff'
|
||||
// Copies above this are worth a popup: below it the copy is over before a
|
||||
// window could paint, and the launcher would flash for nothing.
|
||||
const STAGE_NOTICE_BYTES = 50 * 1000 * 1000
|
||||
|
||||
// The probe measures once and answers from its cache afterwards. It is awaited
|
||||
// here because a shortcut boots HEADLESS: no renderer ran, so nothing else
|
||||
// would have measured, and "auto" would quietly pick the weaker level.
|
||||
async function levelStatus(sandbox) {
|
||||
if (!sandbox || typeof sandbox.status !== 'function') return null
|
||||
const cached = sandbox.status()
|
||||
if (cached) return cached
|
||||
if (typeof sandbox.probe !== 'function') return null
|
||||
try {
|
||||
return await sandbox.probe()
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
function isolationLine({ backend, status }) {
|
||||
if (backend === 'bwrap') return `[plwl] isolation: bwrap ${(status && status.version) || ''}`.trim()
|
||||
const reason = (status && status.reason) || 'missing'
|
||||
return `[plwl] isolation: env-only (reason: ${reason})`
|
||||
// bwrap when it works, plain run otherwise - unless the app demands a sandbox.
|
||||
// `auto` is the only level allowed to take a weaker one: a machine that cannot
|
||||
// sandbox runs the app plain, while an explicit relaxed/isolated refuses
|
||||
// instead of quietly dropping the promise. (Values written by launcher 0.5.x
|
||||
// are mapped to these four by library.js.)
|
||||
async function resolveLevel({ game, sandbox }) {
|
||||
const policy = (game && game.isolation) || 'auto'
|
||||
const status = await levelStatus(sandbox)
|
||||
const available = !!status && status.backend === 'bwrap'
|
||||
if (policy === 'none') return { ok: true, level: 'none', backend: 'env', status }
|
||||
if (policy === 'auto') {
|
||||
return { ok: true, level: available ? 'relaxed' : 'none', backend: available ? 'bwrap' : 'env', status }
|
||||
}
|
||||
if (!available) return { ok: false, error: 'sandbox-required', level: policy, backend: 'env', status }
|
||||
return { ok: true, level: policy, backend: 'bwrap', status }
|
||||
}
|
||||
|
||||
// The line the app log and the UI both quote: a silent downgrade is the one
|
||||
// failure this whole setting exists to avoid.
|
||||
function isolationLine({ level, status }) {
|
||||
const version = (status && status.version) || ''
|
||||
if (level === 'none') {
|
||||
const reason = (status && status.reason) || ''
|
||||
return reason ? `[plwl] isolation: none (no sandbox: ${reason})` : '[plwl] isolation: none (plain run)'
|
||||
}
|
||||
const bwrap = version ? `bwrap ${version}` : 'bwrap'
|
||||
if (level === 'relaxed') return `[plwl] isolation: relaxed (${bwrap}, host filesystem at ${FS_ROOT})`
|
||||
return `[plwl] isolation: isolated (${bwrap})`
|
||||
}
|
||||
|
||||
// "Open with": a native app takes the file where it is (the sandbox sees /).
|
||||
|
|
@ -450,24 +488,39 @@ 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 []
|
||||
// The document a shortcut was handed, in the one shape the chosen level can
|
||||
// reach - and the app is told nothing else, so a second hand-off names the
|
||||
// same file the same way.
|
||||
// none the host path, exactly what a shell would name
|
||||
// relaxed the same file through the whole-host mount every run carries
|
||||
// isolated a copy in the app's own home, refreshed on every open (one copy
|
||||
// per name, so the folder cannot grow; the app's edits stay in the
|
||||
// copy - that is this level's promise)
|
||||
// Returns '' when there is nothing to hand over, so argv stays honest.
|
||||
async function handedPathFor({ openFile, level, root, home, onProgress }) {
|
||||
if (!openFile) return ''
|
||||
const abs = linuxPathFor(openFile)
|
||||
if (level === 'relaxed') return path.join(FS_ROOT, abs)
|
||||
if (level !== 'isolated') return abs
|
||||
|
||||
const name = path.basename(abs)
|
||||
if (!name || name === '.' || name === '..') return ''
|
||||
const stat = fs.statSync(abs)
|
||||
if (!stat.isFile()) return ''
|
||||
const total = stat.size
|
||||
const notice = total >= STAGE_NOTICE_BYTES
|
||||
const emit = (phase, bytes, error) => {
|
||||
if (onProgress) onProgress({ phase, name, bytes: bytes || 0, totalBytes: total, notice, error: error || '' })
|
||||
}
|
||||
if (abs === root || abs.startsWith(root + path.sep)) return []
|
||||
return [abs]
|
||||
emit('copy', 0)
|
||||
try {
|
||||
await copyFileProgress(abs, path.join(root, 'home', HANDOFF_DIR, name), p => emit('copy', p.bytes))
|
||||
} catch (err) {
|
||||
emit('error', 0, String((err && err.message) || err))
|
||||
throw err
|
||||
}
|
||||
emit('done', total)
|
||||
return path.join(home, HANDOFF_DIR, name)
|
||||
}
|
||||
|
||||
function bwrapSource() {
|
||||
|
|
@ -483,16 +536,17 @@ function bwrapSource() {
|
|||
}
|
||||
}
|
||||
|
||||
async function runLinuxGame({ game, sandbox, onExit, openFile, handoff = false }) {
|
||||
const chosen = pickBackend({ game, sandbox })
|
||||
async function runLinuxGame({ game, sandbox, onExit, openFile, handoff = false, onProgress }) {
|
||||
const chosen = await resolveLevel({ game, sandbox })
|
||||
if (!chosen.ok) throw new Error(chosen.error)
|
||||
|
||||
const exe = game.exePath
|
||||
const workdir = game.workdir || path.dirname(exe)
|
||||
const { env, root, home, routing } = buildLinuxEnv({ game, backend: chosen.backend })
|
||||
const argList = parseArgs(game.args)
|
||||
if (openFile) argList.push(linuxPathFor(openFile))
|
||||
const handed = await handedPathFor({ openFile, level: chosen.level, root, home, onProgress })
|
||||
if (handed) argList.push(handed)
|
||||
|
||||
const { env, root, routing } = buildLinuxEnv({ game, backend: chosen.backend })
|
||||
const header = [isolationLine(chosen)]
|
||||
if (game.forceX11) header.push('[plwl] x11 forced (XWayland)')
|
||||
|
||||
|
|
@ -501,7 +555,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), openFiles: handedFileBinds({ openFile, root }), 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), fsRoot: chosen.level === 'relaxed', urlOpener: routing }), '--', exe, ...argList]
|
||||
header.push(`[plwl] HOME=${env.HOME}`)
|
||||
}
|
||||
|
||||
|
|
@ -510,6 +564,8 @@ async function runLinuxGame({ game, sandbox, onExit, openFile, handoff = false }
|
|||
|
||||
module.exports = {
|
||||
DESKTOP_SEEDS,
|
||||
HANDOFF_DIR,
|
||||
STAGE_NOTICE_BYTES,
|
||||
SHIM_REL,
|
||||
OPENER_REL,
|
||||
MIME_REL,
|
||||
|
|
@ -525,9 +581,9 @@ module.exports = {
|
|||
resetSeededConfig,
|
||||
dataShares,
|
||||
buildLinuxEnv,
|
||||
pickBackend,
|
||||
resolveLevel,
|
||||
isolationLine,
|
||||
linuxPathFor,
|
||||
handedFileBinds,
|
||||
handedPathFor,
|
||||
runLinuxGame
|
||||
}
|
||||
|
|
|
|||
11
src/main.js
11
src/main.js
|
|
@ -154,6 +154,15 @@ function loadLangSafe() {
|
|||
}
|
||||
}
|
||||
|
||||
// An isolated hand-off copies the document before the app starts. When that
|
||||
// copy is big enough to be noticed (linuxrunner decides) the user gets a
|
||||
// window with the progress in it - a shortcut boots headless, so without this
|
||||
// a 2 GB document would look like nothing happening at all.
|
||||
function onHandoffProgress(data) {
|
||||
if (!data || !data.notice || data.phase !== 'copy') return
|
||||
if (!win || win.isDestroyed() || !win.isVisible()) showMainWindow()
|
||||
}
|
||||
|
||||
function showMainWindow() {
|
||||
// the user is present now - headless auto-quit must never fire again
|
||||
headless = false
|
||||
|
|
@ -226,7 +235,7 @@ app.whenReady().then(async () => {
|
|||
ensureDirs()
|
||||
await i18n.init()
|
||||
await wines.refreshWines()
|
||||
const { games } = registerIpc({ getWindow, onGamesChanged: rebuildTrayMenu })
|
||||
const { games } = registerIpc({ getWindow, onGamesChanged: rebuildTrayMenu, onHandoffProgress })
|
||||
gamesService = games
|
||||
|
||||
// the tray comes first: nothing may keep it from coming up
|
||||
|
|
|
|||
|
|
@ -15,6 +15,8 @@ function singleSlot(channel) {
|
|||
}
|
||||
|
||||
const onImportProgress = singleSlot('import:progress')
|
||||
// "Open with" into an isolated app: the document is copied first
|
||||
const onHandoffProgress = singleSlot('handoff:progress')
|
||||
// "Open with PLWL": files handed over via --open (Exec %f).
|
||||
const onOpenWith = singleSlot('openwith:file')
|
||||
// one sync (and one pending consent request, enforced server-side) at a time
|
||||
|
|
@ -84,6 +86,7 @@ contextBridge.exposeInMainWorld('plwl', {
|
|||
shellOpenPath: p => ipcRenderer.invoke('shell:openPath', p),
|
||||
shellShowItemInFolder: p => ipcRenderer.invoke('shell:showItemInFolder', p),
|
||||
onImportProgress: onImportProgress,
|
||||
onHandoffProgress: onHandoffProgress,
|
||||
onGamesChanged: cb => ipcRenderer.on('games:changed', () => cb()),
|
||||
lansyncGet: () => ipcRenderer.invoke('lansync:get'),
|
||||
lansyncDevices: () => ipcRenderer.invoke('lansync:devices'),
|
||||
|
|
|
|||
|
|
@ -99,6 +99,14 @@ function createSandboxProbe({ timeoutMs = PROBE_TIMEOUT_MS } = {}) {
|
|||
// prefix is mounted over ~, so a link back into ~ would point at itself.
|
||||
const SHARE_ROOT = '/tmp/.plwl-host'
|
||||
|
||||
// Relaxed isolation hands the app the whole host filesystem as ONE writable
|
||||
// mount. It has to sit under the private /tmp: bubblewrap creates a mount
|
||||
// point itself, and the root of a read-only root cannot be written (measured:
|
||||
// "Can't create file /PLWL_FS_ROOT: Read-only file system"). Being there, one
|
||||
// path string works in every run - which is what a hand-off into an app that
|
||||
// is ALREADY running needs, since a mount cannot be added to a live namespace.
|
||||
const FS_ROOT = '/tmp/plwl-fs-root'
|
||||
|
||||
// The bwrap profile for a native Linux app. Pure: the caller decides which
|
||||
// devices and host folders exist on this machine, so the argv is testable
|
||||
// as-is.
|
||||
|
|
@ -115,7 +123,7 @@ function buildBwrapArgs({
|
|||
etcRoot = '',
|
||||
unshareNet = false,
|
||||
appDir = '',
|
||||
openFiles = [],
|
||||
fsRoot = false,
|
||||
urlOpener = null
|
||||
}) {
|
||||
const xdgDir = sub => path.join(home, sub)
|
||||
|
|
@ -134,6 +142,11 @@ function buildBwrapArgs({
|
|||
'--tmpfs', '/tmp',
|
||||
// X11 sockets live in /tmp, which the tmpfs above just emptied
|
||||
...(x11 ? ['--ro-bind', x11, x11] : []),
|
||||
// relaxed: the host filesystem, whole and writable. Nothing is hidden in
|
||||
// here, so a document handed over later can be opened - and saved - where
|
||||
// the app was told it is. The price is the filesystem promise itself: an
|
||||
// app at this level may write anywhere on the machine.
|
||||
...(fsRoot ? ['--bind', '/', FS_ROOT] : []),
|
||||
// the host's theme data, read-only: fonts, icons, colour schemes
|
||||
...shares.flatMap(dir => ['--ro-bind', dir, path.join(SHARE_ROOT, path.basename(dir))]),
|
||||
// a link clicked in here belongs to the host desktop: xdg-open is replaced
|
||||
|
|
@ -153,10 +166,6 @@ 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'] : []),
|
||||
|
|
@ -169,4 +178,4 @@ function buildBwrapArgs({
|
|||
]
|
||||
}
|
||||
|
||||
module.exports = { createSandboxProbe, buildBwrapArgs, SHARE_ROOT }
|
||||
module.exports = { createSandboxProbe, buildBwrapArgs, SHARE_ROOT, FS_ROOT }
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue