77 lines
3.2 KiB
Markdown
77 lines
3.2 KiB
Markdown
|
|
# Exit status
|
||
|
|
|
||
|
|
**Never call `exit()` with an akerr status. Call `akerr_exit()`.**
|
||
|
|
|
||
|
|
```c
|
||
|
|
void akerr_exit(int status);
|
||
|
|
```
|
||
|
|
|
||
|
|
This applies everywhere you are leaving the process on account of a status, not
|
||
|
|
just in an unhandled-error handler: a CLI's top-level `HANDLE` block, an
|
||
|
|
initialization routine that cannot continue, a `main()` that ends by reporting
|
||
|
|
the status it finished with. One function owns the mapping so that a given
|
||
|
|
status produces the same exit code no matter which of your exits it left by.
|
||
|
|
|
||
|
|
The mapping exists because a process exit status is one byte wide. `exit()`
|
||
|
|
accepts an `int` and the kernel keeps the low 8 bits of it — `_exit()`,
|
||
|
|
`_Exit()`, `quick_exit()` and the raw `exit_group` syscall all behave
|
||
|
|
identically, and even `waitid()`, whose `si_status` is a full `int`, sees the
|
||
|
|
truncated value because the truncation happened before the parent looked. There
|
||
|
|
is no wider `exit()` to reach for.
|
||
|
|
|
||
|
|
That leaves 0 through 255 as the only statuses an exit code can carry, and
|
||
|
|
consumer statuses start at `AKERR_FIRST_CONSUMER_STATUS` (256) — so *no* consumer
|
||
|
|
status can be an exit code:
|
||
|
|
|
||
|
|
```
|
||
|
|
status exit code
|
||
|
|
0 0 (success)
|
||
|
|
1 .. 255 the status
|
||
|
|
negative, or > 255 AKERR_EXIT_STATUS_UNREPRESENTABLE (125)
|
||
|
|
```
|
||
|
|
|
||
|
|
Passing the low byte instead would have made status 256 exit 0 and report
|
||
|
|
success to the shell, and status 300 exit 44 — an unrelated error's code. 125 is
|
||
|
|
the conventional "the tool itself failed" status; 126, 127 and 128+*n* already
|
||
|
|
belong to the shell.
|
||
|
|
|
||
|
|
Status 0 exits 0, because 0 is this library's success status. That is not a hole
|
||
|
|
in the rule that an unhandled error never exits 0: `PROCESS` opens with `case
|
||
|
|
0`, which marks a zero status handled, so a successful context cannot reach
|
||
|
|
`FINISH_NORETURN`'s call to the handler at all.
|
||
|
|
|
||
|
|
Above 255, the exit code tells you the process died of an error, not which one.
|
||
|
|
125 is inside the library's reserved band and so is also some host's `errno`, and
|
||
|
|
every status above 255 collapses onto it. **The stack trace is what identifies
|
||
|
|
the error** — it is printed before the handler runs, carrying the status at full
|
||
|
|
width along with its registered name.
|
||
|
|
|
||
|
|
## Replacing the handler
|
||
|
|
|
||
|
|
After the trace is printed, `FINISH_NORETURN` calls
|
||
|
|
`akerr_handler_unhandled_error`. The default implementation,
|
||
|
|
`akerr_default_handler_unhandled_error()`, hands `errctx->status` to
|
||
|
|
`akerr_exit()` (a NULL context exits 1). Replace it if you need something else
|
||
|
|
to happen first — a core dump, a crash reporter, a flush — and finish by calling
|
||
|
|
`akerr_exit()` so the exit code still means what it means everywhere else:
|
||
|
|
|
||
|
|
```c
|
||
|
|
static void mylib_handler(akerr_ErrorContext *e)
|
||
|
|
{
|
||
|
|
mylib_flush_telemetry();
|
||
|
|
if ( e == NULL ) {
|
||
|
|
akerr_exit(AKERR_API);
|
||
|
|
}
|
||
|
|
akerr_exit(e->status);
|
||
|
|
}
|
||
|
|
|
||
|
|
akerr_handler_unhandled_error = &mylib_handler;
|
||
|
|
```
|
||
|
|
|
||
|
|
`akerr_exit()` is declared `AKERR_NORETURN`, so the compiler knows a handler
|
||
|
|
ending in one of those calls is complete rather than falling off the end.
|
||
|
|
|
||
|
|
Set the handler once, before you start any threads. `tests/err_custom_handler.c`
|
||
|
|
installs one that does not exit at all, which is how the test suite asserts on
|
||
|
|
unhandled errors without dying.
|