mirror of
https://github.com/stablyai/orca.git
synced 2026-10-31 00:57:45 +00:00
fix(linux): direct AppImage serve honours the chosen profile and --supervise (#28227)
* fix(linux): run a direct AppImage serve through the CLI so it keeps its profile and supervisor A direct `Orca.AppImage serve` stayed in the Electron process and skipped the CLI's serve front end, so it ignored ORCA_USER_DATA_PATH and silently dropped --supervise. It now execs into the bundled CLI (same pid, so service managers and signals still reach it), translating a leading --user-data-dir into ORCA_USER_DATA_PATH. Also lists terminal-daemon and reference as CLI commands so a direct binary launch of them reaches the CLI. Fixes BUG-1, BUG-3 and BUG-10 from the real-host Linux test run. * fix(linux): drop desktop switches anywhere in a direct serve launch * fix(linux): keep a direct serve's --no-sandbox for the Electron server the CLI starts --------- Co-authored-by: m4air <m4air@Mac.localdomain>
This commit is contained in:
@@ -38,6 +38,8 @@ export XDG_CONFIG_HOME="$state_dir/config"
|
||||
export XDG_CACHE_HOME="$state_dir/cache"
|
||||
export XDG_RUNTIME_DIR="$state_dir/runtime"
|
||||
export LIBGL_ALWAYS_SOFTWARE=1
|
||||
# Every entrypoint must serve the profile the caller chose, not the default one.
|
||||
export ORCA_USER_DATA_PATH="$state_dir/profile"
|
||||
mkdir -p "$HOME" "$XDG_CONFIG_HOME" "$XDG_CACHE_HOME" "$XDG_RUNTIME_DIR"
|
||||
chmod 700 "$XDG_RUNTIME_DIR"
|
||||
|
||||
@@ -81,6 +83,12 @@ if [[ -z "$ready_line" ]]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ ! -f "$ORCA_USER_DATA_PATH/orca-runtime.json" || -e "$XDG_CONFIG_HOME/orca/orca-runtime.json" ]]; then
|
||||
ls -la "$ORCA_USER_DATA_PATH" "$XDG_CONFIG_HOME" >&2 || true
|
||||
echo "FAIL: serve ignored ORCA_USER_DATA_PATH; its runtime metadata is not in $ORCA_USER_DATA_PATH" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
registered_cli_verified=false
|
||||
if [[ "$entrypoint_kind" == appimage ]]; then
|
||||
registered_cli="$HOME/.local/bin/orca-ide"
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import { existsSync } from 'node:fs'
|
||||
import { dirname, join } from 'node:path'
|
||||
import { runProcessSync } from '@orca/process-host'
|
||||
import { ELECTRON_NO_SANDBOX_REQUEST_ENV } from '../../shared/electron-no-sandbox-request'
|
||||
|
||||
const USER_NAMESPACE_PROBE_TIMEOUT_MS = 2_000
|
||||
|
||||
/** Whether an Electron this CLI starts needs `--no-sandbox`. */
|
||||
export function needsElectronNoSandbox(executable: string): boolean {
|
||||
return (
|
||||
process.env[ELECTRON_NO_SANDBOX_REQUEST_ENV] === '1' ||
|
||||
shouldDisableExtractedAppImageSandbox(executable)
|
||||
)
|
||||
}
|
||||
|
||||
function shouldDisableExtractedAppImageSandbox(executable: string): boolean {
|
||||
if (process.platform !== 'linux' || !existsSync(join(dirname(executable), 'AppRun'))) {
|
||||
return false
|
||||
}
|
||||
// An extracted AppImage has no root-owned setuid sandbox; mirror AppRun's userns fallback.
|
||||
if (process.getuid?.() === 0) {
|
||||
return true
|
||||
}
|
||||
try {
|
||||
return (
|
||||
runProcessSync({
|
||||
program: 'unshare',
|
||||
args: ['-Ur', 'true'],
|
||||
stdio: 'ignore',
|
||||
timeoutMs: USER_NAMESPACE_PROBE_TIMEOUT_MS
|
||||
}).code !== 0
|
||||
)
|
||||
} catch {
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -23,7 +23,7 @@ vi.mock('child_process', () => ({
|
||||
spawnSync: spawnSyncMock
|
||||
}))
|
||||
|
||||
import { launchOrcaApp, serveOrcaApp } from './launch'
|
||||
import { getExecutableAppArgs, launchOrcaApp, serveOrcaApp, stripElectronRunAsNode } from './launch'
|
||||
|
||||
class FakeChildProcess extends EventEmitter {
|
||||
stdout = new EventEmitter()
|
||||
@@ -521,6 +521,16 @@ describe('serveOrcaApp', () => {
|
||||
)
|
||||
})
|
||||
|
||||
it("re-applies a direct launch's --no-sandbox to the Electron it starts, not to terminals", () => {
|
||||
process.env.ORCA_ELECTRON_NO_SANDBOX = '1'
|
||||
try {
|
||||
expect(getExecutableAppArgs('/opt/Orca/orca-ide')).toEqual(['--no-sandbox'])
|
||||
expect(stripElectronRunAsNode(process.env)).not.toHaveProperty('ORCA_ELECTRON_NO_SANDBOX')
|
||||
} finally {
|
||||
delete process.env.ORCA_ELECTRON_NO_SANDBOX
|
||||
}
|
||||
})
|
||||
|
||||
it.each([
|
||||
{ probe: 'exits nonzero', result: { status: 1 }, expectedPrefix: ['--no-sandbox'] },
|
||||
{ probe: 'succeeds', result: { status: 0 }, expectedPrefix: [] },
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { spawn, type SpawnOptions } from 'node:child_process'
|
||||
import { existsSync } from 'node:fs'
|
||||
import { dirname, join, resolve } from 'node:path'
|
||||
import { runProcessSync, spawnProcess } from '@orca/process-host'
|
||||
import { resolve } from 'node:path'
|
||||
import { spawnProcess } from '@orca/process-host'
|
||||
import type { ProcessSpec } from '@orca/process-host/process-spec'
|
||||
import {
|
||||
SERVE_UPDATE_HANDOFF_PATH_ENV,
|
||||
@@ -13,6 +12,8 @@ import {
|
||||
resolveLaunchUserDataPath
|
||||
} from './launch-user-data-path'
|
||||
import { foregroundServeIsolatedProfileSwitches } from '../../shared/electron-isolated-profile-switches'
|
||||
import { ELECTRON_NO_SANDBOX_REQUEST_ENV } from '../../shared/electron-no-sandbox-request'
|
||||
import { needsElectronNoSandbox } from './electron-sandbox-switch'
|
||||
import { getMacAppBundlePath } from './mac-app-update-bundle'
|
||||
import { getPlatformUserDataPath } from './metadata'
|
||||
import { isSameUserDataPath } from '../../shared/serve-user-data-path'
|
||||
@@ -50,7 +51,6 @@ import {
|
||||
type ServeSpawnChild
|
||||
} from './serve-pairing-redaction'
|
||||
|
||||
const USER_NAMESPACE_PROBE_TIMEOUT_MS = 2_000
|
||||
const STARTUP_ABORT_MS = 30_000
|
||||
|
||||
export function launchOrcaApp(): void {
|
||||
@@ -284,34 +284,12 @@ function spawnElectronServe(
|
||||
|
||||
export function getExecutableAppArgs(executable: string): string[] {
|
||||
const args = process.env.ORCA_APP_EXECUTABLE_NEEDS_APP_ROOT === '1' ? [resolveAppRoot()] : []
|
||||
if (shouldDisableExtractedAppImageSandbox(executable)) {
|
||||
if (needsElectronNoSandbox(executable)) {
|
||||
args.push('--no-sandbox')
|
||||
}
|
||||
return args
|
||||
}
|
||||
|
||||
function shouldDisableExtractedAppImageSandbox(executable: string): boolean {
|
||||
if (process.platform !== 'linux' || !existsSync(join(dirname(executable), 'AppRun'))) {
|
||||
return false
|
||||
}
|
||||
// An extracted AppImage has no root-owned setuid sandbox; mirror AppRun's userns fallback.
|
||||
if (process.getuid?.() === 0) {
|
||||
return true
|
||||
}
|
||||
try {
|
||||
return (
|
||||
runProcessSync({
|
||||
program: 'unshare',
|
||||
args: ['-Ur', 'true'],
|
||||
stdio: 'ignore',
|
||||
timeoutMs: USER_NAMESPACE_PROBE_TIMEOUT_MS
|
||||
}).code !== 0
|
||||
)
|
||||
} catch {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
function getExecutableSpawnOptions(executable: string): Pick<SpawnOptions, 'shell'> {
|
||||
return process.platform === 'win32' && /\.(?:cmd|bat)$/i.test(executable) ? { shell: true } : {}
|
||||
}
|
||||
@@ -338,6 +316,8 @@ export function resolveForegroundOrcaExecutable(): string {
|
||||
export function stripElectronRunAsNode(env: NodeJS.ProcessEnv): NodeJS.ProcessEnv {
|
||||
const next = { ...env }
|
||||
delete next.ELECTRON_RUN_AS_NODE
|
||||
// Why: the request is applied as a switch; the server's terminals must not inherit it.
|
||||
delete next[ELECTRON_NO_SANDBOX_REQUEST_ENV]
|
||||
// Why: only the relocated supervisor itself may resolve its app root this way.
|
||||
delete next[SERVE_SUPERVISOR_APP_ROOT_ENV]
|
||||
return next
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { CLI_COMMAND_NAMES } from '../../main/startup/cli-command-names'
|
||||
import { COMMAND_SPECS } from './index'
|
||||
|
||||
describe('CLI launch redirect command names', () => {
|
||||
it('lists every CLI command, so a direct binary launch never boots the desktop app', () => {
|
||||
const roots = new Set(COMMAND_SPECS.map((spec) => spec.path[0]))
|
||||
expect([...roots].filter((root) => !CLI_COMMAND_NAMES.some((name) => name === root))).toEqual(
|
||||
[]
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -50,6 +50,7 @@ export const CLI_COMMAND_NAMES = [
|
||||
'pdf',
|
||||
'profile',
|
||||
'project',
|
||||
'reference',
|
||||
'reload',
|
||||
'repo',
|
||||
'screenshot',
|
||||
@@ -66,6 +67,7 @@ export const CLI_COMMAND_NAMES = [
|
||||
'storage',
|
||||
'tab',
|
||||
'terminal',
|
||||
'terminal-daemon',
|
||||
'type',
|
||||
'uncheck',
|
||||
'upload',
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import { posix, win32 } from 'node:path'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { getCliLaunchArgs, maybeRedirectCliLaunch } from './cli-launch-redirect'
|
||||
import {
|
||||
getCliLaunchArgs,
|
||||
maybeRedirectCliLaunch,
|
||||
type CliLaunchRedirectOptions
|
||||
} from './cli-launch-redirect'
|
||||
|
||||
const COMMAND_NAMES = ['project', 'serve', 'status', 'skills', 'worktree']
|
||||
|
||||
@@ -337,6 +341,119 @@ describe('CLI launch redirect: spawning', () => {
|
||||
expect(run).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('execs a direct serve into the CLI so it keeps its pid and honours profile and supervise', () => {
|
||||
const run = vi.fn<NonNullable<CliLaunchRedirectOptions['run']>>()
|
||||
const execve = vi.fn<NonNullable<CliLaunchRedirectOptions['execve']>>()
|
||||
|
||||
const result = maybeRedirectCliLaunch({
|
||||
argv: [
|
||||
linux.execPath,
|
||||
'--no-sandbox',
|
||||
'--user-data-dir',
|
||||
'/home/u/profiles/b1',
|
||||
'serve',
|
||||
'--supervise',
|
||||
'--port',
|
||||
'6792',
|
||||
'--no-sandbox'
|
||||
],
|
||||
env: { NODE_OPTIONS: '--inspect', APPIMAGE: '/home/u/Orca.AppImage' },
|
||||
platform: 'linux',
|
||||
isPackaged: true,
|
||||
resourcesPath: linux.resourcesPath,
|
||||
execPath: linux.execPath,
|
||||
commandNames: COMMAND_NAMES,
|
||||
exists: () => true,
|
||||
run,
|
||||
execve
|
||||
})
|
||||
|
||||
expect(run).not.toHaveBeenCalled()
|
||||
expect(result).toEqual({ redirected: false })
|
||||
expect(execve).toHaveBeenCalledWith(
|
||||
linux.execPath,
|
||||
[linux.execPath, linux.cliEntryPath, 'serve', '--supervise', '--port', '6792'],
|
||||
expect.objectContaining({
|
||||
ELECTRON_RUN_AS_NODE: '1',
|
||||
ORCA_USER_DATA_PATH: '/home/u/profiles/b1',
|
||||
ORCA_NODE_OPTIONS: '--inspect',
|
||||
APPIMAGE: '/home/u/Orca.AppImage'
|
||||
})
|
||||
)
|
||||
const env = execve.mock.calls[0]?.[2]
|
||||
// Why: the CLI must still start its Electron server with the caller's --no-sandbox.
|
||||
expect(env).toHaveProperty('ORCA_ELECTRON_NO_SANDBOX', '1')
|
||||
expect(env).not.toHaveProperty('ORCA_CLI_LAUNCH_REDIRECTED')
|
||||
expect(env).not.toHaveProperty('NODE_OPTIONS')
|
||||
})
|
||||
|
||||
it('keeps ORCA_USER_DATA_PATH for a direct serve without --user-data-dir', () => {
|
||||
const execve = vi.fn<NonNullable<CliLaunchRedirectOptions['execve']>>()
|
||||
maybeRedirectCliLaunch({
|
||||
argv: [linux.execPath, 'serve', '--no-pairing'],
|
||||
env: { ORCA_USER_DATA_PATH: '/home/u/profiles/b1' },
|
||||
platform: 'linux',
|
||||
isPackaged: true,
|
||||
resourcesPath: linux.resourcesPath,
|
||||
execPath: linux.execPath,
|
||||
commandNames: COMMAND_NAMES,
|
||||
exists: () => true,
|
||||
execve
|
||||
})
|
||||
expect(execve).toHaveBeenCalledWith(
|
||||
linux.execPath,
|
||||
[linux.execPath, linux.cliEntryPath, 'serve', '--no-pairing'],
|
||||
expect.objectContaining({ ORCA_USER_DATA_PATH: '/home/u/profiles/b1' })
|
||||
)
|
||||
expect(execve.mock.calls[0]?.[2]).not.toHaveProperty('ORCA_ELECTRON_NO_SANDBOX')
|
||||
})
|
||||
|
||||
it('serves in-process when this runtime cannot exec or the exec fails', () => {
|
||||
const base = {
|
||||
argv: [linux.execPath, 'serve'],
|
||||
env: {},
|
||||
platform: 'linux' as const,
|
||||
isPackaged: true,
|
||||
resourcesPath: linux.resourcesPath,
|
||||
execPath: linux.execPath,
|
||||
commandNames: COMMAND_NAMES,
|
||||
exists: () => true
|
||||
}
|
||||
expect(maybeRedirectCliLaunch({ ...base, execve: null })).toEqual({ redirected: false })
|
||||
const stderr = vi.spyOn(process.stderr, 'write').mockImplementation(() => true)
|
||||
expect(
|
||||
maybeRedirectCliLaunch({
|
||||
...base,
|
||||
execve: () => {
|
||||
throw new Error('ENOEXEC')
|
||||
}
|
||||
})
|
||||
).toEqual({ redirected: false })
|
||||
expect(stderr).toHaveBeenCalledWith(expect.stringContaining('ENOEXEC'))
|
||||
stderr.mockRestore()
|
||||
})
|
||||
|
||||
it('never execs the --serve child the CLI itself spawns, or serve on other platforms', () => {
|
||||
const execve = vi.fn()
|
||||
for (const [argv, platform] of [
|
||||
[[linux.execPath, '--user-data-dir=/p', '--serve', '--serve-port', '0'], 'linux'],
|
||||
[[linux.execPath, 'serve'], 'darwin']
|
||||
] as const) {
|
||||
maybeRedirectCliLaunch({
|
||||
argv: [...argv],
|
||||
env: {},
|
||||
platform,
|
||||
isPackaged: true,
|
||||
resourcesPath: linux.resourcesPath,
|
||||
execPath: linux.execPath,
|
||||
commandNames: COMMAND_NAMES,
|
||||
exists: () => true,
|
||||
execve
|
||||
})
|
||||
}
|
||||
expect(execve).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('surfaces a spawn failure as a non-zero exit', () => {
|
||||
const result = maybeRedirectCliLaunch({
|
||||
argv: [linux.execPath, 'status'],
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { existsSync } from 'node:fs'
|
||||
import { posix, win32 } from 'node:path'
|
||||
import { posix, resolve, win32 } from 'node:path'
|
||||
import { runProcessSync } from '@orca/process-host'
|
||||
import { CLI_BOOLEAN_FLAGS, findCliCommandIndex } from '../../shared/cli-argument-boundary'
|
||||
import { ELECTRON_NO_SANDBOX_REQUEST_ENV } from '../../shared/electron-no-sandbox-request'
|
||||
import { CLI_COMMAND_NAMES } from './cli-command-names'
|
||||
import { VALUE_TAKING_FLAGS } from './serve-mode-argv'
|
||||
|
||||
@@ -17,6 +18,8 @@ export type CliLaunchRedirectOptions = {
|
||||
commandNames?: readonly string[]
|
||||
exists?: typeof existsSync
|
||||
run?: typeof runProcessSync
|
||||
/** Replaces this process; null when the runtime cannot, which keeps serve in-process. */
|
||||
execve?: ((file: string, args: string[], env: NodeJS.ProcessEnv) => void) | null
|
||||
}
|
||||
|
||||
const CLI_EARLY_EXIT_FLAGS = new Set(['--help', '-h', 'help', '--version', '-v'])
|
||||
@@ -47,6 +50,15 @@ export function maybeRedirectCliLaunch(
|
||||
})
|
||||
|
||||
if (!cliArgs) {
|
||||
const serve = getDirectServeLaunch(argv, {
|
||||
platform,
|
||||
isPackaged,
|
||||
commandNames: options.commandNames ?? CLI_COMMAND_NAMES
|
||||
})
|
||||
const execve = options.execve === undefined ? defaultExecve() : options.execve
|
||||
if (serve && execve && exists(cliEntryPath)) {
|
||||
execDirectServe(execve, execPath, cliEntryPath, serve, env)
|
||||
}
|
||||
return { redirected: false }
|
||||
}
|
||||
if (env[REDIRECT_ATTEMPT_ENV] === '1') {
|
||||
@@ -74,6 +86,76 @@ export function maybeRedirectCliLaunch(
|
||||
}
|
||||
}
|
||||
|
||||
type DirectServeLaunch = { args: string[]; userDataPath: string | null; noSandbox: boolean }
|
||||
|
||||
/**
|
||||
* A direct `<binary> serve` on Linux. It runs through the CLI like `orca-ide serve` does, so it
|
||||
* honours ORCA_USER_DATA_PATH and --supervise; exec keeps the pid, so signals still reach it.
|
||||
*/
|
||||
export function getDirectServeLaunch(
|
||||
argv: string[],
|
||||
options: { platform: NodeJS.Platform; isPackaged: boolean; commandNames: readonly string[] }
|
||||
): DirectServeLaunch | null {
|
||||
if (!options.isPackaged || options.platform !== 'linux') {
|
||||
return null
|
||||
}
|
||||
const args = argv.slice(1)
|
||||
const commandIndex = findCommandIndex(args, options.commandNames)
|
||||
if (commandIndex === -1 || args[commandIndex] !== 'serve') {
|
||||
return null
|
||||
}
|
||||
// Why: the CLI has no --user-data-dir; it reads the profile from ORCA_USER_DATA_PATH.
|
||||
let userDataPath: string | null = null
|
||||
const leading: string[] = []
|
||||
for (let index = 0; index < commandIndex; index += 1) {
|
||||
const arg = args[index]!
|
||||
if (arg.startsWith('--user-data-dir=')) {
|
||||
userDataPath = arg.slice('--user-data-dir='.length)
|
||||
} else if (arg === '--user-data-dir' && index + 1 < commandIndex) {
|
||||
userDataPath = args[++index]!
|
||||
} else {
|
||||
leading.push(arg)
|
||||
}
|
||||
}
|
||||
const rest = [...leading, ...args.slice(commandIndex)]
|
||||
return {
|
||||
// Why everywhere: serve has no such flags, and in-process serve tolerated them after the command.
|
||||
args: stripDesktopFlags(rest, rest.length),
|
||||
userDataPath: userDataPath ? resolve(userDataPath) : null,
|
||||
noSandbox: rest.includes('--no-sandbox')
|
||||
}
|
||||
}
|
||||
|
||||
function execDirectServe(
|
||||
execve: NonNullable<CliLaunchRedirectOptions['execve']>,
|
||||
execPath: string,
|
||||
cliEntryPath: string,
|
||||
serve: DirectServeLaunch,
|
||||
env: NodeJS.ProcessEnv
|
||||
): void {
|
||||
const childEnv = buildElectronRunAsNodeEnv(env)
|
||||
// Why no fence: exec sets ELECTRON_RUN_AS_NODE itself, and the server's terminals inherit this env.
|
||||
delete childEnv[REDIRECT_ATTEMPT_ENV]
|
||||
if (serve.userDataPath) {
|
||||
childEnv.ORCA_USER_DATA_PATH = serve.userDataPath
|
||||
}
|
||||
if (serve.noSandbox) {
|
||||
childEnv[ELECTRON_NO_SANDBOX_REQUEST_ENV] = '1'
|
||||
}
|
||||
try {
|
||||
execve(execPath, [execPath, cliEntryPath, ...serve.args], childEnv)
|
||||
} catch (error) {
|
||||
process.stderr.write(
|
||||
`[serve] Serving in-process; could not start the Orca CLI: ${error instanceof Error ? error.message : String(error)}\n`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
function defaultExecve(): CliLaunchRedirectOptions['execve'] {
|
||||
const execve = process.execve
|
||||
return execve ? (file, args, env) => execve.call(process, file, args, env) : null
|
||||
}
|
||||
|
||||
export function getCliLaunchArgs(
|
||||
argv: string[],
|
||||
cliEntryPath: string,
|
||||
@@ -118,7 +200,7 @@ function getCommandLaunchArgs(
|
||||
const commandIndex = findCommandIndex(args, options.commandNames)
|
||||
const cliArgs = stripDesktopFlags(args, commandIndex)
|
||||
const command = commandIndex === -1 ? null : args[commandIndex]
|
||||
// Keep direct serve in-process so signals reach its full child tree.
|
||||
// Direct serve is exec'd into the CLI instead (getDirectServeLaunch), keeping its pid.
|
||||
if (command && command !== 'serve') {
|
||||
return cliArgs
|
||||
}
|
||||
|
||||
@@ -136,7 +136,7 @@ function initializeMainProcessPreflight(options: MainProcessPreflightOptions): b
|
||||
}
|
||||
// Why: on Windows a CLI launch that lost ELECTRON_RUN_AS_NODE would boot the GUI and exit silently; redirect to node mode before the lock gate below.
|
||||
// The redirect runs before the serve-argv rewrite so it still matches on the launch argv verbatim.
|
||||
// Direct serve stays in-process so its signal handlers own all children.
|
||||
// A direct Linux `serve` is exec'd into the CLI here (same pid); it only continues in-process if exec fails.
|
||||
const cliLaunchRedirect = maybeRedirectCliLaunch({
|
||||
isPackaged: app.isPackaged,
|
||||
resourcesPath: process.resourcesPath,
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
/**
|
||||
* Set on the CLI that a direct `<binary> --no-sandbox serve` execs into: the CLI has no Chromium
|
||||
* switches of its own, so it re-applies the caller's --no-sandbox to every Electron it starts.
|
||||
*/
|
||||
export const ELECTRON_NO_SANDBOX_REQUEST_ENV = 'ORCA_ELECTRON_NO_SANDBOX'
|
||||
Reference in New Issue
Block a user