diff --git a/packages/cli/src/commands/workflow.test.ts b/packages/cli/src/commands/workflow.test.ts index 5fc63173be..ea8f6943d5 100644 --- a/packages/cli/src/commands/workflow.test.ts +++ b/packages/cli/src/commands/workflow.test.ts @@ -2364,6 +2364,7 @@ describe('workflowRunCommand — detach', () => { const execBefore = (executeWorkflow as ReturnType).mock.calls.length; const spawnSpy = spyOn(Bun, 'spawn').mockReturnValue({ + pid: 12345, unref: mock(() => undefined), } as unknown as ReturnType); const savedArgv = process.argv; @@ -2372,11 +2373,15 @@ describe('workflowRunCommand — detach', () => { // Capture call data BEFORE mockRestore() — restoring a spy clears its recorded calls. let spawnCallCount = 0; let spawnCmd: string[] = []; - let spawnOptions: { cwd: string; cmd: string[] } | undefined; + let spawnOptions: + | { cwd: string; cmd: string[]; detached?: boolean; windowsHide?: boolean } + | undefined; try { await workflowRunCommand('/test/path', 'assist', 'hello', { detach: true }); spawnCallCount = spawnSpy.mock.calls.length; - spawnOptions = spawnSpy.mock.calls[0]?.[0] as { cwd: string; cmd: string[] } | undefined; + spawnOptions = spawnSpy.mock.calls[0]?.[0] as + | { cwd: string; cmd: string[]; detached?: boolean; windowsHide?: boolean } + | undefined; spawnCmd = (spawnOptions?.cmd ?? []).slice(); } finally { process.argv = savedArgv; @@ -2384,6 +2389,10 @@ describe('workflowRunCommand — detach', () => { } expect(spawnCallCount).toBe(1); + // The actual Windows fix: the child must be spawned into its own process + // group, or the launching shell's teardown kills it (~1s in). + expect(spawnOptions?.detached).toBe(true); + expect(spawnOptions?.windowsHide).toBe(true); expect(spawnCmd).not.toContain('--detach'); expect(spawnCmd).toContain('--branch'); expect(spawnCmd).toContain('--conversation-id'); @@ -2421,6 +2430,7 @@ describe('workflowRunCommand — detach', () => { }); const spawnSpy = spyOn(Bun, 'spawn').mockReturnValue({ + pid: 12345, unref: mock(() => undefined), } as unknown as ReturnType); const savedArgv = process.argv; @@ -2452,6 +2462,7 @@ describe('workflowRunCommand — detach', () => { throw new Error('no home in test'); }); const spawnSpy = spyOn(Bun, 'spawn').mockReturnValue({ + pid: 12345, unref: mock(() => undefined), } as unknown as ReturnType); const savedArgv = process.argv; @@ -2477,6 +2488,38 @@ describe('workflowRunCommand — detach', () => { expect(parsed).toMatchObject({ ok: true, action: 'run', detached: true, workflow: 'assist' }); expect(typeof parsed.conversationId).toBe('string'); }); + + it('throws (no false success ack) when the detached child fails to spawn', async () => { + const { discoverWorkflowsWithConfig } = await import('@archon/workflows/workflow-discovery'); + const paths = await import('@archon/paths'); + (discoverWorkflowsWithConfig as ReturnType).mockResolvedValueOnce({ + workflows: [makeTestWorkflowWithSource({ name: 'assist', description: 'Help' })], + errors: [], + }); + (paths.getArchonHome as ReturnType).mockImplementationOnce(() => { + throw new Error('no home in test'); + }); + // Node's spawn does not throw synchronously on a bad executable — the only + // synchronous failure signal is an undefined pid. + const spawnSpy = spyOn(Bun, 'spawn').mockReturnValue({ + pid: undefined, + unref: mock(() => undefined), + } as unknown as ReturnType); + const savedArgv = process.argv; + process.argv = ['bun', '/abs/cli.ts', 'workflow', 'run', 'assist', 'hello', '--detach']; + + try { + await expect( + workflowRunCommand('/test/path', 'assist', 'hello', { detach: true }) + ).rejects.toThrow(/Failed to start detached workflow child/); + } finally { + process.argv = savedArgv; + spawnSpy.mockRestore(); + } + // The success ack must never have been printed. + const logged = consoleSpy.mock.calls.map(call => String(call[0])); + expect(logged).not.toContain("Started 'assist' in the background."); + }); }); describe('buildDetachedRunCmd', () => { diff --git a/packages/cli/src/commands/workflow.ts b/packages/cli/src/commands/workflow.ts index e5a82f4d15..cd2d1c4383 100644 --- a/packages/cli/src/commands/workflow.ts +++ b/packages/cli/src/commands/workflow.ts @@ -29,6 +29,7 @@ import { } from '@archon/paths'; import { join } from 'node:path'; import { mkdirSync, openSync, closeSync } from 'node:fs'; +import { spawn } from 'node:child_process'; import { createWorkflowDeps } from '@archon/core/workflows/store-adapter'; import { discoverWorkflowsWithConfig } from '@archon/workflows/workflow-discovery'; import { resolveWorkflowName } from '@archon/workflows/router'; @@ -199,12 +200,34 @@ function spawnDetachedWorkflowRun( } try { - const child = Bun.spawn({ - cmd, + // Node's spawn with `detached: true` puts the child in its own process + // group so it survives the parent's exit. Bun.spawn + unref() does NOT + // detach on Windows — the child was killed ~1s in (at worktree_creating) + // when the launching shell/console tore down. `detached: true` is the + // standard fix, also used by setup.ts's trySpawn(); if a kill-on-close Job + // Object wrapper ever defeats it, a `start /b` breakaway fallback is the + // next step. `windowsHide` keeps the child headless. + const child = spawn(cmd[0], cmd.slice(1), { cwd, env: process.env, stdio: ['ignore', logFd ?? 'ignore', logFd ?? 'ignore'], + detached: true, + windowsHide: true, }); + // Unlike Bun.spawn, Node's spawn does NOT throw synchronously on a bad + // executable or cwd — the failure arrives as an async 'error' event, which + // would crash the CLI as an uncaught exception without this listener. + child.on('error', (error: Error) => { + getLog().error( + { err: error, execPath: cmd[0], conversationId }, + 'cli.detached_run_spawn_failed' + ); + }); + // pid is set synchronously iff the OS-level spawn succeeded (same check as + // setup.ts's trySpawn) — fail fast instead of acking a run that never started. + if (child.pid === undefined) { + throw new Error(`Failed to start detached workflow child (executable: ${cmd[0]})`); + } child.unref(); } finally { // The child inherits its own dup of the log fd; close the parent's copy so a @@ -779,7 +802,13 @@ export async function workflowRunCommand( } else { console.log(`Started '${workflow.name}' in the background.`); console.log('Track it with: archon workflow runs'); - if (logPath) console.log(`Child output: ${logPath}`); + if (logPath) { + console.log(`Child output: ${logPath}`); + } else { + // Log file couldn't be opened — the child runs with its output discarded, + // so if it dies before creating a run record there will be no trail. + console.warn('Warning: could not open a log file — child output will not be captured.'); + } } return; } diff --git a/packages/docs-web/src/content/docs/deployment/windows.md b/packages/docs-web/src/content/docs/deployment/windows.md index 47113344a7..35a0d21f46 100644 --- a/packages/docs-web/src/content/docs/deployment/windows.md +++ b/packages/docs-web/src/content/docs/deployment/windows.md @@ -112,6 +112,10 @@ Do not kill `claude.exe` processes — those are active Claude Code sessions. See also: [Port Conflicts](/reference/troubleshooting/#port-conflicts) in the troubleshooting guide. +## Sleep During Workflow Runs (Native Windows Only) + +While a workflow run is active on native Windows, Archon holds the system awake (via `SetThreadExecutionState`) so Modern Standby cannot freeze the executor mid-run — the display can still turn off, only system sleep is inhibited. This is automatic and best-effort: it releases as soon as the last active run finishes, requires no configuration, and if the OS call is unavailable Archon simply runs without it. + ## Tips - **VS Code Integration**: Install the "Remote - WSL" extension to edit WSL2 files from VS Code diff --git a/packages/workflows/src/executor.test.ts b/packages/workflows/src/executor.test.ts index 2e94e2ce5f..ea88bc61c0 100644 --- a/packages/workflows/src/executor.test.ts +++ b/packages/workflows/src/executor.test.ts @@ -3,7 +3,7 @@ * Covers concurrent-run guards, model/provider resolution, and resume logic * that the inner dag-executor.test.ts cannot reach. */ -import { describe, it, expect, mock, beforeEach } from 'bun:test'; +import { describe, it, expect, mock, beforeEach, spyOn } from 'bun:test'; import { join } from 'path'; // --- Mock logger --- @@ -73,6 +73,7 @@ registerBuiltinProviders(); // --- Import after mocks --- import { executeWorkflow, hydrateResumableRun, resolveProjectPaths } from './executor'; +import { keepAwake } from './utils/keep-awake'; import type { WorkflowDeps, IWorkflowPlatform, WorkflowConfig } from './deps'; import type { IWorkflowStore } from './store'; import type { WorkflowDefinition, WorkflowRun } from './schemas'; @@ -194,6 +195,8 @@ describe('executeWorkflow', () => { ); expect(result.success).toBe(false); expect(result.error).toContain('Database error'); + // Blocked before the execution window — keep-awake must never have fired. + expect(keepAwake.activeCount()).toBe(0); }); it('blocks workflow when another is actively running', async () => { @@ -219,6 +222,61 @@ describe('executeWorkflow', () => { expect(result.error).toContain('already active'); }); + // ----------------------------------------------------------------------- + // Keep-awake pairing (acquire before the run's try, release in its finally) + // ----------------------------------------------------------------------- + + // Safe to spy on the real singleton: off-Windows its native fn is + // undefined, so acquire/release only touch the refcount. + it('acquires and releases keep-awake exactly once on a successful run', async () => { + const acquireSpy = spyOn(keepAwake, 'acquire'); + const releaseSpy = spyOn(keepAwake, 'release'); + try { + const result = await executeWorkflow( + makeDeps(), + makePlatform(), + 'conv-1', + '/tmp', + makeWorkflow(), + 'test message', + 'db-conv-1' + ); + expect(result.workflowRunId).toBe('run-123'); + expect(acquireSpy).toHaveBeenCalledTimes(1); + expect(releaseSpy).toHaveBeenCalledTimes(1); + expect(keepAwake.activeCount()).toBe(0); + } finally { + acquireSpy.mockRestore(); + releaseSpy.mockRestore(); + } + }); + + it('still releases keep-awake when the DAG throws an unhandled error', async () => { + mockExecuteDagWorkflow.mockImplementationOnce(async () => { + throw new Error('DAG exploded'); + }); + const acquireSpy = spyOn(keepAwake, 'acquire'); + const releaseSpy = spyOn(keepAwake, 'release'); + try { + const result = await executeWorkflow( + makeDeps(), + makePlatform(), + 'conv-1', + '/tmp', + makeWorkflow(), + 'test message', + 'db-conv-1' + ); + expect(result.success).toBe(false); + expect(acquireSpy).toHaveBeenCalledTimes(1); + expect(releaseSpy).toHaveBeenCalledTimes(1); + expect(keepAwake.activeCount()).toBe(0); + } finally { + acquireSpy.mockRestore(); + releaseSpy.mockRestore(); + } + }); + it('passes self-id and started_at to the lock query so self is excluded', async () => { // The guard runs AFTER workflowRun is finalized so we always have // a self-ID. Without these args, the dispatch's own row would match diff --git a/packages/workflows/src/executor.ts b/packages/workflows/src/executor.ts index 6d54305970..dcb57bd2eb 100644 --- a/packages/workflows/src/executor.ts +++ b/packages/workflows/src/executor.ts @@ -18,6 +18,7 @@ import { isLoopNode, isApprovalNode, isScriptNode, isBashNode } from './schemas' import { executeDagWorkflow } from './dag-executor'; import { logWorkflowStart, logWorkflowError } from './logger'; import { formatDuration, parseDbTimestamp } from './utils/duration'; +import { keepAwake } from './utils/keep-awake'; import { getWorkflowEventEmitter } from './event-emitter'; import { isRegisteredProvider, getRegisteredProviders } from '@archon/providers'; import { @@ -723,7 +724,14 @@ export async function executeWorkflow( const userProviderEnv = await resolveUserProviderEnvForWorkflow(deps, userId, artifactsDir); config.envVars = { ...config.envVars, ...userProviderEnv }; - // Wrap execution in try-catch to ensure workflow is marked as failed on any error + // Wrap execution in try-catch to ensure workflow is marked as failed on any error. + // + // Hold a Windows keep-awake request for the executing window (see + // utils/keep-awake.ts for the Modern Standby / mid-run-death rationale and + // best-effort semantics). Placed HERE, not at function top, so the + // early-return validation paths above never leak an unpaired acquire; the + // matching release is the first statement of this try's finally. + keepAwake.acquire(); try { getLog().info( { @@ -989,6 +997,10 @@ export async function executeWorkflow( // Return failure result instead of re-throwing return { success: false, workflowRunId: workflowRun.id, error: err.message }; } finally { + // Release the keep-awake request FIRST — before the backstop DB calls that + // may throw — so it always pairs with the acquire above this try, on every + // exit path (success, thrown error, or backstop failure). + keepAwake.release(); // Defensive backstop: if the workflow run is still 'running' after all // normal and exceptional code paths, flip it to 'failed' to prevent zombie // accumulation. Guards against any future code path that exits without diff --git a/packages/workflows/src/utils/keep-awake.test.ts b/packages/workflows/src/utils/keep-awake.test.ts new file mode 100644 index 0000000000..788e01c2e6 --- /dev/null +++ b/packages/workflows/src/utils/keep-awake.test.ts @@ -0,0 +1,127 @@ +import { describe, test, expect } from 'bun:test'; +import { createKeepAwake } from './keep-awake'; + +// Expected flag values the native fn should receive. +const ACQUIRE = 0x80000001; // ES_CONTINUOUS | ES_SYSTEM_REQUIRED, unsigned +const RELEASE = 0x80000000; // ES_CONTINUOUS alone (clears prior flags) + +/** A fake SetThreadExecutionState that records the flags it was called with. */ +function makeFake(returnValue = 1): { fn: (flags: number) => number; calls: number[] } { + const calls: number[] = []; + return { + fn: (flags: number): number => { + calls.push(flags); + return returnValue; + }, + calls, + }; +} + +describe('createKeepAwake (win32)', () => { + test('first acquire fires ES_CONTINUOUS|ES_SYSTEM_REQUIRED; nested acquire does not re-fire', () => { + const fake = makeFake(); + const ka = createKeepAwake(fake.fn, 'win32'); + + ka.acquire(); + expect(fake.calls).toEqual([ACQUIRE]); + expect(ka.activeCount()).toBe(1); + + ka.acquire(); + expect(fake.calls).toEqual([ACQUIRE]); // still exactly one call + expect(ka.activeCount()).toBe(2); + }); + + test('release from 2→1 does not clear; 1→0 clears with ES_CONTINUOUS', () => { + const fake = makeFake(); + const ka = createKeepAwake(fake.fn, 'win32'); + + ka.acquire(); + ka.acquire(); + fake.calls.length = 0; // ignore the single acquire call + + ka.release(); // 2 → 1 + expect(fake.calls).toEqual([]); + expect(ka.activeCount()).toBe(1); + + ka.release(); // 1 → 0 + expect(fake.calls).toEqual([RELEASE]); + expect(ka.activeCount()).toBe(0); + }); + + test('re-acquire after full release fires the native call again', () => { + const fake = makeFake(); + const ka = createKeepAwake(fake.fn, 'win32'); + + ka.acquire(); // 0 → 1: ACQUIRE + ka.release(); // 1 → 0: RELEASE + ka.acquire(); // 0 → 1: ACQUIRE + + expect(fake.calls).toEqual([ACQUIRE, RELEASE, ACQUIRE]); + expect(ka.activeCount()).toBe(1); + }); + + test('unbalanced release at refcount 0 makes no native call and does not throw', () => { + const fake = makeFake(); + const ka = createKeepAwake(fake.fn, 'win32'); + + expect(() => ka.release()).not.toThrow(); + expect(fake.calls).toEqual([]); + expect(ka.activeCount()).toBe(0); + }); + + test('native failure (returns 0) does not throw and still increments refcount', () => { + const fake = makeFake(0); // 0 = API failure signal + const ka = createKeepAwake(fake.fn, 'win32'); + + expect(() => ka.acquire()).not.toThrow(); + expect(fake.calls).toEqual([ACQUIRE]); + expect(ka.activeCount()).toBe(1); // refcount tracked so release stays paired + + ka.release(); // 1 → 0 still fires the clear + expect(fake.calls).toEqual([ACQUIRE, RELEASE]); + expect(ka.activeCount()).toBe(0); + }); + + test('a THROWING native fn never propagates and keeps the refcount paired', () => { + let calls = 0; + const throwing = (): number => { + calls += 1; + throw new Error('FFI call failed'); + }; + const ka = createKeepAwake(throwing, 'win32'); + + expect(() => ka.acquire()).not.toThrow(); + expect(ka.activeCount()).toBe(1); + + expect(() => ka.release()).not.toThrow(); + expect(ka.activeCount()).toBe(0); + + expect(calls).toBe(2); // both the 0→1 and 1→0 transitions attempted the call + }); +}); + +describe('createKeepAwake (disabled: non-win32 or no native fn)', () => { + test('non-win32 platform never calls the native fn but tracks refcount', () => { + const fake = makeFake(); + const ka = createKeepAwake(fake.fn, 'linux'); + + ka.acquire(); + ka.acquire(); + ka.release(); + ka.release(); + + expect(fake.calls).toEqual([]); + expect(ka.activeCount()).toBe(0); + }); + + test('undefined native fn on win32 is a safe no-op with refcount tracking', () => { + const ka = createKeepAwake(undefined, 'win32'); + + expect(() => { + ka.acquire(); + ka.acquire(); + ka.release(); + }).not.toThrow(); + expect(ka.activeCount()).toBe(1); + }); +}); diff --git a/packages/workflows/src/utils/keep-awake.ts b/packages/workflows/src/utils/keep-awake.ts new file mode 100644 index 0000000000..2a4a538a20 --- /dev/null +++ b/packages/workflows/src/utils/keep-awake.ts @@ -0,0 +1,162 @@ +/** + * Keep the system awake for the duration of a workflow run (Windows). + * + * Holds a Windows execution-state request (`SetThreadExecutionState` with + * `ES_CONTINUOUS | ES_SYSTEM_REQUIRED`) while ≥1 workflow run is active, so an + * unattended machine cannot drop into Modern Standby (S0 Low Power Idle) and + * freeze a mid-DAG executor. Root cause + evidence: + * `2026-07-05-archon-mid-run-death-problem-record.md` — a standby-frozen + * executor thaws into bash spawns that exit 66 (`EX_NOINPUT`) with no output, + * collapsing the DAG tail; other casualties present as zombie `running` rows. + * + * Best-effort by design: on non-Windows, or if `bun:ffi` / kernel32 is + * unavailable, every method is a no-op, and the native calls themselves are + * try/catch-guarded. A keep-awake failure must NEVER block or fail a workflow + * run (intentional, documented fallback — the CLAUDE.md fail-fast rule's + * sanctioned exception). + * + * The screen is allowed to turn off — we request `ES_SYSTEM_REQUIRED` only, not + * `ES_DISPLAY_REQUIRED`. The per-thread execution state is cleared by the OS on + * process exit, so a crash mid-run leaks nothing. + */ +import { dlopen, FFIType } from 'bun:ffi'; +import { createLogger } from '@archon/paths'; + +/** `SetThreadExecutionState` flags (winbase.h). */ +const ES_CONTINUOUS = 0x80000000; +const ES_SYSTEM_REQUIRED = 0x00000001; + +/** + * Combined acquire flags, coerced to UNSIGNED. `ES_CONTINUOUS | ES_SYSTEM_REQUIRED` + * evaluates to a negative int32 in JS (the `0x80000000` sign bit is set); `>>> 0` + * reinterprets the bit pattern as the uint32 `0x80000001` the Win32 API expects. + */ +const ACQUIRE_FLAGS = (ES_CONTINUOUS | ES_SYSTEM_REQUIRED) >>> 0; +/** `ES_CONTINUOUS` alone clears all prior flags — this IS the release mechanism, not a bug. */ +const RELEASE_FLAGS = ES_CONTINUOUS >>> 0; + +/** Native `SetThreadExecutionState(flags)` → previous state (0 on failure). */ +type SetExecStateFn = (flags: number) => number; + +export interface KeepAwake { + acquire(): void; + release(): void; + /** Current refcount — exposed for tests. */ + activeCount(): number; +} + +/** + * Lazy-initialized logger. Deferred until first use in the common case; the + * one import-time invocation is `loadNative()`'s catch path (Windows dlopen + * failure), which is acceptable — it's already an error-reporting path. + */ +let cachedLog: ReturnType | undefined; +function getLog(): ReturnType { + if (!cachedLog) cachedLog = createLogger('workflow.keep-awake'); + return cachedLog; +} + +/** + * Create a refcounted keep-awake controller. + * + * Refcounting (not a boolean) is required because a single server process runs + * concurrent workflow runs — the request must be held until the LAST active run + * releases it. + * + * @param setExecState Native `SetThreadExecutionState`, or `undefined` to disable + * (non-Windows / unavailable FFI). When disabled the refcount is still tracked + * so `activeCount()` stays meaningful, but no native call is ever made. + * @param platform Host platform (injectable for tests); native calls fire only + * on `win32`. + */ +export function createKeepAwake( + setExecState: SetExecStateFn | undefined, + platform: NodeJS.Platform = process.platform +): KeepAwake { + // `native` undefined ⇒ every method keeps the refcount but makes no syscall. + const native: SetExecStateFn | undefined = platform === 'win32' ? setExecState : undefined; + let count = 0; + + return { + acquire(): void { + count += 1; + if (!native || count !== 1) return; + // try/catch enforces the module contract: a throwing FFI call here would + // otherwise escape into executeWorkflow BEFORE its try block and leave + // the run row stuck 'running' — the exact zombie class this module fights. + try { + const previous = native(ACQUIRE_FLAGS); + if (previous === 0) { + // API failure: refcount already incremented so release stays paired. + getLog().warn({ flags: ACQUIRE_FLAGS }, 'keepawake.acquire_failed'); + return; + } + getLog().info({ activeCount: count }, 'keepawake.acquire_completed'); + } catch (error) { + getLog().warn({ err: error as Error, flags: ACQUIRE_FLAGS }, 'keepawake.acquire_failed'); + } + }, + release(): void { + if (count === 0) { + // Should never fire (executor pairs acquire/release via try/finally); + // include a stack so an unbalanced call site is findable if it does. + getLog().warn({ stack: new Error().stack }, 'keepawake.release_unbalanced'); + return; + } + count -= 1; + if (!native || count !== 0) return; + // try/catch: a throw from the first statement of the executor's finally + // would replace the run's return value and skip the zombie backstop. + try { + const previous = native(RELEASE_FLAGS); + if (previous === 0) { + // Failed clear leaves ES_SYSTEM_REQUIRED asserted until process exit + // — the host cannot sleep. Nothing more we can do, but never say + // "completed" when the OS said no. + getLog().warn({ flags: RELEASE_FLAGS }, 'keepawake.release_failed'); + return; + } + getLog().info({ activeCount: count }, 'keepawake.release_completed'); + } catch (error) { + getLog().warn({ err: error as Error, flags: RELEASE_FLAGS }, 'keepawake.release_failed'); + } + }, + activeCount(): number { + return count; + }, + }; +} + +/** + * Load the native `SetThreadExecutionState` from kernel32.dll via `bun:ffi`. + * Returns `undefined` off-Windows or on any load failure (best-effort — see + * the module doc above for why). `bun:ffi` itself resolves on all platforms; + * only the `dlopen('kernel32.dll')` call is Windows-only, so it stays behind + * the platform guard. + */ +function loadNative(): SetExecStateFn | undefined { + if (process.platform !== 'win32') return undefined; + try { + const lib = dlopen('kernel32.dll', { + SetThreadExecutionState: { args: [FFIType.u32], returns: FFIType.u32 }, + }); + return (flags: number): number => lib.symbols.SetThreadExecutionState(flags); + } catch (error) { + getLog().warn( + { err: error as Error, errorType: (error as Error).constructor.name }, + 'keepawake.unavailable' + ); + return undefined; + } +} + +/** + * Process-wide keep-awake singleton (see `createKeepAwake` above for the + * refcounting rationale). + * + * Workflow execution never runs in a Worker thread in this codebase, so + * acquire and clear always land on the same (main) thread — a hard requirement + * of the per-thread `SetThreadExecutionState` API. If workflow execution ever + * moves to a Worker, do NOT call these from it. + */ +export const keepAwake: KeepAwake = createKeepAwake(loadNative(), process.platform);