mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-09-30 17:40:53 -07:00
Merge remote-tracking branch 'origin/main' into add-hachidori-backend
This commit is contained in:
@@ -0,0 +1,4 @@
|
|||||||
|
type: fixed
|
||||||
|
area: linux
|
||||||
|
|
||||||
|
- Fixed the AppImage exiting without opening a window when launched inside a sandbox that mounts the image itself, such as `firejail --appimage` (used by the AppImage catalog test). The detached background app now runs straight from the sandbox's squashfs mount instead of trying to re-mount the AppImage over FUSE.
|
||||||
@@ -8,18 +8,31 @@ import {
|
|||||||
APPIMAGE_MOUNT_KEEPALIVE_LABEL,
|
APPIMAGE_MOUNT_KEEPALIVE_LABEL,
|
||||||
APPIMAGE_MOUNT_KEEPALIVE_SCRIPT,
|
APPIMAGE_MOUNT_KEEPALIVE_SCRIPT,
|
||||||
resolveAppImageMountKeepaliveInvocation,
|
resolveAppImageMountKeepaliveInvocation,
|
||||||
|
resolveMountFsType,
|
||||||
} from './appimage-mount-keepalive';
|
} from './appimage-mount-keepalive';
|
||||||
|
|
||||||
|
const FUSE_EXEC_PATH = '/tmp/.mount_SubMinAb12Cd/SubMiner';
|
||||||
|
const MOUNT_INFO = [
|
||||||
|
'1 0 0:1 / / rw,relatime - btrfs /dev/nvme0n1p2 rw',
|
||||||
|
'773 75 0:95 / /tmp/.mount_SubMinAb12Cd ro,nosuid shared:809 - fuse.SubMiner.AppImage SubMiner.AppImage ro',
|
||||||
|
'1238 1154 7:5 / /run/firejail/appimage ro,nosuid - squashfs /dev/loop5 ro',
|
||||||
|
'900 75 0:99 / /mnt/with\\040space rw - ext4 /dev/sdb1 rw',
|
||||||
|
].join('\n');
|
||||||
|
|
||||||
|
function resolveOnFuse(env: NodeJS.ProcessEnv, platform: NodeJS.Platform) {
|
||||||
|
return resolveAppImageMountKeepaliveInvocation(env, platform, FUSE_EXEC_PATH, () => MOUNT_INFO);
|
||||||
|
}
|
||||||
|
|
||||||
test('resolveAppImageMountKeepaliveInvocation is linux-only', () => {
|
test('resolveAppImageMountKeepaliveInvocation is linux-only', () => {
|
||||||
const env = { APPIMAGE: '/opt/SubMiner.AppImage' };
|
const env = { APPIMAGE: '/opt/SubMiner.AppImage' };
|
||||||
assert.equal(resolveAppImageMountKeepaliveInvocation(env, 'win32'), null);
|
assert.equal(resolveOnFuse(env, 'win32'), null);
|
||||||
assert.equal(resolveAppImageMountKeepaliveInvocation(env, 'darwin'), null);
|
assert.equal(resolveOnFuse(env, 'darwin'), null);
|
||||||
assert.notEqual(resolveAppImageMountKeepaliveInvocation(env, 'linux'), null);
|
assert.notEqual(resolveOnFuse(env, 'linux'), null);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('resolveAppImageMountKeepaliveInvocation requires APPIMAGE env', () => {
|
test('resolveAppImageMountKeepaliveInvocation requires APPIMAGE env', () => {
|
||||||
assert.equal(resolveAppImageMountKeepaliveInvocation({}, 'linux'), null);
|
assert.equal(resolveOnFuse({}, 'linux'), null);
|
||||||
assert.equal(resolveAppImageMountKeepaliveInvocation({ APPIMAGE: ' ' }, 'linux'), null);
|
assert.equal(resolveOnFuse({ APPIMAGE: ' ' }, 'linux'), null);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('resolveAppImageMountKeepaliveInvocation honors disable env', () => {
|
test('resolveAppImageMountKeepaliveInvocation honors disable env', () => {
|
||||||
@@ -27,14 +40,47 @@ test('resolveAppImageMountKeepaliveInvocation honors disable env', () => {
|
|||||||
APPIMAGE: '/opt/SubMiner.AppImage',
|
APPIMAGE: '/opt/SubMiner.AppImage',
|
||||||
SUBMINER_NO_APPIMAGE_MOUNT_KEEPALIVE: '1',
|
SUBMINER_NO_APPIMAGE_MOUNT_KEEPALIVE: '1',
|
||||||
};
|
};
|
||||||
assert.equal(resolveAppImageMountKeepaliveInvocation(env, 'linux'), null);
|
assert.equal(resolveOnFuse(env, 'linux'), null);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('resolveAppImageMountKeepaliveInvocation skips kernel squashfs mounts owned by a sandbox', () => {
|
||||||
|
const env = { APPIMAGE: '/tmp/SubMiner.AppImage' };
|
||||||
|
assert.equal(
|
||||||
|
resolveAppImageMountKeepaliveInvocation(
|
||||||
|
env,
|
||||||
|
'linux',
|
||||||
|
'/run/firejail/appimage/SubMiner',
|
||||||
|
() => MOUNT_INFO,
|
||||||
|
),
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
assert.notEqual(
|
||||||
|
resolveAppImageMountKeepaliveInvocation(
|
||||||
|
env,
|
||||||
|
'linux',
|
||||||
|
'/tmp/appimage_extracted_42c4346b/SubMiner',
|
||||||
|
() => MOUNT_INFO,
|
||||||
|
),
|
||||||
|
null,
|
||||||
|
'extract-and-run directories are deleted with the bootstrap, so they keep the supervisor',
|
||||||
|
);
|
||||||
|
assert.notEqual(
|
||||||
|
resolveAppImageMountKeepaliveInvocation(env, 'linux', FUSE_EXEC_PATH, () => null),
|
||||||
|
null,
|
||||||
|
'unknown mount type keeps the supervisor',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('resolveMountFsType picks the longest containing mount point', () => {
|
||||||
|
assert.equal(resolveMountFsType(FUSE_EXEC_PATH, MOUNT_INFO), 'fuse.SubMiner.AppImage');
|
||||||
|
assert.equal(resolveMountFsType('/run/firejail/appimage/SubMiner', MOUNT_INFO), 'squashfs');
|
||||||
|
assert.equal(resolveMountFsType('/mnt/with space/SubMiner', MOUNT_INFO), 'ext4');
|
||||||
|
assert.equal(resolveMountFsType('/tmp/.mount_SubMinAb12CdX/SubMiner', MOUNT_INFO), 'btrfs');
|
||||||
|
assert.equal(resolveMountFsType('/usr/bin/sh', ''), null);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('resolveAppImageMountKeepaliveInvocation builds sh invocation with AppImage path', () => {
|
test('resolveAppImageMountKeepaliveInvocation builds sh invocation with AppImage path', () => {
|
||||||
const invocation = resolveAppImageMountKeepaliveInvocation(
|
const invocation = resolveOnFuse({ APPIMAGE: '/opt/SubMiner.AppImage' }, 'linux');
|
||||||
{ APPIMAGE: '/opt/SubMiner.AppImage' },
|
|
||||||
'linux',
|
|
||||||
);
|
|
||||||
assert.ok(invocation);
|
assert.ok(invocation);
|
||||||
assert.equal(invocation.command, '/bin/sh');
|
assert.equal(invocation.command, '/bin/sh');
|
||||||
assert.deepEqual(invocation.args, [
|
assert.deepEqual(invocation.args, [
|
||||||
|
|||||||
@@ -8,6 +8,15 @@
|
|||||||
// AppImage via `--appimage-mount` (holder process keeps the mount alive), runs
|
// AppImage via `--appimage-mount` (holder process keeps the mount alive), runs
|
||||||
// AppRun from that mount, and after the app exits waits until no process is still
|
// AppRun from that mount, and after the app exits waits until no process is still
|
||||||
// executing from the mount before releasing the holder.
|
// executing from the mount before releasing the holder.
|
||||||
|
//
|
||||||
|
// A kernel squashfs mount is the exception: whoever mounted it owns its lifetime,
|
||||||
|
// not the runtime. `firejail --appimage` (used by the AppImage catalog test)
|
||||||
|
// loop-mounts the image for the sandbox's lifetime and sets NoNewPrivs, so FUSE
|
||||||
|
// cannot mount there and the supervisor could never start the detached app.
|
||||||
|
// Extract-and-run directories are NOT exempt: the runtime deletes them when the
|
||||||
|
// bootstrap exits, so those still need the supervisor's re-run.
|
||||||
|
|
||||||
|
import fs from 'node:fs';
|
||||||
|
|
||||||
export interface AppImageMountKeepaliveInvocation {
|
export interface AppImageMountKeepaliveInvocation {
|
||||||
command: string;
|
command: string;
|
||||||
@@ -60,14 +69,51 @@ kill "$holder" 2>/dev/null
|
|||||||
exit "$rc"
|
exit "$rc"
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
function unescapeMountInfoPath(value: string): string {
|
||||||
|
return value.replace(/\\([0-7]{3})/g, (_, octal: string) =>
|
||||||
|
String.fromCharCode(parseInt(octal, 8)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filesystem type of the mount containing `filePath`, from /proc/<pid>/mountinfo
|
||||||
|
// text. Null when no mount matches.
|
||||||
|
export function resolveMountFsType(filePath: string, mountInfo: string): string | null {
|
||||||
|
let best: { mountPoint: string; fsType: string } | null = null;
|
||||||
|
for (const line of mountInfo.split('\n')) {
|
||||||
|
const [mountFields, fsFields] = line.split(' - ');
|
||||||
|
const mountPointField = mountFields?.split(' ')[4];
|
||||||
|
const fsType = fsFields?.split(' ')[0];
|
||||||
|
if (!mountPointField || !fsType) continue;
|
||||||
|
const mountPoint = unescapeMountInfoPath(mountPointField);
|
||||||
|
const contains =
|
||||||
|
mountPoint === '/' || filePath === mountPoint || filePath.startsWith(`${mountPoint}/`);
|
||||||
|
if (contains && (!best || mountPoint.length >= best.mountPoint.length)) {
|
||||||
|
best = { mountPoint, fsType };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return best?.fsType ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function readSelfMountInfo(): string | null {
|
||||||
|
try {
|
||||||
|
return fs.readFileSync('/proc/self/mountinfo', 'utf8');
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function resolveAppImageMountKeepaliveInvocation(
|
export function resolveAppImageMountKeepaliveInvocation(
|
||||||
env: NodeJS.ProcessEnv,
|
env: NodeJS.ProcessEnv,
|
||||||
platform: NodeJS.Platform = process.platform,
|
platform: NodeJS.Platform = process.platform,
|
||||||
|
execPath: string = process.execPath,
|
||||||
|
readMountInfo: () => string | null = readSelfMountInfo,
|
||||||
): AppImageMountKeepaliveInvocation | null {
|
): AppImageMountKeepaliveInvocation | null {
|
||||||
if (platform !== 'linux') return null;
|
if (platform !== 'linux') return null;
|
||||||
if (env[DISABLE_ENV] === '1') return null;
|
if (env[DISABLE_ENV] === '1') return null;
|
||||||
const appImagePath = env.APPIMAGE?.trim();
|
const appImagePath = env.APPIMAGE?.trim();
|
||||||
if (!appImagePath) return null;
|
if (!appImagePath) return null;
|
||||||
|
const mountInfo = readMountInfo();
|
||||||
|
if (mountInfo !== null && resolveMountFsType(execPath, mountInfo) === 'squashfs') return null;
|
||||||
return {
|
return {
|
||||||
command: '/bin/sh',
|
command: '/bin/sh',
|
||||||
args: ['-c', APPIMAGE_MOUNT_KEEPALIVE_SCRIPT, APPIMAGE_MOUNT_KEEPALIVE_LABEL, appImagePath],
|
args: ['-c', APPIMAGE_MOUNT_KEEPALIVE_SCRIPT, APPIMAGE_MOUNT_KEEPALIVE_LABEL, appImagePath],
|
||||||
|
|||||||
Reference in New Issue
Block a user