Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

System GClasses

Core runtime, file-system watcher, pseudo-terminal, OTA updates, and miscellaneous system services.

Source: kernel/c/root-linux/src/c_yuno.c, c_fs.c, c_pty.c, c_ota.c, c_gss_udp_s.c


C_YUNO

Main yuno GClass — the root grandmother of every yuno application. Manages services, configuration, logging, tracing, IP filtering, and system-wide commands.

PropertyValue
StatesST_STOPPED, ST_IDLE

Commands (selection)

CommandDescription
view-gclass-registerList all registered GClasses.
view-services / view-top-servicesList services.
view-gobj / view-gobj-treeInspect the gobj tree.
enable-gobj / disable-gobjEnable or disable a gobj.
write-attr / view-attrs / attrs-schemaRead/write gobj attributes.
set-global-trace / set-gclass-trace / set-gobj-traceConfigure trace levels.
reset-all-traces / set-deep-traceTrace management.
info-global-traceShow trace configuration.
view-configShow yuno configuration.
info-memMemory usage info.
info-cpus / info-ifs / info-osSystem information.
info-uptimeUptime and boot date of the machine, and uptime and start date of this yuno.
info-inotifyinotify limits (/proc/sys/fs/inotify/*) plus this yuno’s own usage (instances + watches).
list-allowed-ips / add-allowed-ipIP access control.
truncate-log-fileTruncate the log file.
view-log-countersShow log event counters.
add-log-handler / del-log-handler / list-log-handlersLog handler management.
shutdownStop this yuno, orderly.

Open files

limit_open_files (default 0) is the soft limit of open files the yuno asks for at its start. With 0 it raises its soft limit to its hard one: a yuno started from a desktop session gets the soft 1024 of systemd --user, and a timeranger2 follower holds a descriptor per key directory of each feed (FS_FLAG_DIR_FDS), so 1024 ran out (up to 7.25.21, 0 left the limit as it came). A positive value sets both limits to it when the soft one is lower. What it got is in limit_open_files_done.

{
    "yuno": {
        "limit_open_files": 200000
    }
}

Uptime

The uptime stat is the yuno’s: seconds since its mt_create, on the monotonic clock (a change of the wall clock does not move it). The machine’s is answered by info-uptime, from CLOCK_BOOTTIME (which, like /proc/uptime, counts the time the machine was suspended), with the boot time it gives:

ycommand -c 'command-yuno id=<id> service=__yuno__ command=info-uptime'
# "data": {"machine_uptime": 3142, "machine_boot_time": 1790924758,
#          "machine_boot_date": "2026-10-02T09:05:58.0+0200",
#          "yuno_uptime": 2803, "yuno_start_time": 1790925097,
#          "yuno_start_date": "2026-10-02T09:11:37.123456789+0200"}

Up to 7.25.21 the uptime stat was the machine’s uptime in jiffies (/proc/uptime × HZ), under a description that said it was the yuno’s.

See also the Yuno API for C helper functions.


C_FS

File-system watcher — monitors directory changes using the fs_watcher engine.

PropertyValue
StatesST_IDLE
Output eventsEV_FS_CHANGED, EV_FS_RENAMED, both with {"path", "filename"}

Key attributes

AttributeTypeDescription
pathstringDirectory to watch.
recursivebool1: watch the whole tree under path, the subdirectories created later included. 0 (default): only the entries of path itself.
infoboolLog the watched directory on startup.
subscriberpointerWho gets its events. Default: its parent (the CHILD subscription model).
size_dl_watchint (stats)1 while the path is watched, 0 if not. One watcher, whether recursive or not (the name is older than that).

A watch that cannot be armed fails the start of C_FS (logged), and a watch whose read fails later (FS_WATCHER_GONE_TYPE) is logged and gone: size_dl_watch reads 0, and nothing more is published.

What it publishes

One event per change the watcher reports, path being the directory and filename the entry that changed:

Change (fs_watcher type)Event
directory created / deleted (FS_SUBDIR_CREATED_TYPE, FS_SUBDIR_DELETED_TYPE)EV_FS_CHANGED
file created / deleted / modified (FS_FILE_CREATED_TYPE, FS_FILE_DELETED_TYPE, FS_FILE_MODIFIED_TYPE)EV_FS_CHANGED
rename (FS_FILE_RENAME_TYPE, not reported by the watcher today)EV_FS_RENAMED
events lost (FS_OVERFLOW_TYPE)EV_FS_CHANGED once, for the watched root (filename empty); the rescan pass that follows publishes nothing

A host creates it as its child and declares both events in its FSM: a C_FS subscribes its parent (or the gobj in subscriber) by itself, the CHILD subscription model:

priv->gobj_fs = gobj_create("", C_FS, json_pack("{s:s, s:b}",
    "path", "/yuneta/development/docs",
    "recursive", 1
), gobj);
gobj_start(priv->gobj_fs);

Up to 7.25.20 it subscribed nobody, and every host subscribed by hand (gobj_subscribe_event(priv->gobj_fs, NULL, 0, gobj)). A host that still does subscribes twice, and the second subscription replaces the first with a warning (“subscription(s) REPEATED, will be deleted and override”): drop the call.

Up to 7.25.20 the types were read as bits: a deleted directory published nothing (and leaked the kw built for it), and the other changes were published only because the value of “file modified” (5) shared bits with theirs.

recursive is one watcher, whichever its value: with 1 it recurses by itself (FS_FLAG_RECURSIVE_PATHS). Up to 7.25.20 a recursive C_FS also added a recursive watcher for each subdirectory it found at start, each with its own inotify fd, so a change N levels down was published N+1 times (a file created in a/b/ three times); and every watcher recursed, so recursive: 0 reported the subdirectories too. With "recursive", 0:

priv->gobj_fs = gobj_create("", C_FS, json_pack("{s:s, s:b}",
    "path", "/yuneta/development/docs",
    "recursive", 0      // docs/x.md is published, docs/api/y.md is not
), gobj);

C_PTY

Pseudo-terminal — spawns a shell or process in a PTY and provides bidirectional I/O.

PropertyValue
StatesST_STOPPED, ST_DISCONNECTED, ST_CONNECTED
Input eventsEV_TX_DATA, EV_DROP
Output eventsEV_RX_DATA, EV_CONNECTED, EV_DISCONNECTED

Key attributes

AttributeTypeDescription
processstringCommand to execute (default: user’s shell).
rowsintegerTerminal rows.
colsintegerTerminal columns.
cwdstringWorking directory.
max_tx_queueintegerMaximum transmit queue size.

process is remote-settable (the agent’s authz-gated open-console command), so since 7.6.0 it is resolved against a fixed trusted-dir allowlist, never the inherited $PATH: an absolute path is accepted only if executable, a bare name is resolved against /bin, /usr/bin, /sbin, /usr/sbin, /usr/local/bin (in order), a relative-with-slash name is rejected, and anything else fails closed (no exec). The spawn uses execv, not execvp, so a planted $PATH entry cannot hijack a bare process name.


C_OTA

Over-the-air update manager — downloads and applies firmware updates.

PropertyValue
StatesST_STOPPED, ST_IDLE, ST_WAIT_RESPONSE

Key attributes

AttributeTypeDescription
url_otastringOTA server URL.
cert_pemstringTLS certificate (PEM).
ota_statestringCurrent OTA state (read-only).
forceboolForce update even if role differs.
timeout_validateintegerValidation timeout in seconds.

Commands

CommandDescription
download-firmwareStart firmware download.

C_GSS_UDP_S

Gossamer UDP server — manages multiple virtual channels over a single UDP socket. It is what logcenter listens with: a C_UDP_S child, one CHANNEL per peer, and the frames of each channel joined until a NUL (the log handler of a yuno sends a log line longer than one datagram in pieces, the NUL after the last one).

PropertyValue
StatesST_IDLE
Input eventsEV_SEND_MESSAGE (a gbuffer to send: to its address, or, with none, to the known peer its label names -- see Answer a peer), and from its C_UDP_S: EV_RX_DATA, EV_TX_READY, EV_STOPPED; EV_TIMEOUT_PERIODIC from its timer, EV_TIMEOUT from its restart timer
Output eventsEV_ON_OPEN (a new channel), EV_ON_MESSAGE (a whole frame, its gbuffer in the kw, without the NUL; the gbuffer carries its peer as label and address), EV_ON_CLOSE (a channel with no datagram for seconds_inactivity)

A channel is keyed by the PEER of the datagram, the label C_UDP_S writes in the gbuffer ("ip:port"). So the pieces of the frames of two peers never mix, however they interleave:

peer A "a1"   peer B "b1"   peer A "a2\0"   peer B "b2\0"
-> EV_ON_OPEN, EV_ON_OPEN, EV_ON_MESSAGE "a1a2", EV_ON_MESSAGE "b1b2"

Up to 7.25.4 C_UDP_S wrote that label only when tracing, every peer was the channel "", and the same datagrams came out as "a1b1a2" and "b2" -- on logcenter, corrupt log records whenever two yunos sent long lines at the same time. tests/c/c_udp_s_rx.

json_t *kw_gss = json_pack("{s:s, s:I}",
    "url", "udp://127.0.0.1:1992",
    "seconds_inactivity", (json_int_t)300
);
hgobj gss = gobj_create("logs", C_GSS_UDP_S, kw_gss, gobj);    // gobj hears EV_ON_*

Answer a peer

The gbuffer of an EV_ON_MESSAGE carries its peer twice: as its LABEL (gbuffer_getlabel(), "ip:port", the name of its channel) and as its ADDRESS (gbuffer_getaddr()). EV_SEND_MESSAGE takes either:

PRIVATE int ac_on_message(hgobj gobj, gobj_event_t event, json_t *kw, hgobj src)
{
    gbuffer_t *frame = (gbuffer_t *)(uintptr_t)kw_get_int(gobj, kw, "gbuffer", 0, 0);

    gbuffer_t *answer = gbuffer_create(64, 64);
    gbuffer_append_string(answer, "ack");
    gbuffer_setlabel(answer, gbuffer_getlabel(frame));  // or gbuffer_setaddr() with its address
    gobj_send_event(gss, EV_SEND_MESSAGE,
        json_pack("{s:I}", "gbuffer", (json_int_t)(uintptr_t)answer),   // the kw owns it
        gobj
    );

    KW_DECREF(kw)
    return 0;
}

Up to 7.25.4 a frame carried neither its peer’s label nor its address, so no host could answer the peer of a frame; and EV_SEND_MESSAGE looked the channel up by the label and did not use it: a send by address (the documented way) logged “UDP channel NOT FOUND” for every datagram, and a send by label had no address: the kernel refused the datagram (EINVAL), and C_UDP_S took that for a disconnection and stopped, reading too. The label contract is the original one (the pre-v7 C_UDP_S sent to the "ip:port" of the label); the address is how C_UDP_S sends since v7. tests/c/c_udp_s_rx, case 5.

When its C_UDP_S stops by itself

C_UDP_S stops by itself when its read fails or cannot be started again (no memory for the next read, a socket error): it logs the cause as an ERROR and publishes EV_STOPPED -- while it still RUNS, which is how C_GSS_UDP_S tells it from a stop made from outside. C_GSS_UDP_S then:

A send is refused whenever the C_UDP_S is not in ST_IDLE: stopped, or still stopping (ST_WAIT_STOPPED, while a send of its own is in flight, the window before its EV_STOPPED). The event answers -1, and ONE warning is logged per stop, “EV_SEND_MESSAGE while the UDP server is stopped, dropped”, with udp_state; the count comes with the restart, or, if the C_GSS_UDP_S is stopped first, with its stop (“UDP server stops with sends refused while it was stopped”, tx_dropped).

A C_UDP_S stopped from OUTSIDE (a gobj_stop() of it alone: it no longer runs when its EV_STOPPED comes) is not started again -- that stop was somebody’s decision. It is said once, INFO, “UDP server stopped from outside, it is not started again”, and the sends are refused as above.

Its own stop (gobj_stop() of the C_GSS_UDP_S) stops the C_UDP_S too, and that EV_STOPPED is its end, not a restart. A start that comes while that stop still waits (a stop and a start in the same turn, the read of the C_UDP_S still being canceled) starts the C_UDP_S when its EV_STOPPED comes, on the next cycle of the loop (EV_START_UDP_SERVER): starting it at once would find it still holding its socket.

A timeout_base of 0 or less would arm no timer at all: no peer forgotten after seconds_inactivity, and no base for the backoff. It is refused at the create with a WARNING (“timeout_base <= 0 would arm no timer ...”), and the default 5000 is used (and written back to the attribute):

json_t *kw_gss = json_pack("{s:s, s:i}",
    "url", "udp://127.0.0.1:1992",
    "timeout_base", 2000        // inactivity check, and first restart delay
);
hgobj gss = gobj_create("logs", C_GSS_UDP_S, kw_gss, gobj);

Up to 7.25.20 the EV_STOPPED was taken with no action: the service could not receive again, and every send reached a stopped C_UDP_S and logged “Event NOT DEFINED in state”. tests/c/c_gss_udp_s_self_stop.

Key attributes

AttributeTypeDescription
urlstringUDP listening URL.
timeout_baseintegerPeriod of the inactivity check, and first delay of a restart of the C_UDP_S, in milliseconds (default 5000; <= 0 is refused with a warning and the default used).
seconds_inactivityintegerSeconds without a datagram before a channel is closed (default 300).
disable_end_of_frameboolPublish every datagram as it comes (EV_ON_MESSAGE with the EV_RX_DATA kw), without joining until a NUL.
max_channelsintegerPeers held at once (default 1024, 0 no limit). A datagram of a new peer beyond it is dropped.
max_frame_sizeintegerBytes of one frame (default 1048576). A frame with no NUL within it is delivered cut at this size.
max_pending_bytesintegerBytes of all the unfinished frames together (default 8388608, 0 no limit). Beyond it the datagram is dropped, with the unfinished frame of its peer.

What a peer can hold

A frame costs this yuno memory before it is a frame, and the peer decides how much: every source port of a host is a peer, and each keeps its unfinished frame for seconds_inactivity. So three caps bound it:

CapWhen it is reachedLogged
max_channelsthe datagrams of a NEW peer are dropped; the peers held go ononce, “Too many peers, datagrams of new peers dropped”, and again only after a peer has gone
max_frame_sizewhat came is delivered cut, and a new frame begins“Frame without end within max_frame_size, delivered cut”, at most once per 10 s, with the count
max_pending_bytesthe datagram is dropped, with the unfinished frame of its peer“Too many bytes in unfinished frames, ...”, at most once per 10 s, with the count

The buffer of a frame starts at 4 KB and doubles as bytes come, up to max_frame_size. A buffer of 1 MB from the first byte of each peer, with no cap on the peers, would let a sender of one-byte datagrams from many source ports take logcenter to its memory ceiling. (Up to 7.25.4 every peer shared one channel, and the corrupt joins above were the price.) tests/c/c_udp_s_rx, case 4.

The defaults fit logcenter, which creates its C_GSS_UDP_S with only the url: one peer per yuno of the node. A host that expects more peers, or longer frames, passes the caps when it creates it:

json_t *kw_gss = json_pack("{s:s, s:i, s:i}",
    "url", "udp://127.0.0.1:1992",
    "max_channels", 4096,
    "max_pending_bytes", 32*1024*1024
);
hgobj gss = gobj_create("logs", C_GSS_UDP_S, kw_gss, gobj);