refactor(startup): unify dev and cli daemon startup through runner
This commit is contained in:
+1
-1
@@ -12,7 +12,7 @@
|
|||||||
],
|
],
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "./scripts/dev.sh",
|
"dev": "./scripts/dev.sh",
|
||||||
"dev:server": "NODE_ENV=development tsx packages/server/scripts/dev-runner.ts",
|
"dev:server": "NODE_ENV=development tsx packages/server/scripts/daemon-runner.ts --dev",
|
||||||
"dev:app": "npm run start --workspace=@getpaseo/app",
|
"dev:app": "npm run start --workspace=@getpaseo/app",
|
||||||
"dev:website": "npm run dev --workspace=@getpaseo/website",
|
"dev:website": "npm run dev --workspace=@getpaseo/website",
|
||||||
"postinstall": "node scripts/postinstall-patches.mjs",
|
"postinstall": "node scripts/postinstall-patches.mjs",
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
import { spawn } from 'node:child_process'
|
import { spawn, spawnSync } from 'node:child_process'
|
||||||
import { closeSync, existsSync, openSync, readFileSync, rmSync } from 'node:fs'
|
import { closeSync, existsSync, openSync, readFileSync, rmSync } from 'node:fs'
|
||||||
import { createRequire } from 'node:module'
|
import { createRequire } from 'node:module'
|
||||||
import path from 'node:path'
|
import path from 'node:path'
|
||||||
import { loadConfig, resolvePaseoHome } from '@getpaseo/server'
|
import { loadConfig, resolvePaseoHome } from '@getpaseo/server'
|
||||||
import { applySherpaLoaderEnv } from './sherpa-env.js'
|
|
||||||
|
|
||||||
export interface DaemonStartOptions {
|
export interface DaemonStartOptions {
|
||||||
port?: string
|
port?: string
|
||||||
@@ -100,6 +99,22 @@ function buildRunnerArgs(options: DaemonStartOptions): string[] {
|
|||||||
return args
|
return args
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function buildChildEnv(options: DaemonStartOptions): NodeJS.ProcessEnv {
|
||||||
|
const childEnv: NodeJS.ProcessEnv = { ...process.env }
|
||||||
|
if (options.home) {
|
||||||
|
childEnv.PASEO_HOME = options.home
|
||||||
|
}
|
||||||
|
if (options.listen) {
|
||||||
|
childEnv.PASEO_LISTEN = options.listen
|
||||||
|
} else if (options.port) {
|
||||||
|
childEnv.PASEO_LISTEN = `127.0.0.1:${options.port}`
|
||||||
|
}
|
||||||
|
if (options.allowedHosts) {
|
||||||
|
childEnv.PASEO_ALLOWED_HOSTS = options.allowedHosts
|
||||||
|
}
|
||||||
|
return childEnv
|
||||||
|
}
|
||||||
|
|
||||||
function resolveDaemonRunnerEntry(): string {
|
function resolveDaemonRunnerEntry(): string {
|
||||||
const serverExportPath = require.resolve('@getpaseo/server')
|
const serverExportPath = require.resolve('@getpaseo/server')
|
||||||
let currentDir = path.dirname(serverExportPath)
|
let currentDir = path.dirname(serverExportPath)
|
||||||
@@ -280,19 +295,7 @@ export async function startLocalDaemonDetached(
|
|||||||
throw new Error('Cannot use --listen and --port together')
|
throw new Error('Cannot use --listen and --port together')
|
||||||
}
|
}
|
||||||
|
|
||||||
const childEnv: NodeJS.ProcessEnv = { ...process.env }
|
const childEnv = buildChildEnv(options)
|
||||||
applySherpaLoaderEnv(childEnv)
|
|
||||||
if (options.home) {
|
|
||||||
childEnv.PASEO_HOME = options.home
|
|
||||||
}
|
|
||||||
if (options.listen) {
|
|
||||||
childEnv.PASEO_LISTEN = options.listen
|
|
||||||
} else if (options.port) {
|
|
||||||
childEnv.PASEO_LISTEN = `127.0.0.1:${options.port}`
|
|
||||||
}
|
|
||||||
if (options.allowedHosts) {
|
|
||||||
childEnv.PASEO_ALLOWED_HOSTS = options.allowedHosts
|
|
||||||
}
|
|
||||||
|
|
||||||
const paseoHome = resolvePaseoHome(childEnv)
|
const paseoHome = resolvePaseoHome(childEnv)
|
||||||
const logPath = path.join(paseoHome, DAEMON_LOG_FILENAME)
|
const logPath = path.join(paseoHome, DAEMON_LOG_FILENAME)
|
||||||
@@ -358,6 +361,29 @@ export async function startLocalDaemonDetached(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function startLocalDaemonForeground(options: DaemonStartOptions): number {
|
||||||
|
if (options.listen && options.port) {
|
||||||
|
throw new Error('Cannot use --listen and --port together')
|
||||||
|
}
|
||||||
|
|
||||||
|
const childEnv = buildChildEnv(options)
|
||||||
|
const daemonRunnerEntry = resolveDaemonRunnerEntry()
|
||||||
|
const result = spawnSync(
|
||||||
|
process.execPath,
|
||||||
|
[...process.execArgv, daemonRunnerEntry, ...buildRunnerArgs(options)],
|
||||||
|
{
|
||||||
|
env: childEnv,
|
||||||
|
stdio: 'inherit',
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
if (result.error) {
|
||||||
|
throw result.error
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.status ?? 1
|
||||||
|
}
|
||||||
|
|
||||||
export async function stopLocalDaemon(
|
export async function stopLocalDaemon(
|
||||||
options: StopLocalDaemonOptions = {}
|
options: StopLocalDaemonOptions = {}
|
||||||
): Promise<StopLocalDaemonResult> {
|
): Promise<StopLocalDaemonResult> {
|
||||||
|
|||||||
@@ -1,53 +0,0 @@
|
|||||||
import { spawnSync } from 'node:child_process'
|
|
||||||
import {
|
|
||||||
applySherpaLoaderEnv as applySherpaLoaderEnvFromServer,
|
|
||||||
resolveSherpaLoaderEnv,
|
|
||||||
} from '@getpaseo/server'
|
|
||||||
|
|
||||||
const SHERPA_ENV_BOOTSTRAPPED = 'PASEO_SHERPA_ENV_BOOTSTRAPPED'
|
|
||||||
|
|
||||||
export function applySherpaLoaderEnv(env: NodeJS.ProcessEnv): {
|
|
||||||
changed: boolean
|
|
||||||
key: 'LD_LIBRARY_PATH' | 'DYLD_LIBRARY_PATH' | 'PATH' | null
|
|
||||||
libDir: string | null
|
|
||||||
} {
|
|
||||||
const result = applySherpaLoaderEnvFromServer(env)
|
|
||||||
return {
|
|
||||||
changed: result.changed,
|
|
||||||
key: result.key,
|
|
||||||
libDir: result.libDir,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function reexecWithSherpaLoaderEnvIfNeeded(): never | void {
|
|
||||||
const resolved = resolveSherpaLoaderEnv()
|
|
||||||
if (!resolved) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (process.env[SHERPA_ENV_BOOTSTRAPPED] === '1') {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const envCopy: NodeJS.ProcessEnv = { ...process.env }
|
|
||||||
const applied = applySherpaLoaderEnvFromServer(envCopy)
|
|
||||||
const updatedValue = applied.key ? envCopy[applied.key] ?? '' : ''
|
|
||||||
if (applied.key && updatedValue === (process.env[applied.key] ?? '')) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const env: NodeJS.ProcessEnv = {
|
|
||||||
...envCopy,
|
|
||||||
[SHERPA_ENV_BOOTSTRAPPED]: '1',
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = spawnSync(process.execPath, process.argv.slice(1), {
|
|
||||||
env,
|
|
||||||
stdio: 'inherit',
|
|
||||||
})
|
|
||||||
|
|
||||||
if (result.error) {
|
|
||||||
throw result.error
|
|
||||||
}
|
|
||||||
|
|
||||||
process.exit(result.status ?? 1)
|
|
||||||
}
|
|
||||||
@@ -1,18 +1,10 @@
|
|||||||
import { Command } from 'commander'
|
import { Command } from 'commander'
|
||||||
import chalk from 'chalk'
|
import chalk from 'chalk'
|
||||||
import {
|
import {
|
||||||
createPaseoDaemon,
|
startLocalDaemonForeground,
|
||||||
loadConfig,
|
|
||||||
resolvePaseoHome,
|
|
||||||
createRootLogger,
|
|
||||||
loadPersistedConfig,
|
|
||||||
} from '@getpaseo/server'
|
|
||||||
import type { CliConfigOverrides } from '@getpaseo/server'
|
|
||||||
import {
|
|
||||||
startLocalDaemonDetached,
|
startLocalDaemonDetached,
|
||||||
type DaemonStartOptions as StartOptions,
|
type DaemonStartOptions as StartOptions,
|
||||||
} from './local-daemon.js'
|
} from './local-daemon.js'
|
||||||
import { reexecWithSherpaLoaderEnvIfNeeded } from './sherpa-env.js'
|
|
||||||
import { getErrorMessage } from '../../utils/errors.js'
|
import { getErrorMessage } from '../../utils/errors.js'
|
||||||
|
|
||||||
export type { DaemonStartOptions as StartOptions } from './local-daemon.js'
|
export type { DaemonStartOptions as StartOptions } from './local-daemon.js'
|
||||||
@@ -35,34 +27,6 @@ export function startCommand(): Command {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function toCliOverrides(options: StartOptions): CliConfigOverrides {
|
|
||||||
const cliOverrides: CliConfigOverrides = {}
|
|
||||||
|
|
||||||
if (options.listen) {
|
|
||||||
cliOverrides.listen = options.listen
|
|
||||||
} else if (options.port) {
|
|
||||||
cliOverrides.listen = `127.0.0.1:${options.port}`
|
|
||||||
}
|
|
||||||
|
|
||||||
if (options.relay === false) {
|
|
||||||
cliOverrides.relayEnabled = false
|
|
||||||
}
|
|
||||||
|
|
||||||
if (options.allowedHosts) {
|
|
||||||
const raw = options.allowedHosts.trim()
|
|
||||||
cliOverrides.allowedHosts =
|
|
||||||
raw.toLowerCase() === 'true'
|
|
||||||
? true
|
|
||||||
: raw.split(',').map(h => h.trim()).filter(Boolean)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (options.mcp === false) {
|
|
||||||
cliOverrides.mcpEnabled = false
|
|
||||||
}
|
|
||||||
|
|
||||||
return cliOverrides
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function runStart(options: StartOptions): Promise<void> {
|
export async function runStart(options: StartOptions): Promise<void> {
|
||||||
if (options.listen && options.port) {
|
if (options.listen && options.port) {
|
||||||
console.error(chalk.red('Cannot use --listen and --port together'))
|
console.error(chalk.red('Cannot use --listen and --port together'))
|
||||||
@@ -79,65 +43,9 @@ export async function runStart(options: StartOptions): Promise<void> {
|
|||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
reexecWithSherpaLoaderEnvIfNeeded()
|
|
||||||
|
|
||||||
if (options.home) {
|
|
||||||
process.env.PASEO_HOME = options.home
|
|
||||||
}
|
|
||||||
|
|
||||||
let paseoHome: string
|
|
||||||
let logger: ReturnType<typeof createRootLogger>
|
|
||||||
let config: ReturnType<typeof loadConfig>
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
paseoHome = resolvePaseoHome()
|
const status = startLocalDaemonForeground(options)
|
||||||
const persistedConfig = loadPersistedConfig(paseoHome)
|
process.exit(status)
|
||||||
logger = createRootLogger(persistedConfig)
|
|
||||||
config = loadConfig(paseoHome, { cli: toCliOverrides(options) })
|
|
||||||
} catch (err) {
|
|
||||||
exitWithError(getErrorMessage(err))
|
|
||||||
}
|
|
||||||
|
|
||||||
let daemon: Awaited<ReturnType<typeof createPaseoDaemon>>
|
|
||||||
try {
|
|
||||||
daemon = await createPaseoDaemon(config, logger)
|
|
||||||
} catch (err) {
|
|
||||||
const message = getErrorMessage(err)
|
|
||||||
exitWithError(`Failed to initialize daemon: ${message}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
let shuttingDown = false
|
|
||||||
const handleShutdown = async (signal: string) => {
|
|
||||||
if (shuttingDown) {
|
|
||||||
logger.info('Forcing exit...')
|
|
||||||
process.exit(1)
|
|
||||||
}
|
|
||||||
shuttingDown = true
|
|
||||||
logger.info(`${signal} received, shutting down gracefully... (press Ctrl+C again to force exit)`)
|
|
||||||
|
|
||||||
const forceExit = setTimeout(() => {
|
|
||||||
logger.warn('Forcing shutdown - HTTP server didn\'t close in time')
|
|
||||||
process.exit(1)
|
|
||||||
}, 10000)
|
|
||||||
|
|
||||||
try {
|
|
||||||
await daemon.stop()
|
|
||||||
clearTimeout(forceExit)
|
|
||||||
logger.info('Server closed')
|
|
||||||
process.exit(0)
|
|
||||||
} catch (err) {
|
|
||||||
clearTimeout(forceExit)
|
|
||||||
logger.error({ err }, 'Shutdown failed')
|
|
||||||
process.exit(1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
process.on('SIGTERM', () => handleShutdown('SIGTERM'))
|
|
||||||
process.on('SIGINT', () => handleShutdown('SIGINT'))
|
|
||||||
|
|
||||||
try {
|
|
||||||
await daemon.start()
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const message = getErrorMessage(err)
|
const message = getErrorMessage(err)
|
||||||
exitWithError(`Failed to start daemon: ${message}`)
|
exitWithError(`Failed to start daemon: ${message}`)
|
||||||
|
|||||||
@@ -1,6 +1,27 @@
|
|||||||
import { fileURLToPath } from "url";
|
import { fileURLToPath } from "url";
|
||||||
import { existsSync } from "node:fs";
|
import { existsSync } from "node:fs";
|
||||||
import { runSupervisor } from "./supervisor.js";
|
import { runSupervisor } from "./supervisor.js";
|
||||||
|
import { applySherpaLoaderEnv } from "../src/server/speech/providers/local/sherpa/sherpa-runtime-env.js";
|
||||||
|
|
||||||
|
type DaemonRunnerConfig = {
|
||||||
|
devMode: boolean;
|
||||||
|
workerArgs: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
function parseConfig(argv: string[]): DaemonRunnerConfig {
|
||||||
|
let devMode = false;
|
||||||
|
const workerArgs: string[] = [];
|
||||||
|
|
||||||
|
for (const arg of argv) {
|
||||||
|
if (arg === "--dev") {
|
||||||
|
devMode = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
workerArgs.push(arg);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { devMode, workerArgs };
|
||||||
|
}
|
||||||
|
|
||||||
function resolveWorkerEntry(): string {
|
function resolveWorkerEntry(): string {
|
||||||
const candidates = [
|
const candidates = [
|
||||||
@@ -19,18 +40,32 @@ function resolveWorkerEntry(): string {
|
|||||||
return candidates[0];
|
return candidates[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolveWorkerExecArgv(): string[] {
|
function resolveDevWorkerEntry(): string {
|
||||||
const workerEntry = resolveWorkerEntry();
|
const candidate = fileURLToPath(new URL("../src/server/index.ts", import.meta.url));
|
||||||
|
if (!existsSync(candidate)) {
|
||||||
|
throw new Error(`Dev worker entry not found: ${candidate}`);
|
||||||
|
}
|
||||||
|
return candidate;
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveWorkerExecArgv(workerEntry: string): string[] {
|
||||||
return workerEntry.endsWith(".ts") ? ["--import", "tsx"] : [];
|
return workerEntry.endsWith(".ts") ? ["--import", "tsx"] : [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const config = parseConfig(process.argv.slice(2));
|
||||||
|
const workerEntry = config.devMode ? resolveDevWorkerEntry() : resolveWorkerEntry();
|
||||||
|
|
||||||
|
applySherpaLoaderEnv(process.env);
|
||||||
|
|
||||||
runSupervisor({
|
runSupervisor({
|
||||||
name: "DaemonRunner",
|
name: "DaemonRunner",
|
||||||
startupMessage: "Starting daemon worker (IPC restart enabled)",
|
startupMessage: config.devMode
|
||||||
resolveWorkerEntry,
|
? "Starting daemon worker (dev mode, crash restarts enabled)"
|
||||||
workerArgs: process.argv.slice(2),
|
: "Starting daemon worker (IPC restart enabled)",
|
||||||
|
resolveWorkerEntry: () => workerEntry,
|
||||||
|
workerArgs: config.workerArgs,
|
||||||
workerEnv: process.env,
|
workerEnv: process.env,
|
||||||
workerExecArgv: resolveWorkerExecArgv(),
|
workerExecArgv: resolveWorkerExecArgv(workerEntry),
|
||||||
restartOnCrash: false,
|
restartOnCrash: config.devMode,
|
||||||
shutdownReasons: ["cli_shutdown"],
|
shutdownReasons: ["cli_shutdown"],
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user