diff --git a/apps/server/src/api/routes/shutdown.ts b/apps/server/src/api/routes/shutdown.ts new file mode 100644 index 00000000..9a4c7ca3 --- /dev/null +++ b/apps/server/src/api/routes/shutdown.ts @@ -0,0 +1,18 @@ +/** + * @license + * Copyright 2025 BrowserOS + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import { Hono } from 'hono' + +interface ShutdownRouteConfig { + onShutdown: () => void +} + +export function createShutdownRoute(config: ShutdownRouteConfig) { + return new Hono().post('/', (c) => { + setImmediate(config.onShutdown) + return c.json({ status: 'ok' }) + }) +} diff --git a/apps/server/src/api/routes/extension-status.ts b/apps/server/src/api/routes/status.ts similarity index 80% rename from apps/server/src/api/routes/extension-status.ts rename to apps/server/src/api/routes/status.ts index 01a8a89c..84a66403 100644 --- a/apps/server/src/api/routes/extension-status.ts +++ b/apps/server/src/api/routes/status.ts @@ -7,11 +7,11 @@ import { Hono } from 'hono' import type { ControllerContext } from '../../browser/extension/context' -interface ExtensionStatusDeps { +interface StatusDeps { controllerContext: ControllerContext } -export function createExtensionStatusRoute(deps: ExtensionStatusDeps) { +export function createStatusRoute(deps: StatusDeps) { const { controllerContext } = deps return new Hono().get('/', (c) => diff --git a/apps/server/src/api/server.ts b/apps/server/src/api/server.ts index 85018447..75b1f74d 100644 --- a/apps/server/src/api/server.ts +++ b/apps/server/src/api/server.ts @@ -17,13 +17,14 @@ import { HttpAgentError } from '../agent/errors' import { logger } from '../lib/logger' import { bindPortWithRetry } from '../lib/port-binding' import { createChatRoutes } from './routes/chat' -import { createExtensionStatusRoute } from './routes/extension-status' import { createGraphRoutes } from './routes/graph' import { createHealthRoute } from './routes/health' import { createKlavisRoutes } from './routes/klavis' import { createMcpRoutes } from './routes/mcp' import { createProviderRoutes } from './routes/provider' import { createSdkRoutes } from './routes/sdk' +import { createShutdownRoute } from './routes/shutdown' +import { createStatusRoute } from './routes/status' import type { Env, HttpServerConfig } from './types' import { defaultCorsConfig } from './utils/cors' @@ -49,16 +50,17 @@ export async function createHttpServer(config: HttpServerConfig) { allowRemote, } = config - const { healthWatchdog } = config + const { healthWatchdog, onShutdown } = config // DECLARATIVE route composition - chain .route() calls for type inference const app = new Hono() .use('/*', cors(defaultCorsConfig)) .route('/health', createHealthRoute({ watchdog: healthWatchdog })) .route( - '/extension-status', - createExtensionStatusRoute({ controllerContext }), + '/shutdown', + createShutdownRoute({ onShutdown: onShutdown ?? (() => {}) }), ) + .route('/status', createStatusRoute({ controllerContext })) .route('/test-provider', createProviderRoutes()) .route('/klavis', createKlavisRoutes({ browserosId: browserosId || '' })) .route( diff --git a/apps/server/src/api/types.ts b/apps/server/src/api/types.ts index ab70ac9b..d4dc5781 100644 --- a/apps/server/src/api/types.ts +++ b/apps/server/src/api/types.ts @@ -84,6 +84,9 @@ export interface HttpServerConfig { // For health monitoring healthWatchdog?: HealthWatchdog + + // For shutdown route + onShutdown?: () => void } // Graph request schemas diff --git a/apps/server/src/main.ts b/apps/server/src/main.ts index 219a2e8f..59d4edc0 100644 --- a/apps/server/src/main.ts +++ b/apps/server/src/main.ts @@ -93,6 +93,7 @@ export class Application { rateLimiter: new RateLimiter(this.getDb(), dailyRateLimit), codegenServiceUrl: this.config.codegenServiceUrl, healthWatchdog: this.healthWatchdog ?? undefined, + onShutdown: () => this.stop(), }) } catch (error) { this.handleStartupError('HTTP server', this.config.serverPort, error) diff --git a/apps/server/tests/__helpers__/setup.ts b/apps/server/tests/__helpers__/setup.ts index 93965ca4..81b5e7af 100644 --- a/apps/server/tests/__helpers__/setup.ts +++ b/apps/server/tests/__helpers__/setup.ts @@ -44,7 +44,7 @@ const DEFAULT_BINARY_PATH = async function isExtensionConnected(port: number): Promise { try { - const response = await fetch(`http://127.0.0.1:${port}/extension-status`, { + const response = await fetch(`http://127.0.0.1:${port}/status`, { signal: AbortSignal.timeout(1000), }) if (response.ok) { diff --git a/apps/server/tests/server.integration.test.ts b/apps/server/tests/server.integration.test.ts index dc00a4b8..035aa198 100644 --- a/apps/server/tests/server.integration.test.ts +++ b/apps/server/tests/server.integration.test.ts @@ -68,9 +68,9 @@ describe('HTTP Server Integration Tests', () => { }) }) - describe('Extension status endpoint', () => { + describe('Status endpoint', () => { it('reports extension as connected', async () => { - const response = await fetch(`${getBaseUrl()}/extension-status`) + const response = await fetch(`${getBaseUrl()}/status`) assert.strictEqual(response.status, 200) const json = (await response.json()) as { diff --git a/bun.lock b/bun.lock index 7f6a1a8d..3ba7f246 100644 --- a/bun.lock +++ b/bun.lock @@ -1516,7 +1516,7 @@ "chroma-js": ["chroma-js@3.2.0", "", {}, "sha512-os/OippSlX1RlWWr+QDPcGUZs0uoqr32urfxESG9U93lhUfbnlyckte84Q8P1UQY/qth983AS1JONKmLS4T0nw=="], - "chrome-devtools-mcp": ["chrome-devtools-mcp@0.13.0", "", { "bin": { "chrome-devtools-mcp": "build/src/index.js" } }, "sha512-CgotJczVYe6wG2b5cqwNFq7n4VXIM8qfvfutdzVlABPKf0b99b0TDPuRsWrviCthigSHzhpMyFQW03P5Utt1Fg=="], + "chrome-devtools-mcp": ["chrome-devtools-mcp@0.14.0", "", { "bin": { "chrome-devtools-mcp": "build/src/index.js" } }, "sha512-JsnA8tApxOZHAUwduMsGFk0Mc3aQF0MX58fo9LoPxJFkyKdq34QonGPGNG38rWXJVQ2X70eI8wosJbOrXN79dQ=="], "chrome-launcher": ["chrome-launcher@1.2.0", "", { "dependencies": { "@types/node": "*", "escape-string-regexp": "^4.0.0", "is-wsl": "^2.2.0", "lighthouse-logger": "^2.0.1" }, "bin": { "print-chrome-path": "bin/print-chrome-path.cjs" } }, "sha512-JbuGuBNss258bvGil7FT4HKdC3SC2K7UAEUqiPy3ACS3Yxo3hAW6bvFpCu2HsIJLgTqxgEX6BkujvzZfLpUD0Q=="],