Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 12 additions & 6 deletions docs/SELF_HOSTED_REMOTE_DAEMON.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,13 +62,17 @@ pane --remote-setup --label "VM"
```

On displayless Linux hosts, prefer `runpane install daemon`; the npm and PyPI
wrappers set Electron's required headless environment automatically. For a
direct packaged launch, set it explicitly:
wrappers pass Electron's required headless flags automatically. For a direct
packaged launch, pass them explicitly:

```bash
ELECTRON_OZONE_PLATFORM_HINT=headless pane --remote-setup --label "VM"
pane --ozone-platform=headless --disable-gpu --remote-setup --label "VM"
```

These must be command-line arguments: Electron picks its Ozone platform before
the app starts, so the environment variable `ELECTRON_OZONE_PLATFORM_HINT` no
longer works (removed in Electron 38, ignored from 39 on).

The wrappers are lightweight installers and configurators. They currently
download the packaged Pane runtime because the daemon is not distributed as a
separate binary. AppImage installs therefore require FUSE; on Debian-based
Expand Down Expand Up @@ -247,9 +251,11 @@ the security tradeoff.

### Remote setup exits because X11 or `$DISPLAY` is missing

Use `runpane install daemon`, or prefix a direct packaged launch with
`ELECTRON_OZONE_PLATFORM_HINT=headless`. This failure occurs before Tailscale
setup and does not mean the daemon requires a graphical session.
Use `runpane install daemon`, or add `--ozone-platform=headless --disable-gpu`
to a direct packaged launch. This failure occurs before Tailscale setup and does
not mean the daemon requires a graphical session. If you previously relied on
`ELECTRON_OZONE_PLATFORM_HINT=headless`, switch to the flags: Electron removed
that variable in 38 and ignores it from 39 on.

### The headless daemon starts but remote connect fails

Expand Down
14 changes: 9 additions & 5 deletions main/src/daemon/setupRemoteHost.ts
Original file line number Diff line number Diff line change
Expand Up @@ -573,21 +573,25 @@ function buildHeadlessDaemonCommand(paneDir: string): string {
if (process.platform === 'win32') {
return `cd /d ${quoteForWindows(sourceRoot)} && set "PANE_DIR=${paneDir}" && pnpm daemon:headless -- --pane-dir ${quoteForWindows(paneDir)}`;
}
return `cd ${quoteForPosix(sourceRoot)} && ${buildPosixHeadlessEnvironment(paneDir)} pnpm daemon:headless -- --pane-dir ${quoteForPosix(paneDir)}`;
return `cd ${quoteForPosix(sourceRoot)} && ${buildPosixHeadlessEnvironment(paneDir)} pnpm daemon:headless --${buildLinuxHeadlessFlags()} --pane-dir ${quoteForPosix(paneDir)}`;
}

if (process.platform === 'win32') {
return `${quoteForWindows(process.execPath)} --daemon-headless --pane-dir ${quoteForWindows(paneDir)}`;
}

return `${buildPosixHeadlessEnvironment(paneDir)} ${quoteForPosix(process.execPath)} --daemon-headless --pane-dir ${quoteForPosix(paneDir)}`;
return `${buildPosixHeadlessEnvironment(paneDir)} ${quoteForPosix(process.execPath)}${buildLinuxHeadlessFlags()} --daemon-headless --pane-dir ${quoteForPosix(paneDir)}`;
}

// Ozone selects its platform before the app's main script runs, so this must be
// passed as argv — app.commandLine.appendSwitch() is too late, and
// ELECTRON_OZONE_PLATFORM_HINT was removed in Electron 38 (no-op since 39).
function buildLinuxHeadlessFlags(): string {
return process.platform === 'linux' ? ' --ozone-platform=headless --disable-gpu' : '';
}

function buildPosixHeadlessEnvironment(paneDir: string): string {
const entries = [`PANE_DIR=${quoteForPosix(paneDir)}`];
if (process.platform === 'linux') {
entries.push('ELECTRON_OZONE_PLATFORM_HINT=headless');
}
return entries.join(' ');
}

Expand Down
9 changes: 6 additions & 3 deletions main/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,12 @@ if (process.platform === 'linux') {
app.commandLine.appendSwitch('gtk-version', '3');

if (launchHeadlessDaemon || launchRemoteSetup) {
// Best-effort fallback. Packaged no-display Linux launches need
// ELECTRON_OZONE_PLATFORM_HINT=headless in the parent environment.
app.commandLine.appendSwitch('ozone-platform', 'headless');
// Ozone has already picked its platform by the time this script runs, so
// appending --ozone-platform here cannot select headless: no-display Linux
// launches must pass `--ozone-platform=headless` as argv. (Before Electron
// 38, ELECTRON_OZONE_PLATFORM_HINT=headless did that for us; the variable
// was removed in 38 and is a no-op from 39 on.) --disable-gpu is still read
// late enough to take effect here.
app.commandLine.appendSwitch('disable-gpu');
}
}
Expand Down
15 changes: 14 additions & 1 deletion packages/runpane-py/src/runpane/installers.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,12 +75,25 @@ def install_pane_artifact(

def spawn_pane(executable_path: str, args: List[str]) -> int:
try:
return subprocess.call([executable_path, *args], env=build_pane_daemon_environment())
return subprocess.call(
[executable_path, *build_pane_daemon_args(args)],
env=build_pane_daemon_environment(),
)
except OSError as error:
print(f"Failed to launch Pane: {error}")
return 1


def build_pane_daemon_args(args: List[str], system_name: Optional[str] = None) -> List[str]:
"""Ozone resolves its platform before the app's main script runs, so headless
selection must arrive as argv. ELECTRON_OZONE_PLATFORM_HINT (below) was removed
in Electron 38 and is a no-op from 39 on; it is kept only so older Pane builds
keep working."""
if (system_name or platform.system()).lower() != "linux":
return list(args)
return ["--ozone-platform=headless", "--disable-gpu", *args]


def build_pane_daemon_environment(
system_name: Optional[str] = None,
base_environment: Optional[dict] = None,
Expand Down
14 changes: 13 additions & 1 deletion packages/runpane/src/installers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ export async function installPaneArtifact(

export function spawnPane(executablePath: string, args: string[]): Promise<number> {
return new Promise((resolve) => {
const child = childProcess.spawn(executablePath, args, {
const child = childProcess.spawn(executablePath, buildPaneDaemonArgs(args), {
stdio: 'inherit',
shell: false,
env: buildPaneDaemonEnvironment()
Expand All @@ -72,6 +72,18 @@ export function spawnPane(executablePath: string, args: string[]): Promise<numbe
});
}

// Ozone resolves its platform before the app's main script runs, so headless
// selection must arrive as argv. ELECTRON_OZONE_PLATFORM_HINT (below) was
// removed in Electron 38 and is a no-op from 39 on; it is kept only so older
// Pane builds keep working.
export function buildPaneDaemonArgs(args: string[], platform = process.platform): string[] {
if (platform !== 'linux') {
return [...args];
}

return ['--ozone-platform=headless', '--disable-gpu', ...args];
}

export function buildPaneDaemonEnvironment(
platform = process.platform,
baseEnvironment: NodeJS.ProcessEnv = process.env
Expand Down
2 changes: 1 addition & 1 deletion scripts/test-packaged-headless-remote-setup.sh
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,10 @@ trap 'rm -rf "$pane_dir" "$output_file"' EXIT

set +e
env -u DISPLAY \
ELECTRON_OZONE_PLATFORM_HINT=headless \
"$appimage" \
--appimage-extract-and-run \
--no-sandbox \
--ozone-platform=headless \
--remote-setup \
--label "Headless CI" \
--pane-dir "$pane_dir" \
Expand Down
29 changes: 29 additions & 0 deletions scripts/test-runpane-contract.js
Original file line number Diff line number Diff line change
Expand Up @@ -604,6 +604,34 @@ print(json.dumps({
assert.deepStrictEqual(pythonJson.darwin, baseEnvironment);
}

function compareDaemonLaunchArgsParity() {
const { buildPaneDaemonArgs } = require(path.join(rootDir, 'packages', 'runpane', 'dist', 'installers.js'));
const baseArgs = ['--remote-setup', '--label', 'VM'];

// Ozone reads its platform from argv before the app boots, so the headless
// flags must lead the argument list on Linux and be absent elsewhere.
assert.deepStrictEqual(buildPaneDaemonArgs(baseArgs, 'linux'), [
'--ozone-platform=headless',
'--disable-gpu',
...baseArgs
]);
assert.deepStrictEqual(buildPaneDaemonArgs(baseArgs, 'darwin'), baseArgs);

const pythonOutput = runPythonSnippet(`
import json
from runpane.installers import build_pane_daemon_args

base = ["--remote-setup", "--label", "VM"]
print(json.dumps({
"linux": build_pane_daemon_args(base, "Linux"),
"darwin": build_pane_daemon_args(base, "Darwin"),
}))
`);
const pythonJson = JSON.parse(pythonOutput.split(/\r?\n/).filter(Boolean).pop());
assert.deepStrictEqual(pythonJson.linux, buildPaneDaemonArgs(baseArgs, 'linux'));
assert.deepStrictEqual(pythonJson.darwin, buildPaneDaemonArgs(baseArgs, 'darwin'));
}

function compareRemoteSetupDiagnosticParity() {
const { collectRemoteSetupCheck } = require(path.join(rootDir, 'packages', 'runpane', 'dist', 'doctor.js'));
const probes = {
Expand Down Expand Up @@ -1255,6 +1283,7 @@ async function runChecks() {
compareWrapperTelemetrySanitizers();
compareExistingReusePolicy();
compareDaemonLaunchEnvironmentParity();
compareDaemonLaunchArgsParity();
compareRemoteSetupDiagnosticParity();
checkPlatformMatchingEdgeCases();
await checkExistingDaemonShortCircuit();
Expand Down
Loading