diff --git a/projects/start-os/CHANGELOG.md b/projects/start-os/CHANGELOG.md index d2c1920934..118d596747 100644 --- a/projects/start-os/CHANGELOG.md +++ b/projects/start-os/CHANGELOG.md @@ -18,6 +18,15 @@ file tracks notable changes since the move to the monorepo. image would be installed without complaint. It now verifies whenever `CHECKSUM` is set. +- **A service whose startup routine throws now reports the failure in its own + logs, and StartOS names the failure for what it is.** The container runtime + handed the exception back to StartOS over its socket without also printing + it, so a service that failed to start went quiet in `Logs` at the moment it + needed to speak, while restarting every ten seconds. The exception now + appears in the service's own log next to the procedure that raised it, and + StartOS labels a failure that came from the runtime `Service Runtime Error` + rather than `Unknown Error`. + - **The login banner reports system status for every user, not just the first one to log in.** It staged its database snapshot at a fixed path in `/tmp`, which `pam_motd` created as root at login — so any subsequent non-root run diff --git a/projects/start-os/container-runtime/RPCSpec.md b/projects/start-os/container-runtime/RPCSpec.md index 57ff31348b..beb09ed386 100644 --- a/projects/start-os/container-runtime/RPCSpec.md +++ b/projects/start-os/container-runtime/RPCSpec.md @@ -144,3 +144,33 @@ The `execute` and `sandbox` methods route to procedures based on the `procedure` | `/backup/create` | Create a backup | | `/actions/{name}/getInput` | Get input spec for an action | | `/actions/{name}/run` | Run an action with input | + +## Errors + +A failed call answers with a JSON-RPC error object: + +```ts +{ + code: number, + message: string, // fixed label for the code + data: { + details: string, // what went wrong + debug?: string, // stack trace, when there is one + }, +} +``` + +`code` selects the error; `message` is that code's fixed label and carries no +per-call detail. StartOS reads `code` and renders its own translated label from +it, so anything written into `message` here reaches nobody — the text an +operator reads is `data.details`. + +Codes are either a standard JSON-RPC code or a `start-core` `ErrorKind` +discriminant (`shared-libs/crates/start-core/src/error.rs`): + +| code | message | raised when | +| -------- | ----------------------- | ---------------------------------------- | +| `-32602` | `invalid params` | the request carries no `method` | +| `-32601` | `Method not found` | the `method` is not one of the above | +| `38` | `Invalid Request` | the line is malformed, or dispatch fails | +| `59` | `Service Runtime Error` | a method or procedure threw | diff --git a/projects/start-os/container-runtime/src/Adapters/RpcListener.ts b/projects/start-os/container-runtime/src/Adapters/RpcListener.ts index 4a0f2dbd0a..2679ecfa4e 100644 --- a/projects/start-os/container-runtime/src/Adapters/RpcListener.ts +++ b/projects/start-os/container-runtime/src/Adapters/RpcListener.ts @@ -112,7 +112,13 @@ const isDevBuild = (process.env.STARTOS_ENVIRONMENT ?? '') const jsonParse = (x: string) => JSON.parse(x) -const handleRpc = (id: IdType, result: Promise) => +// codes are start-core ErrorKind discriminants; the OS localizes the message from the code +const errorKind = { + invalidRequest: { code: 38, message: 'Invalid Request' }, + serviceRuntime: { code: 59, message: 'Service Runtime Error' }, +} as const + +const handleRpc = (id: IdType, method: string, result: Promise) => result .then(result => { return { @@ -129,15 +135,17 @@ const handleRpc = (id: IdType, result: Promise) => (x as any).result = null return x }) - .catch(error => ({ - jsonrpc, - id, - error: { - code: 0, - message: typeof error, - data: { details: '' + error, debug: error?.stack }, - }, - })) + .catch(error => { + console.error(`${method} failed`, utils.asError(error)) + return { + jsonrpc, + id, + error: { + ...errorKind.serviceRuntime, + data: { details: '' + error, debug: error?.stack }, + }, + } + }) const hasIdSchema = z.object({ id: idType }) const hasId = (v: unknown): v is z.infer => @@ -181,12 +189,11 @@ export class RpcListener { jsonrpc, id, error: { - message: typeof error, + ...errorKind.invalidRequest, data: { details: error?.message ?? String(error), debug: error?.stack, }, - code: 1, }, }) const writeDataToSocket = (x: SocketResponse) => { @@ -270,7 +277,7 @@ export class RpcListener { const { input: inp, timeout, id: eventId } = params const result = this.getResult(procedure, system, eventId, timeout, inp) - return handleRpc(id, result) + return handleRpc(id, 'execute', result) } case 'sandbox': { const { id, params } = sandboxRunType.parse(input) @@ -279,7 +286,7 @@ export class RpcListener { const { input: inp, timeout, id: eventId } = params const result = this.getResult(procedure, system, eventId, timeout, inp) - return handleRpc(id, result) + return handleRpc(id, 'sandbox', result) } case 'callback': { const { @@ -298,6 +305,7 @@ export class RpcListener { }) return handleRpc( id, + 'start', this.system.start(effects).then(result => ({ result })), ) } @@ -306,6 +314,7 @@ export class RpcListener { this.callbacks?.removeChild('main') return handleRpc( id, + 'stop', this.system.stop().then(result => ({ result })), ) } @@ -313,6 +322,7 @@ export class RpcListener { const { id, params } = exitType.parse(input) return handleRpc( id, + 'exit', (async () => { if (this._system) { let target = null @@ -337,6 +347,7 @@ export class RpcListener { const { id, params } = initType.parse(input) return handleRpc( id, + 'init', (async () => { if (!this._system) { const system = await this.getDependencies.system() @@ -364,6 +375,7 @@ export class RpcListener { const { id, params } = evalType.parse(input) return handleRpc( id, + 'eval', (async () => { const result = await new Function( `return (async () => { return (${params.script}) }).call(this)`, @@ -448,17 +460,16 @@ export class RpcListener { } } })().then(ensureResultTypeShape, error => { - const errorSchema = z.object({ - error: z.string(), - code: z.number().default(0), - }) - const parsed = errorSchema.safeParse(error) - if (parsed.success) { - return { - error: { code: parsed.data.code, message: parsed.data.error }, - } + const legacy = z.object({ error: z.string() }).safeParse(error) + return { + error: { + ...errorKind.serviceRuntime, + data: { + details: legacy.success ? legacy.data.error : String(error), + debug: error?.stack, + }, + }, } - return { error: { code: 0, message: String(error) } } }) } } diff --git a/shared-libs/crates/start-core/locales/i18n.yaml b/shared-libs/crates/start-core/locales/i18n.yaml index a169d8c7ba..8b4baf9137 100644 --- a/shared-libs/crates/start-core/locales/i18n.yaml +++ b/shared-libs/crates/start-core/locales/i18n.yaml @@ -803,12 +803,12 @@ error.lan-port-conflict: fr_FR: "Configuration de Port LAN Incompatible" pl_PL: "Niekompatybilna Konfiguracja Portu LAN" -error.javascript: - en_US: "Javascript Engine Error" - de_DE: "JavaScript-Engine-Fehler" - es_ES: "Error del Motor JavaScript" - fr_FR: "Erreur du Moteur JavaScript" - pl_PL: "Błąd Silnika JavaScript" +error.service-runtime: + en_US: "Service Runtime Error" + de_DE: "Dienstlaufzeitfehler" + es_ES: "Error del Entorno de Ejecución del Servicio" + fr_FR: "Erreur du Runtime de Service" + pl_PL: "Błąd Środowiska Wykonawczego Usługi" error.pem: en_US: "PEM Encoding Error" diff --git a/shared-libs/crates/start-core/src/error.rs b/shared-libs/crates/start-core/src/error.rs index d225247e7a..a75650624d 100644 --- a/shared-libs/crates/start-core/src/error.rs +++ b/shared-libs/crates/start-core/src/error.rs @@ -80,7 +80,7 @@ pub enum ErrorKind { InvalidBackupTargetId = 56, ProductKeyMismatch = 57, LanPortConflict = 58, - Javascript = 59, + ServiceRuntime = 59, Pem = 60, TLSInit = 61, Ascii = 62, @@ -166,7 +166,7 @@ impl ErrorKind { InvalidBackupTargetId => t!("error.invalid-backup-target-id"), ProductKeyMismatch => t!("error.product-key-mismatch"), LanPortConflict => t!("error.lan-port-conflict"), - Javascript => t!("error.javascript"), + ServiceRuntime => t!("error.service-runtime"), Pem => t!("error.pem"), TLSInit => t!("error.tls-init"), Ascii => t!("error.ascii"), diff --git a/shared-libs/crates/start-core/src/s9pk/v2/pack.rs b/shared-libs/crates/start-core/src/s9pk/v2/pack.rs index dced09101b..40c9612d6b 100644 --- a/shared-libs/crates/start-core/src/s9pk/v2/pack.rs +++ b/shared-libs/crates/start-core/src/s9pk/v2/pack.rs @@ -665,7 +665,7 @@ pub async fn pack(ctx: CliContext, params: PackParams) -> Result<(), Error> { "console.log(JSON.stringify(require('{}/index.js').manifest))", js_dir.display() )) - .invoke(ErrorKind::Javascript) + .invoke(ErrorKind::ServiceRuntime) .await? .into(); files.insert( @@ -893,7 +893,7 @@ pub async fn list_ingredients(_: CliContext, params: PackParams) -> Result