Files
Nikhil 492f3fcdf2 feat(openclaw): prewarm ghcr image in vm (#887)
* feat(openclaw): add gateway image inspection

* feat(openclaw): pull gateway image from registry

* refactor(vm): decouple readiness from image cache

* refactor(openclaw): remove vm cache from runtime factory

* feat(openclaw): detect current gateway image

* feat(openclaw): prewarm vm runtime and reuse current gateway

* feat(openclaw): prewarm runtime on server startup

* refactor(vm): remove browseros image cache runtime

* refactor(build-tools): remove openclaw tarball pipeline

* chore: self-review fixes

* fix(openclaw): suppress prewarm pull progress logs

* fix(openclaw): address review feedback

* fix(openclaw): resolve review findings

* fix(dev): stop stale watch supervisors
2026-04-30 11:18:11 -07:00

274 lines
8.4 KiB
TypeScript

/**
* @license
* Copyright 2025 BrowserOS
* SPDX-License-Identifier: AGPL-3.0-or-later
*
* BrowserOS Server Application
*
* Manages server lifecycle: initialization, startup, and shutdown.
*/
import type { Database } from 'bun:sqlite'
import fs from 'node:fs'
import path from 'node:path'
import { EXIT_CODES } from '@browseros/shared/constants/exit-codes'
import { createHttpServer } from './api/server'
import {
configureOpenClawService,
configureVmRuntime,
getOpenClawService,
} from './api/services/openclaw/openclaw-service'
import { CdpBackend } from './browser/backends/cdp'
import { Browser } from './browser/browser'
import type { ServerConfig } from './config'
import { INLINED_ENV } from './env'
import {
cleanOldSessions,
ensureBrowserosDir,
removeServerConfigSync,
writeServerConfig,
} from './lib/browseros-dir'
import { initializeDb } from './lib/db'
import { identity } from './lib/identity'
import { logger } from './lib/logger'
import { metrics } from './lib/metrics'
import { isPortInUseError } from './lib/port-binding'
import { Sentry } from './lib/sentry'
import { seedSoulTemplate } from './lib/soul'
import { migrateBuiltinSkills } from './skills/migrate'
import {
startSkillSync,
stopSkillSync,
syncBuiltinSkills,
} from './skills/remote-sync'
import { registry } from './tools/registry'
import { VERSION } from './version'
export class Application {
private config: ServerConfig
private db: Database | null = null
constructor(config: ServerConfig) {
this.config = config
}
async start(): Promise<void> {
logger.info(`Starting BrowserOS Server v${VERSION}`)
logger.debug('Directory config', {
executionDir: path.resolve(this.config.executionDir),
resourcesDir: path.resolve(this.config.resourcesDir),
})
const resourcesDir = path.resolve(this.config.resourcesDir)
configureVmRuntime({ resourcesDir })
await this.initCoreServices()
if (!this.config.cdpPort) {
logger.error('CDP port is required (--cdp-port)')
process.exit(EXIT_CODES.GENERAL_ERROR)
}
const cdp = new CdpBackend({ port: this.config.cdpPort })
try {
logger.debug(`Connecting to CDP on port ${this.config.cdpPort}`)
await cdp.connect()
logger.info(`Connected to CDP on port ${this.config.cdpPort}`)
} catch (error) {
return this.handleStartupError('CDP', this.config.cdpPort, error)
}
const browser = new Browser(cdp)
logger.info(`Loaded ${registry.names().length} unified tools`)
try {
await createHttpServer({
port: this.config.serverPort,
host: '0.0.0.0',
version: VERSION,
browser,
registry,
browserosId: identity.getBrowserOSId(),
executionDir: this.config.executionDir,
resourcesDir: this.config.resourcesDir,
codegenServiceUrl: this.config.codegenServiceUrl,
aiSdkDevtoolsEnabled: this.config.aiSdkDevtoolsEnabled,
onShutdown: () => this.stop('shutdown-endpoint'),
})
} catch (error) {
this.handleStartupError('HTTP server', this.config.serverPort, error)
}
try {
await writeServerConfig({
server_port: this.config.serverPort,
url: `http://127.0.0.1:${this.config.serverPort}`,
server_version: VERSION,
browseros_version: this.config.instanceBrowserosVersion,
chromium_version: this.config.instanceChromiumVersion,
browseros_id: identity.getBrowserOSId(),
})
} catch (error) {
logger.warn('Failed to write server config for auto-discovery', {
error: error instanceof Error ? error.message : String(error),
})
}
logger.info(
`HTTP server listening on http://127.0.0.1:${this.config.serverPort}`,
)
logger.info(
`Health endpoint: http://127.0.0.1:${this.config.serverPort}/health`,
)
this.logStartupSummary()
startSkillSync()
// OpenClaw is best-effort — a failure here must not crash the server.
// The container runtime constructor throws synchronously on non-darwin
// (e.g. Linux CI runners), and the .catch() on tryAutoStart() only
// handles async throws inside auto-start. Wrap both in try/catch so the
// process keeps running even when OpenClaw can't initialize at all.
try {
const openClawService = configureOpenClawService({
browserosServerPort: this.config.serverPort,
resourcesDir,
})
void openClawService.prewarm().catch((err) =>
logger.warn('OpenClaw prewarm failed', {
error: err instanceof Error ? err.message : String(err),
}),
)
void openClawService.tryAutoStart().catch((err) =>
logger.warn('OpenClaw auto-start failed', {
error: err instanceof Error ? err.message : String(err),
}),
)
} catch (err) {
logger.warn('OpenClaw configuration failed, continuing without it', {
error: err instanceof Error ? err.message : String(err),
})
}
metrics.log('http_server.started', { version: VERSION })
}
stop(reason?: string): void {
logger.info('Shutting down server...', { reason })
stopSkillSync()
getOpenClawService()
.shutdown()
.catch(() => {})
removeServerConfigSync()
// Immediate exit without graceful shutdown. Chromium may kill us on update/restart,
// and we need to free the port instantly so the HTTP port doesn't keep switching.
// Exit 0 only for managed shutdowns (POST /shutdown from Chromium).
// Signal kills exit non-zero so Chromium's OnProcessExited restarts us.
const code =
reason === 'SIGTERM' || reason === 'SIGINT'
? EXIT_CODES.SIGNAL_KILL
: EXIT_CODES.SUCCESS
process.exit(code)
}
private async initCoreServices(): Promise<void> {
this.configureLogDirectory()
await ensureBrowserosDir()
await cleanOldSessions()
await seedSoulTemplate()
await migrateBuiltinSkills()
await syncBuiltinSkills()
const dbPath = path.join(
this.config.executionDir || this.config.resourcesDir,
'browseros.db',
)
this.db = initializeDb(dbPath)
identity.initialize({
installId: this.config.instanceInstallId,
db: this.db,
})
const browserosId = identity.getBrowserOSId()
logger.info('BrowserOS ID initialized', {
browserosId: browserosId.slice(0, 12),
fromConfig: !!this.config.instanceInstallId,
})
metrics.initialize({
client_id: this.config.instanceClientId,
install_id: this.config.instanceInstallId,
browseros_version: this.config.instanceBrowserosVersion,
chromium_version: this.config.instanceChromiumVersion,
server_version: VERSION,
})
if (!metrics.isEnabled()) {
logger.warn('Metrics disabled: missing POSTHOG_API_KEY')
}
if (!INLINED_ENV.SENTRY_DSN) {
logger.debug('Sentry disabled: missing SENTRY_DSN')
}
Sentry.setUser({ id: browserosId })
Sentry.setContext('browseros', {
client_id: this.config.instanceClientId,
install_id: this.config.instanceInstallId,
browseros_version: this.config.instanceBrowserosVersion,
chromium_version: this.config.instanceChromiumVersion,
server_version: VERSION,
})
}
private configureLogDirectory(): void {
const logDir = this.config.executionDir
const resolvedDir = path.isAbsolute(logDir)
? logDir
: path.resolve(process.cwd(), logDir)
try {
fs.mkdirSync(resolvedDir, { recursive: true })
logger.setLogFile(resolvedDir)
} catch (error) {
console.warn(
`Failed to configure log directory ${resolvedDir}: ${
error instanceof Error ? error.message : String(error)
}`,
)
}
}
private handleStartupError(
serverName: string,
port: number,
error: unknown,
): never {
const errorMsg = error instanceof Error ? error.message : String(error)
logger.error(`Failed to start ${serverName}`, { port, error: errorMsg })
console.error(
`[FATAL] Failed to start ${serverName} on port ${port}: ${errorMsg}`,
)
if (isPortInUseError(error)) {
console.error(
`[FATAL] Port ${port} is already in use. Chromium should try a different port.`,
)
process.exit(EXIT_CODES.PORT_CONFLICT)
}
Sentry.captureException(error)
process.exit(EXIT_CODES.GENERAL_ERROR)
}
private logStartupSummary(): void {
logger.info('')
logger.info('Services running:')
logger.info(` HTTP Server: http://127.0.0.1:${this.config.serverPort}`)
logger.info('')
}
}