Management API (sig_mgmt)
sig_mgmt (include/sig/mgmt/sig_mgmt.h, built by -DSIG_MGMT=ON) is the
supported API for managing a running libsig instance: inspecting services,
configurations and capsules, editing configurations and services, and starting,
stopping and restarting services. It is a set of plain C operations that take
and return sig_value trees and sig_status_t codes. It knows nothing about
any transport or response format (configuration and service definition documents are JSON text, which edits accept and results return parsed). REST is one transport over it
(sig_mgmt_http); a REPL, an ssh text tool or an IPC executable would be
another.
The rule: nothing outside sig_mgmt should mutate a running project. Go
through these operations so every client gets the same validation, the same
locking and the same errors. This is a documented rule, not an enforced one:
the core headers (sig_runtime, sig_project, …) are not hidden.
- Setup
- Results and errors
- Read operations
- Action operations
- Edit operations
- Locking and threads
- Writing a transport
Register the module before anything that depends on it, then find the instance
with sig_mgmt_from_runtime:
sig_runtime_register_module(rt, sig_mgmt_module(), NULL);/* ... register transports after it ... */sig_mgmt *m;sig_mgmt_from_runtime(rt, &m); /* from a module's create hook or later */sig_mgmt_from_runtime returns SIG_ERR_NOT_FOUND if the module is not
registered and SIG_ERR_STATE if it is registered but not yet created. A
SIG_MGMT-only build (no SIG_NET, no SIG_HTTP_SERVER) builds and tests
everything on this page.
Results and errors
Section titled “Results and errors”Every operation returns a sig_status_t.
- On
SIG_OK, an operation that has anout(orout_state) parameter has filled it with an ownedsig_valuetree. The caller releases it withsig_value_dtor. On failure the out value is left untouched (do not dtor it). - A tree owns all its memory: it is a deep copy with no pointers into runtime state, so it stays valid after the call returns and after the runtime stops.
- Lists are sorted by name. Field names in the trees below are part of the contract.
- Configuration documents in results (
config) are parsed trees, not the stored text. Number spelling and key order may differ from the file. - A property value is copied into the tree as it is, whatever its kind (including bytes, maps and lists). Deciding how to show a kind a transport cannot express is the transport’s business.
- Edit operations take an optional
errmsgbuffer (char *errmsg, size_t errmsg_len, may be NULL). It receives a NUL-terminated reason of at mosterrmsg_len - 1bytes, and is empty unless stated below.
| Status | Meaning |
|---|---|
SIG_ERR_INVALID_ARG |
A NULL argument. For edits also: a name or new name the project refuses, a new name of 80 bytes or more, an empty or NULL document (errmsg is document required). For start/restart also: the service could not start because a capsule configuration is invalid (the host log has the detail). |
SIG_ERR_NOT_FOUND |
The object does not exist. This includes any name that could not name one (empty, over 64 characters, or containing a character other than letters, digits, _ and -, such as .. or /). |
SIG_ERR_EXISTS |
Start of a running service; delete of a configuration a service uses (empty errmsg); rename onto a taken name, including its own (errmsg names it); a restart that lost a race to a concurrent start. |
SIG_ERR_PARSE |
The document failed validation. errmsg is the project’s reason for configurations; empty for services (the reason is only in the host log). |
SIG_ERR_STATE |
There is no project (the runtime is not running), or a service vanished before its state could be read, or a start was overtaken by concurrent stops or edits 8 times in a row. |
SIG_ERR_ALLOC |
Out of memory. An edit that succeeded in the project but could not build its result returns this with the edit already applied. |
| anything else | A failure inside the runtime or project, passed through. |
On any failure of an edit, nothing was changed (except the SIG_ERR_ALLOC case
above).
Read operations
Section titled “Read operations”Each reads a snapshot and fills *out with a map or list tree.
| Operation | Result tree |
|---|---|
sig_mgmt_system(m, out) |
{version, uptime_seconds, modules: [name]} |
sig_mgmt_list_services(m, out) |
{services: [{name, state, dirty, instances}]} |
sig_mgmt_get_service(m, name, out) |
{name, state, dirty, instances: [{name, configuration, type, inputs: [name], outputs: [name]}], links: [{from: {instance, terminal}, to: {instance, terminal}}]} |
sig_mgmt_get_instance(m, service, instance, out) |
{name, service, configuration, type, properties: {key: value}} |
sig_mgmt_list_capsules(m, out) |
{capsules: [{name, category?, inputs, outputs, properties: [{name, type, required, default?, min?, max?, choices?, description}]}]} |
sig_mgmt_list_configurations(m, out) |
{configurations: [{name, type}]} |
sig_mgmt_get_configuration(m, name, out) |
{name, type, config: <definition>, used_by: [service]} |
sig_mgmt_get_service_config(m, name, out) |
{name, config: <definition>, dirty} |
state is "running" or "stopped". dirty is true when the stored
definition has been edited since the service was built (see
Editing and the dirty flag).
Fields marked ? are present only when set. uptime_seconds is measured from
mgmt’s own start time.
Errors: SIG_ERR_INVALID_ARG (NULL argument), SIG_ERR_NOT_FOUND (no such
object, or a name that could not name one; not returned by sig_mgmt_system
or sig_mgmt_list_capsules), SIG_ERR_STATE (no project; not returned by
sig_mgmt_system or sig_mgmt_list_capsules, which need no project),
SIG_ERR_ALLOC.
Action operations
Section titled “Action operations”| Operation | Effect |
|---|---|
sig_mgmt_start_service(m, name, out_state) |
Starts a stopped service. |
sig_mgmt_stop_service(m, name, out_state) |
Stops a service. Stopping a stopped service is SIG_OK. |
sig_mgmt_restart_service(m, name, out_state) |
Stop, then start. |
sig_mgmt_request_shutdown(m) |
Asks the runtime to shut down and returns SIG_OK (SIG_ERR_INVALID_ARG for NULL). It does not wait. |
On SIG_OK, *out_state is {name, state}. start, stop and restart block
until the service’s hooks have run. Errors: SIG_ERR_NOT_FOUND,
SIG_ERR_EXISTS (start of a running service), SIG_ERR_STATE,
SIG_ERR_INVALID_ARG (NULL, or an invalid capsule configuration), anything
else the runtime returns.
A start (or the start half of a restart) of a stopped, dirty service first
rebuilds it from its edited definition. The rebuild frees the old service, so
mgmt does it under its write lock, then starts with
sig_runtime_service_start_prebuilt, which returns SIG_ERR_STALE instead of
rebuilding if the service went stale again in between. mgmt then repeats, up to
8 times, and gives up with SIG_ERR_STATE.
Restart is therefore not atomic: a concurrent start can win between its two
halves (SIG_ERR_EXISTS), and if the rebuild or the start fails the service is
left stopped.
Edit operations
Section titled “Edit operations”An edit takes the document as text (doc, doc_len bytes, JSON, exactly as the
project layer’s put and patch functions take it). out receives an owned tree
on SIG_OK.
| Operation | Result tree |
|---|---|
sig_mgmt_put_configuration(m, name, doc, doc_len, &created, out, errmsg, errmsg_len) |
{name, config} |
sig_mgmt_patch_configuration(m, name, doc, doc_len, out, errmsg, errmsg_len) |
{name, config} |
sig_mgmt_delete_configuration(m, name) |
none |
sig_mgmt_rename_configuration(m, name, new_name, out, errmsg, errmsg_len) |
{name: <new name>, config} |
sig_mgmt_put_service(m, name, doc, doc_len, &created, out, errmsg, errmsg_len) |
{name, config, dirty} |
sig_mgmt_patch_service(m, name, doc, doc_len, out, errmsg, errmsg_len) |
{name, config, dirty} |
sig_mgmt_delete_service(m, name) |
none |
put creates or replaces; *created (put only) says whether the object is new.
patch takes a JSON Merge Patch (RFC 7386) against the stored document. Rename takes the new name as a plain
string. Errors are in the table above: put never returns
SIG_ERR_NOT_FOUND (it creates), so only patch, delete and rename do.
Edits change the stored definition and mark affected services dirty. A stopped service is rebuilt at once; a running one keeps its old instances until it is restarted.
Locking and threads
Section titled “Locking and threads”Each sig_mgmt has one reader/writer lock (sig_rwlock).
- Reads take it shared, build the snapshot, and release it. Readers do not block one another.
- Edits (put, patch, delete, rename) take it exclusive, and only while the project is edited. They wait for in-flight readers to finish and readers wait for them. The result tree is built afterwards from text copied out under the lock, so no project pointer outlives it.
- start, stop, restart and shutdown do not hold the lock while the
service’s hooks run (they may block for long). Only the short lookup that
builds
out_statetakes it shared, plus the short exclusive rebuild of a stopped, dirty service described above. - Residual stall: an edit or rebuild that waits on the runtime’s
service-control lock behind a slow start of another service holds the write
lock while it waits, so reads stall until that start returns. This is
inherent in
sig_runtime_edit. - An operation never calls another operation while holding the lock, and the lock is not recursive.
- Never call these operations from a service or capsule thread (a handler,
a job, a start or stop hook): start, stop and restart wait for service threads
to be joined and can deadlock. Call them from host, module or transport
threads. They need the runtime to be running (
SIG_ERR_STATEotherwise).
Writing a transport
Section titled “Writing a transport”A transport is a runtime module that finds sig_mgmt in its create hook and
maps its own framing onto the operations. Register sig_mgmt_module() first.
This compiles against the header:
#include <stdio.h>#include "sig/mgmt/sig_mgmt.h"
static sig_status_t text_create(sig_runtime *rt, const char *config, void **state) { (void)config; sig_mgmt *m; /* the mgmt module must be registered before this one */ sig_status_t st = sig_mgmt_from_runtime(rt, &m); *state = (st == SIG_OK) ? m : NULL; return st;}
/* Called by your transport (a REPL line, an ssh command) on its own thread. */static void text_list_services(sig_mgmt *m) { sig_value out; if (sig_mgmt_list_services(m, &out) != SIG_OK) { puts("error"); return; } const sig_value *list, *svc, *name, *state; const char *n, *s; size_t nl, sl; sig_value_map_get(&out, "services", &list); for (size_t i = 0; sig_value_list_get(list, i, &svc) == SIG_OK; i++) { sig_value_map_get(svc, "name", &name); sig_value_map_get(svc, "state", &state); sig_value_get_string(name, &n, &nl); sig_value_get_string(state, &s, &sl); printf("%s %s\n", n, s); } sig_value_dtor(&out);}
static const sig_runtime_module text_module = {.name = "text", .create = text_create};Register it with sig_runtime_register_module(rt, &text_module, NULL) after
sig_mgmt_module(); the state pointer it keeps is the sig_mgmt * to pass to
each operation. Error handling on the sig_value_* getters is omitted for
brevity. To print a whole tree as JSON use sig_value_to_json
(sig/value/sig_value_json.h), which sorts map keys; the result is a heap
string that the caller releases with sig_free().
What a transport owns, and sig_mgmt does not: parsing requests, choosing how results are
rendered, mapping sig_status_t to its own error vocabulary, and
authorization. sig_mgmt_http is the worked example: see
src/mgmt/sig_mgmt_rest.c and the status table in rest-api.md.