Migrate to Reef 0.7.6: Result/Option, TOML inline arrays, socket module rename
Migrate to Reef 0.7.6: Result/Option, TOML inline arrays, socket module rename Reef 0.7.6 brought three breaking changes; adapt zyginit + zygctl: - Result/Option stdlib migration: io.file/io.dir/net.unix/hashmap.get/ toml_parse now return Result[T,Error] / Option[T]. Guard with res.is_err/unwrap_ok/unwrap_or and opt.is_some/unwrap. io.dir.list_dir also changed shape (returns the array; length via arr.length()). Struct-field generic-return inference needs explicit 'let x: opt.Option[int] = ...' annotations (0.7.6 compiler bug). - TOML inline-array flattening: 'key = [...]' now flattens to key.0 / key.#len instead of a raw string. New read_toml_array helper in config.reef; removes the now-dead parse_toml_array. Fixes silently empty requires/after/conflicts/environment/contract arrays. - Rename local module 'socket' -> 'ctlsocket' (socket.reef -> ctlsocket.reef): stdlib net.unix now imports net.socket, clashing with the local short name. Linux dev build green; integration 91/91, migrate_layout 5/5.
Author:
Chris Tusa <chris.tusa@leafscale.com>
Date:
Jul 17, 2026 19:47
Changeset:
d07e2e820232b75e49368cd735af7fc2a7d91b8f
Branch:
default
Changed files:
D
src/socket.reef
Diff
diff -r f716017622c4 -r d07e2e820232 CLAUDE.md --- a/CLAUDE.md Sat Jun 13 18:55:07 2026 -0500 +++ b/CLAUDE.md Fri Jul 17 19:47:52 2026 -0500 @@ -216,7 +216,7 @@ │ ├── depgraph.reef # Dependency graph and topological sort │ ├── contract.reef # Hammerhead process contract FFI bindings │ ├── supervisor.reef # Service lifecycle management -│ └── socket.reef # Unix domain socket server for zygctl +│ └── ctlsocket.reef # Unix domain socket server for zygctl (module `ctlsocket`; renamed from `socket` to avoid a name clash with stdlib `net.socket`, which `net.unix` now imports as of Reef 0.7.6) ├── services/ │ └── examples/ # Example TOML service definitions ├── tests/ diff -r f716017622c4 -r d07e2e820232 src/config.reef --- a/src/config.reef Sat Jun 13 18:55:07 2026 -0500 +++ b/src/config.reef Fri Jul 17 19:47:52 2026 -0500 @@ -21,6 +21,7 @@ import io.file import io.dir import core.str +import core.result as res export type ServiceDef @@ -93,9 +94,6 @@ fn parse_service(filepath: string, name: string, svc_dir: string, svc: ServiceDef): bool fn scan_enabled(config_dir: string, names: [string], max_names: int): int fn load_enabled_services(config_dir: string, services: [ServiceDef], max_services: int): int - - // Inline array helper (exported for testing) - fn parse_toml_array(value: string, result: [string], max_items: int): int end export // FFI: helpers.c — resolve a symlink or path to its canonical absolute form. @@ -468,46 +466,31 @@ return STOP_METHOD_CONTRACT() end parse_stop_method -// Parse a TOML inline array like ["a", "b", "c"] into a string array. -// Returns the number of items parsed. -// Adapted from Coral's parse_toml_array pattern. -fn parse_toml_array(value: string, result: [string], max_items: int): int - let len = str.length(value) - if len < 2 - return 0 - end if - - if value[0] != '[' +// Read an inline-array TOML field from the flattened key/value model. +// +// Reef 0.7.5+ flattens `key = ["a", "b"]` into indexed keys `key.0`, `key.1`, +// ... plus a synthetic `key.#len` count marker — there is no longer a single +// `key` entry holding the raw "[...]" text. This reads up to `max_items` +// elements of `prefix` into `result` and returns the number stored. A missing +// field or empty array yields 0. +fn read_toml_array(keys: [string], vals: [string], count: int, prefix: string, result: [string], max_items: int): int + let len_key = str.concat(prefix, ".#len") + if not toml.toml_has_key(keys, vals, count, len_key) return 0 end if - - mut count = 0 - mut i = 1 - mut in_string = false - mut item_start = 0 - - while i < len and count < max_items - let c = value[i] - - if c == '"' and not in_string - in_string = true - item_start = i + 1 - elif c == '"' and in_string - let item_len = i - item_start - if item_len > 0 - result[count] = str.substring(value, item_start, item_len) - count = count + 1 - end if - in_string = false - elif c == ']' and not in_string - return count + let n = toml.toml_get_int(keys, vals, count, len_key) + mut stored = 0 + mut i = 0 + while i < n and stored < max_items + let item_key = str.concat(str.concat(prefix, "."), str.from_int(i)) + if toml.toml_has_key(keys, vals, count, item_key) + result[stored] = toml.toml_get(keys, vals, count, item_key) + stored = stored + 1 end if - i = i + 1 end while - - return count -end parse_toml_array + return stored +end read_toml_array // ============================================================================ // Service parsing @@ -549,14 +532,22 @@ return false end if - let content = file.readFile(filepath) + let content_r = file.readFile(filepath) + if res.is_err(content_r) + return false + end if + let content = res.unwrap_ok(content_r) if str.length(content) == 0 return false end if let keys = toml.toml_alloc_keys() let vals = toml.toml_alloc_values() - let count = toml.toml_parse(content, keys, vals) + let count_r = toml.toml_parse(content, keys, vals) + if res.is_err(count_r) + return false + end if + let count = res.unwrap_ok(count_r) if count == 0 return false @@ -590,10 +581,7 @@ svc.working_dir = toml.toml_get(keys, vals, count, "exec.working_directory") end if - if toml.toml_has_key(keys, vals, count, "exec.environment") - let env_str = toml.toml_get(keys, vals, count, "exec.environment") - svc.environment_count = parse_toml_array(env_str, svc.environment, 16) - end if + svc.environment_count = read_toml_array(keys, vals, count, "exec.environment", svc.environment, 16) if toml.toml_has_key(keys, vals, count, "exec.user") svc.user = toml.toml_get(keys, vals, count, "exec.user") @@ -617,31 +605,13 @@ end if // [dependencies] section — inline arrays - if toml.toml_has_key(keys, vals, count, "dependencies.requires") - let req_str = toml.toml_get(keys, vals, count, "dependencies.requires") - svc.requires_count = parse_toml_array(req_str, svc.requires, 16) - end if - - if toml.toml_has_key(keys, vals, count, "dependencies.after") - let after_str = toml.toml_get(keys, vals, count, "dependencies.after") - svc.after_count = parse_toml_array(after_str, svc.after, 16) - end if - - if toml.toml_has_key(keys, vals, count, "dependencies.conflicts") - let conf_str = toml.toml_get(keys, vals, count, "dependencies.conflicts") - svc.conflicts_count = parse_toml_array(conf_str, svc.conflicts, 16) - end if + svc.requires_count = read_toml_array(keys, vals, count, "dependencies.requires", svc.requires, 16) + svc.after_count = read_toml_array(keys, vals, count, "dependencies.after", svc.after, 16) + svc.conflicts_count = read_toml_array(keys, vals, count, "dependencies.conflicts", svc.conflicts, 16) // [contract] section — inline arrays - if toml.toml_has_key(keys, vals, count, "contract.param") - let param_str = toml.toml_get(keys, vals, count, "contract.param") - svc.contract_param_count = parse_toml_array(param_str, svc.contract_param, 8) - end if - - if toml.toml_has_key(keys, vals, count, "contract.fatal") - let fatal_str = toml.toml_get(keys, vals, count, "contract.fatal") - svc.contract_fatal_count = parse_toml_array(fatal_str, svc.contract_fatal, 8) - end if + svc.contract_param_count = read_toml_array(keys, vals, count, "contract.param", svc.contract_param, 8) + svc.contract_fatal_count = read_toml_array(keys, vals, count, "contract.fatal", svc.contract_fatal, 8) // [restart] section — all optional with defaults if toml.toml_has_key(keys, vals, count, "restart.on") @@ -667,10 +637,7 @@ svc.cond_exists_file = toml.toml_get(keys, vals, count, "condition.exists_file") end if - if toml.toml_has_key(keys, vals, count, "condition.exists_file_any") - let any_str = toml.toml_get(keys, vals, count, "condition.exists_file_any") - svc.cond_exists_file_any_count = parse_toml_array(any_str, svc.cond_exists_file_any, 16) - end if + svc.cond_exists_file_any_count = read_toml_array(keys, vals, count, "condition.exists_file_any", svc.cond_exists_file_any, 16) if toml.toml_has_key(keys, vals, count, "condition.command") svc.cond_command = toml.toml_get(keys, vals, count, "condition.command") @@ -706,8 +673,12 @@ return 0 end if - let entries = new [string](128) - let entry_count = dir.list_dir(enabled_dir, entries, 128) + let entries_r = dir.list_dir(enabled_dir) + if res.is_err(entries_r) + return 0 + end if + let entries = res.unwrap_ok(entries_r) + let entry_count = entries.length() mut name_count = 0 mut i = 0 diff -r f716017622c4 -r d07e2e820232 src/ctlsocket.reef --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/ctlsocket.reef Fri Jul 17 19:47:52 2026 -0500 @@ -0,0 +1,510 @@ +/****************************************************************************** + __ ____ __ + / / ___ ____ _/ __/_____________ _/ /__ + / / / _ \/ __ `/ /_/ ___/ ___/ __ `/ / _ \ + / /___/ __/ /_/ / __(__ ) /__/ /_/ / / __/ + /_____/\___/\__,_/_/ /____/\___/\__,_/_/\___/ + + (C)opyright 2025-2026, Leafscale, LLC - https://www.leafscale.com + + Project: zyginit + Filename: ctlsocket.reef + Authors: Chris Tusa <chris.tusa@leafscale.com> + License: <see LICENSE file included with this source code> +Description: Unix domain socket server for zygctl communication + +******************************************************************************/ + +// Protocol: newline-terminated text commands and responses. +// Request: "COMMAND [ARG]\n" +// Response: plain text lines, connection closed after response. + +module ctlsocket + +import net.unix as unix +import io.file +import io.dir +import sys.env as env +import supervisor +import config +import shutdown +import core.str +import core.result as res +import version +import ui +import ui_render + +export + fn create_socket(path: string): int + proc destroy_socket(path: string, server_fd: int) + proc handle_client(server_fd: int, table: supervisor.ServiceTable) + fn reload_requested(): bool + proc clear_reload_flag() + fn replace_requested(): bool + proc clear_replace_flag() + fn replace_wait_seconds(): int + fn shutdown_requested(): int + proc clear_shutdown_request() + fn runlevel_requested(): int + proc clear_runlevel_request() +end export + +extern "C" fn zyginit_symlink(target: string, linkpath: string): int +extern "C" fn zyginit_fsync_path(path: string): int + +// Reload flag — set by "reload" command, checked by main event loop +mut g_reload_requested: bool = false + +fn reload_requested(): bool + return g_reload_requested +end reload_requested + +proc clear_reload_flag() + g_reload_requested = false +end clear_reload_flag + +// Replace flag — set by "replace" command, checked by main event loop. +// Wait seconds is the optional --wait=N value; 0 means "refuse immediately +// on transient state". +mut g_replace_requested: bool = false +mut g_replace_wait: int = 0 + +fn replace_requested(): bool + return g_replace_requested +end replace_requested + +proc clear_replace_flag() + g_replace_requested = false + g_replace_wait = 0 +end clear_replace_flag + +fn replace_wait_seconds(): int + return g_replace_wait +end replace_wait_seconds + +// Shutdown request flag — set by halt/reboot/poweroff commands, +// checked by main event loop each iteration. +// 0 = no shutdown requested; non-zero = SHUT_HALT/REBOOT/POWEROFF value. +mut g_shutdown_request: int = 0 + +fn shutdown_requested(): int + return g_shutdown_request +end shutdown_requested + +proc clear_shutdown_request() + g_shutdown_request = 0 +end clear_shutdown_request + +// Runlevel transition request — set by `single` or `multi` socket +// commands, picked up by main loop and applied via runlevel_transition. +// Sentinel values: -1 = no request; 0 = MULTI; 1 = SINGLE (matching +// main.reef's RUNLEVEL_* constants). +mut g_runlevel_request: int = 0 - 1 + +fn runlevel_requested(): int + return g_runlevel_request +end runlevel_requested + +proc clear_runlevel_request() + g_runlevel_request = 0 - 1 +end clear_runlevel_request + +// ============================================================================ +// Socket lifecycle +// ============================================================================ + +// Create and bind a Unix domain socket for listening. +// Returns the server fd on success, -1 on error. +fn create_socket(path: string): int + // Remove stale socket file if it exists. unlink failure is non-fatal — + // the subsequent listen will fail loudly if the path is unusable. + let unlink_rc = unix.unix_unlink(path) + if unlink_rc < 0 + println("socket: unlink failed (non-fatal): " + path) + end if + + let listen_r = unix.unix_listen(path, 5) + if res.is_err(listen_r) + println("socket: failed to listen on " + path) + return 0 - 1 + end if + let fd = res.unwrap_ok(listen_r) + + println("socket: listening on " + path) + return fd +end create_socket + +// Close the socket and remove the socket file. Called during shutdown. +// We fsync the parent directory after unlink to force the ZFS txg +// commit before uadmin reboots — otherwise the next boot sees a stale +// socket file in /var/run and create_socket fails (unlink runs again +// but bind hits residual state). Without this, zygctl can't connect +// after a zygctl-triggered reboot. +proc destroy_socket(path: string, server_fd: int) + println("socket: closing server fd") + unix.unix_close(server_fd) + let rc = unix.unix_unlink(path) + if rc < 0 + println("socket: unlink failed: " + path) + else + println("socket: unlinked " + path) + end if + let _ = zyginit_fsync_path(path) +end destroy_socket + +// ============================================================================ +// Client handling +// ============================================================================ + +// Accept a client connection, read command, dispatch, respond, close. +proc handle_client(server_fd: int, table: supervisor.ServiceTable) + let accept_r = unix.unix_accept(server_fd) + if res.is_err(accept_r) + return + end if + let client_fd = res.unwrap_ok(accept_r) + + // Read the command (max 1024 bytes) + let recv_r = unix.unix_recv(client_fd, 1024) + if res.is_err(recv_r) + unix.unix_close(client_fd) + return + end if + let input = res.unwrap_ok(recv_r) + if str.length(input) == 0 + unix.unix_close(client_fd) + return + end if + + // Strip trailing newline/whitespace + let cmd_raw = str.trim_ws(input) + + // Parse command and argument + let space_pos = str.index_of(cmd_raw, " ") + mut command = cmd_raw + mut arg = "" + + if space_pos >= 0 + command = str.substring(cmd_raw, 0, space_pos) + arg = str.substring(cmd_raw, space_pos + 1, str.length(cmd_raw) - space_pos - 1) + end if + + // Dispatch + let response = dispatch_command(command, arg, table) + + // Send response and close + unix.unix_send(client_fd, response) + unix.unix_close(client_fd) +end handle_client + +// ============================================================================ +// Command dispatch +// ============================================================================ + +fn dispatch_command(command: string, arg: string, table: supervisor.ServiceTable): string + if command == "status" + // Parse optional PLAIN modifier: "PLAIN" or "PLAIN <name>" + mut plain = 0 + mut svc_arg = arg + if str.length(arg) >= 5 and str.substring(arg, 0, 5) == "plain" + if str.length(arg) == 5 + plain = 1 + svc_arg = "" + elif str.substring(arg, 5, 1) == " " + plain = 1 + svc_arg = str.substring(arg, 6, str.length(arg) - 6) + end if + end if + return cmd_status(table, svc_arg, plain) + elif command == "start" + return cmd_start(table, arg) + elif command == "stop" + return cmd_stop(table, arg) + elif command == "restart" + return cmd_restart(table, arg) + elif command == "log" + return cmd_log(table, arg) + elif command == "reload" + g_reload_requested = true + return "reload initiated\n" + elif command == "replace" + // Optional --wait=N argument + mut wait_seconds = 0 + if str.length(arg) > 0 + // Parse --wait=N + if str.substring(arg, 0, 7) == "--wait=" + let val = str.substring(arg, 7, str.length(arg) - 7) + wait_seconds = parse_replace_int_or(val, 0) + else + return "error: replace: unknown argument: " + arg + "\n" + end if + end if + g_replace_requested = true + g_replace_wait = wait_seconds + return str.concat("replace queued (wait=", str.concat(int_to_str(wait_seconds), ")\n")) + elif command == "halt" + g_shutdown_request = shutdown.SHUT_HALT() + return "halting\n" + elif command == "reboot" + g_shutdown_request = shutdown.SHUT_REBOOT() + return "rebooting\n" + elif command == "poweroff" + g_shutdown_request = shutdown.SHUT_POWEROFF() + return "powering off\n" + elif command == "single" + g_runlevel_request = 1 // RUNLEVEL_SINGLE + return "transitioning to single-user\n" + elif command == "multi" + g_runlevel_request = 0 // RUNLEVEL_MULTI + return "transitioning to multi-user\n" + elif command == "multiuser" + // Legacy alias for backward compatibility + g_runlevel_request = 0 + return "transitioning to multi-user\n" + elif command == "enable" + return cmd_enable(arg) + elif command == "disable" + return cmd_disable(arg) + elif command == "list" + // Parse optional PLAIN modifier + mut list_plain = 0 + if arg == "plain" + list_plain = 1 + end if + return cmd_list(table, list_plain) + elif command == "version" + return "zyginit " + version.VERSION() + "\n" + elif command == "help" + return cmd_help() + end if + + return "error: unknown command: " + command + "\n" +end dispatch_command + +// ============================================================================ +// Command implementations +// ============================================================================ + +fn cmd_status(table: supervisor.ServiceTable, arg: string, plain: int): string + let count = supervisor.service_count(table) + + if str.length(arg) > 0 + // Status of a single service — honor the plain flag from the caller + let idx = supervisor.find_service(table, arg) + if idx < 0 + return "error: unknown service: " + arg + "\n" + end if + return ui_render.ui_render_status_one(table, idx, plain) + end if + + // Status of all services + if count == 0 + return "no services loaded\n" + end if + + // plain=1: no banner, no escapes; plain=0: banner + colors via daemon's g_mode + return ui_render.ui_render_status(table, 1, plain) +end cmd_status + +fn cmd_start(table: supervisor.ServiceTable, name: string): string + if str.length(name) == 0 + return "error: usage: start <service>\n" + end if + + let idx = supervisor.find_service(table, name) + if idx < 0 + return "error: unknown service: " + name + "\n" + end if + + let rt = supervisor.get_runtime(table, idx) + let state = supervisor.rt_state(rt) + + if state == supervisor.STATE_RUNNING() + return name + " is already running\n" + end if + + let conflict_peer = supervisor.conflicting_running_peer(table, idx) + if conflict_peer >= 0 + let other = supervisor.service_name_at(table, conflict_peer) + return "error: cannot start " + name + ": conflicts with running service " + other + "\n" + end if + + if supervisor.start_service(table, idx) + return name + " started\n" + end if + + return "error: failed to start " + name + "\n" +end cmd_start + +fn cmd_stop(table: supervisor.ServiceTable, name: string): string + if str.length(name) == 0 + return "error: usage: stop <service>\n" + end if + + let idx = supervisor.find_service(table, name) + if idx < 0 + return "error: unknown service: " + name + "\n" + end if + + if supervisor.stop_service(table, idx) + return name + " stopping\n" + end if + + return name + " is not running\n" +end cmd_stop + +fn cmd_restart(table: supervisor.ServiceTable, name: string): string + if str.length(name) == 0 + return "error: usage: restart <service>\n" + end if + + let idx = supervisor.find_service(table, name) + if idx < 0 + return "error: unknown service: " + name + "\n" + end if + + let rt = supervisor.get_runtime(table, idx) + let state = supervisor.rt_state(rt) + + if state == supervisor.STATE_RUNNING() + supervisor.stop_service(table, idx) + end if + + // Note: actual restart happens via the event loop when the stop completes + // and restart policy triggers. For immediate restart of a stopped service: + if state != supervisor.STATE_RUNNING() + if supervisor.start_service(table, idx) + return name + " restarted\n" + end if + return "error: failed to restart " + name + "\n" + end if + + return name + " stop initiated (will restart via policy)\n" +end cmd_restart + +fn cmd_list(table: supervisor.ServiceTable, plain: int): string + return ui_render.ui_render_list(table, plain) +end cmd_list + +fn cmd_log(table: supervisor.ServiceTable, name: string): string + if str.length(name) == 0 + return "error: usage: log <service>\n" + end if + + let idx = supervisor.find_service(table, name) + if idx < 0 + return "error: unknown service: " + name + "\n" + end if + + return supervisor.get_service_log(name) +end cmd_log + +fn cmd_enable(name: string): string + if str.length(name) == 0 + return "error: usage: enable <service>\n" + end if + + let config_dir = env.get_env_or("ZYGINIT_CONFIG_DIR", "/etc/zyginit") + let toml_path = str.concat(str.concat(config_dir, "/"), str.concat(name, ".toml")) + + if not file.fileExists(toml_path) + return "error: no service definition found: " + toml_path + "\n" + end if + + let enabled_dir = str.concat(config_dir, "/enabled.d") + + if not dir.dir_exists(enabled_dir) + if res.is_err(dir.create_dir(enabled_dir)) + return "error: cannot create " + enabled_dir + "\n" + end if + end if + + let link_path = str.concat(str.concat(enabled_dir, "/"), name) + + if file.fileExists(link_path) + return name + " is already enabled\n" + end if + + let target = str.concat("../", str.concat(name, ".toml")) + let rc = zyginit_symlink(target, link_path) + if rc < 0 + return "error: failed to create symlink for " + name + "\n" + end if + + return name + " enabled\n" +end cmd_enable + +fn cmd_disable(name: string): string + if str.length(name) == 0 + return "error: usage: disable <service>\n" + end if + + let config_dir = env.get_env_or("ZYGINIT_CONFIG_DIR", "/etc/zyginit") + let enabled_dir = str.concat(config_dir, "/enabled.d") + let link_path = str.concat(str.concat(enabled_dir, "/"), name) + + if not file.fileExists(link_path) + return name + " is not enabled\n" + end if + + if res.is_ok(file.deleteFile(link_path)) + return name + " disabled\n" + end if + + return "error: failed to remove " + link_path + "\n" +end cmd_disable + +fn cmd_help(): string + return "commands: status [name], start <name>, stop <name>, restart <name>, enable <name>, disable <name>, reload, replace [--wait=N], halt, reboot, poweroff, single, multiuser, log <name>, list, version, help\n" +end cmd_help + +// ============================================================================ +// Local helper functions +// ============================================================================ + +// Convert an integer to a string +fn int_to_str(n: int): string + if n == 0 + return "0" + end if + + mut value = n + if n < 0 + value = 0 - n + end if + + mut result = "" + while value > 0 + let digit = value % 10 + result = str.concat(str.substring("0123456789", digit, 1), result) + value = value / 10 + end while + + if n < 0 + result = str.concat("-", result) + end if + + return result +end int_to_str + +// Local int parser for replace --wait=N. Negative numbers and non-numeric +// input both yield default_val. Empty string also yields default. +fn parse_replace_int_or(s: string, default_val: int): int + let n = str.length(s) + if n == 0 + return default_val + end if + mut result = 0 + mut i = 0 + while i < n + let c = str.char_at(s, i) + if c < '0' or c > '9' + return default_val + end if + result = result * 10 + (c - '0') + i = i + 1 + end while + return result +end parse_replace_int_or + +end module diff -r f716017622c4 -r d07e2e820232 src/depgraph.reef --- a/src/depgraph.reef Sat Jun 13 18:55:07 2026 -0500 +++ b/src/depgraph.reef Fri Jul 17 19:47:52 2026 -0500 @@ -20,6 +20,7 @@ import config import collections.hashmap as hashmap import core.str +import core.option as opt export type DepGraph @@ -100,8 +101,9 @@ // Add a service node to the graph. Returns its index, or -1 if full. fn add_service(graph: DepGraph, name: string): int // Check if already exists - if graph.name_map.has(name) - return graph.name_map.get(name) + let existing: opt.Option[int] = graph.name_map.get(name) + if opt.is_some(existing) + return opt.unwrap(existing) end if if graph.count >= MAX_SERVICES() @@ -128,8 +130,10 @@ return false end if - let from_idx = graph.name_map.get(name) - let to_idx = graph.name_map.get(dep_name) + let from_opt: opt.Option[int] = graph.name_map.get(name) + let to_opt: opt.Option[int] = graph.name_map.get(dep_name) + let from_idx = opt.unwrap(from_opt) + let to_idx = opt.unwrap(to_opt) let dc = graph.dep_count[from_idx] if dc >= MAX_DEPS() @@ -274,7 +278,8 @@ mut t = 0 while t < tier_size let placed_name = tiers[tier_offset + t] - let placed_idx = graph.name_map.get(placed_name) + let placed_opt: opt.Option[int] = graph.name_map.get(placed_name) + let placed_idx = opt.unwrap(placed_opt) // Scan all nodes to find those that depend on placed_idx i = 0 diff -r f716017622c4 -r d07e2e820232 src/main.reef --- a/src/main.reef Sat Jun 13 18:55:07 2026 -0500 +++ b/src/main.reef Fri Jul 17 19:47:52 2026 -0500 @@ -21,7 +21,7 @@ import depgraph import contract import supervisor -import socket +import ctlsocket import shutdown import replace import ui @@ -819,7 +819,7 @@ // ============================================================================ // Transition the system from current g_runlevel to target_level. Called -// from the main event loop when socket.runlevel_requested() returns a +// from the main event loop when ctlsocket.runlevel_requested() returns a // non-sentinel value. Stops services no longer in the active set // (reverse tier order) and starts services newly in the active set // (forward tier order). Updates g_runlevel and writes a RUN_LVL utmpx @@ -1122,7 +1122,7 @@ let bundle_fd = contract.open_bundle() // Unix domain socket for zygctl communication - let socket_fd = socket.create_socket(SOCKET_PATH()) + let socket_fd = ctlsocket.create_socket(SOCKET_PATH()) // ---- Phase 6: Start services ---- @@ -1182,25 +1182,25 @@ end if // Check for reload requested via socket - if socket.reload_requested() - socket.clear_reload_flag() + if ctlsocket.reload_requested() + ctlsocket.clear_reload_flag() println("zyginit: reload requested via socket") reload_services(table) end if // Check for replace requested via socket - if socket.replace_requested() - let wait = socket.replace_wait_seconds() - socket.clear_replace_flag() + if ctlsocket.replace_requested() + let wait = ctlsocket.replace_wait_seconds() + ctlsocket.clear_replace_flag() replace_self(table, g_boot_time, wait) // If replace_self exec'd successfully, we never get here. // If it returned, log and continue running normally. end if // Check for shutdown requested via socket (halt/reboot/poweroff) - let req = socket.shutdown_requested() + let req = ctlsocket.shutdown_requested() if req != 0 - socket.clear_shutdown_request() + ctlsocket.clear_shutdown_request() g_shutdown_type = req g_shutdown_reason = shutdown_type_to_reason(req) running = false @@ -1208,9 +1208,9 @@ end if // Check for runlevel transition requested via socket (single/multi) - let rl_req = socket.runlevel_requested() + let rl_req = ctlsocket.runlevel_requested() if rl_req >= 0 - socket.clear_runlevel_request() + ctlsocket.clear_runlevel_request() transition_runlevel(table, tiers, tier_counts, num_tiers, rl_req) end if @@ -1241,7 +1241,7 @@ end if if socket_fd >= 0 - socket.destroy_socket(SOCKET_PATH(), socket_fd) + ctlsocket.destroy_socket(SOCKET_PATH(), socket_fd) end if println("zyginit: shutdown complete") @@ -1353,7 +1353,7 @@ end if if ready > 0 and sock_idx >= 0 and poll.poll_readable(sock_idx) - socket.handle_client(socket_fd, table) + ctlsocket.handle_client(socket_fd, table) end if supervisor.check_stop_timeouts(table) diff -r f716017622c4 -r d07e2e820232 src/replace.reef --- a/src/replace.reef Sat Jun 13 18:55:07 2026 -0500 +++ b/src/replace.reef Fri Jul 17 19:47:52 2026 -0500 @@ -33,6 +33,7 @@ import sys.fd as fd import sys.env as sysenv import core.str +import core.result as res import encoding.toml import time.time as time @@ -305,7 +306,12 @@ return new_recovered_state() end if - let content = file.readFile(path) + let content_r = file.readFile(path) + if res.is_err(content_r) + let _ = zyginit_unlink(path) + return new_recovered_state() + end if + let content = res.unwrap_ok(content_r) if str.length(content) == 0 let _ = zyginit_unlink(path) return new_recovered_state() @@ -313,7 +319,13 @@ let keys = toml.toml_alloc_keys() let vals = toml.toml_alloc_values() - let count = toml.toml_parse(content, keys, vals) + let count_r = toml.toml_parse(content, keys, vals) + if res.is_err(count_r) + println("replace: recover: parse error in " + path + " — ignoring") + let _ = zyginit_unlink(path) + return new_recovered_state() + end if + let count = res.unwrap_ok(count_r) if count == 0 println("replace: recover: parse error or empty in " + path + " — ignoring") diff -r f716017622c4 -r d07e2e820232 src/socket.reef --- a/src/socket.reef Sat Jun 13 18:55:07 2026 -0500 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,502 +0,0 @@ -/****************************************************************************** - __ ____ __ - / / ___ ____ _/ __/_____________ _/ /__ - / / / _ \/ __ `/ /_/ ___/ ___/ __ `/ / _ \ - / /___/ __/ /_/ / __(__ ) /__/ /_/ / / __/ - /_____/\___/\__,_/_/ /____/\___/\__,_/_/\___/ - - (C)opyright 2025-2026, Leafscale, LLC - https://www.leafscale.com - - Project: zyginit - Filename: socket.reef - Authors: Chris Tusa <chris.tusa@leafscale.com> - License: <see LICENSE file included with this source code> -Description: Unix domain socket server for zygctl communication - -******************************************************************************/ - -// Protocol: newline-terminated text commands and responses. -// Request: "COMMAND [ARG]\n" -// Response: plain text lines, connection closed after response. - -module socket - -import net.unix as unix -import io.file -import io.dir -import sys.env as env -import supervisor -import config -import shutdown -import core.str -import version -import ui -import ui_render - -export - fn create_socket(path: string): int - proc destroy_socket(path: string, server_fd: int) - proc handle_client(server_fd: int, table: supervisor.ServiceTable) - fn reload_requested(): bool - proc clear_reload_flag() - fn replace_requested(): bool - proc clear_replace_flag() - fn replace_wait_seconds(): int - fn shutdown_requested(): int - proc clear_shutdown_request() - fn runlevel_requested(): int - proc clear_runlevel_request() -end export - -extern "C" fn zyginit_symlink(target: string, linkpath: string): int -extern "C" fn zyginit_fsync_path(path: string): int - -// Reload flag — set by "reload" command, checked by main event loop -mut g_reload_requested: bool = false - -fn reload_requested(): bool - return g_reload_requested -end reload_requested - -proc clear_reload_flag() - g_reload_requested = false -end clear_reload_flag - -// Replace flag — set by "replace" command, checked by main event loop. -// Wait seconds is the optional --wait=N value; 0 means "refuse immediately -// on transient state". -mut g_replace_requested: bool = false -mut g_replace_wait: int = 0 - -fn replace_requested(): bool - return g_replace_requested -end replace_requested - -proc clear_replace_flag() - g_replace_requested = false - g_replace_wait = 0 -end clear_replace_flag - -fn replace_wait_seconds(): int - return g_replace_wait -end replace_wait_seconds - -// Shutdown request flag — set by halt/reboot/poweroff commands, -// checked by main event loop each iteration. -// 0 = no shutdown requested; non-zero = SHUT_HALT/REBOOT/POWEROFF value. -mut g_shutdown_request: int = 0 - -fn shutdown_requested(): int - return g_shutdown_request -end shutdown_requested - -proc clear_shutdown_request() - g_shutdown_request = 0 -end clear_shutdown_request - -// Runlevel transition request — set by `single` or `multi` socket -// commands, picked up by main loop and applied via runlevel_transition. -// Sentinel values: -1 = no request; 0 = MULTI; 1 = SINGLE (matching -// main.reef's RUNLEVEL_* constants). -mut g_runlevel_request: int = 0 - 1 - -fn runlevel_requested(): int - return g_runlevel_request -end runlevel_requested - -proc clear_runlevel_request() - g_runlevel_request = 0 - 1 -end clear_runlevel_request - -// ============================================================================ -// Socket lifecycle -// ============================================================================ - -// Create and bind a Unix domain socket for listening. -// Returns the server fd on success, -1 on error. -fn create_socket(path: string): int - // Remove stale socket file if it exists. unlink failure is non-fatal — - // the subsequent listen will fail loudly if the path is unusable. - let unlink_rc = unix.unix_unlink(path) - if unlink_rc < 0 - println("socket: unlink failed (non-fatal): " + path) - end if - - let fd = unix.unix_listen(path, 5) - if fd < 0 - println("socket: failed to listen on " + path) - return 0 - 1 - end if - - println("socket: listening on " + path) - return fd -end create_socket - -// Close the socket and remove the socket file. Called during shutdown. -// We fsync the parent directory after unlink to force the ZFS txg -// commit before uadmin reboots — otherwise the next boot sees a stale -// socket file in /var/run and create_socket fails (unlink runs again -// but bind hits residual state). Without this, zygctl can't connect -// after a zygctl-triggered reboot. -proc destroy_socket(path: string, server_fd: int) - println("socket: closing server fd") - unix.unix_close(server_fd) - let rc = unix.unix_unlink(path) - if rc < 0 - println("socket: unlink failed: " + path) - else - println("socket: unlinked " + path) - end if - let _ = zyginit_fsync_path(path) -end destroy_socket - -// ============================================================================ -// Client handling -// ============================================================================ - -// Accept a client connection, read command, dispatch, respond, close. -proc handle_client(server_fd: int, table: supervisor.ServiceTable) - let client_fd = unix.unix_accept(server_fd) - if client_fd < 0 - return - end if - - // Read the command (max 1024 bytes) - let input = unix.unix_recv(client_fd, 1024) - if str.length(input) == 0 - unix.unix_close(client_fd) - return - end if - - // Strip trailing newline/whitespace - let cmd_raw = str.trim_ws(input) - - // Parse command and argument - let space_pos = str.index_of(cmd_raw, " ") - mut command = cmd_raw - mut arg = "" - - if space_pos >= 0 - command = str.substring(cmd_raw, 0, space_pos) - arg = str.substring(cmd_raw, space_pos + 1, str.length(cmd_raw) - space_pos - 1) - end if - - // Dispatch - let response = dispatch_command(command, arg, table) - - // Send response and close - unix.unix_send(client_fd, response) - unix.unix_close(client_fd) -end handle_client - -// ============================================================================ -// Command dispatch -// ============================================================================ - -fn dispatch_command(command: string, arg: string, table: supervisor.ServiceTable): string - if command == "status" - // Parse optional PLAIN modifier: "PLAIN" or "PLAIN <name>" - mut plain = 0 - mut svc_arg = arg - if str.length(arg) >= 5 and str.substring(arg, 0, 5) == "plain" - if str.length(arg) == 5 - plain = 1 - svc_arg = "" - elif str.substring(arg, 5, 1) == " " - plain = 1 - svc_arg = str.substring(arg, 6, str.length(arg) - 6) - end if - end if - return cmd_status(table, svc_arg, plain) - elif command == "start" - return cmd_start(table, arg) - elif command == "stop" - return cmd_stop(table, arg) - elif command == "restart" - return cmd_restart(table, arg) - elif command == "log" - return cmd_log(table, arg) - elif command == "reload" - g_reload_requested = true - return "reload initiated\n" - elif command == "replace" - // Optional --wait=N argument - mut wait_seconds = 0 - if str.length(arg) > 0 - // Parse --wait=N - if str.substring(arg, 0, 7) == "--wait=" - let val = str.substring(arg, 7, str.length(arg) - 7) - wait_seconds = parse_replace_int_or(val, 0) - else - return "error: replace: unknown argument: " + arg + "\n" - end if - end if - g_replace_requested = true - g_replace_wait = wait_seconds - return str.concat("replace queued (wait=", str.concat(int_to_str(wait_seconds), ")\n")) - elif command == "halt" - g_shutdown_request = shutdown.SHUT_HALT() - return "halting\n" - elif command == "reboot" - g_shutdown_request = shutdown.SHUT_REBOOT() - return "rebooting\n" - elif command == "poweroff" - g_shutdown_request = shutdown.SHUT_POWEROFF() - return "powering off\n" - elif command == "single" - g_runlevel_request = 1 // RUNLEVEL_SINGLE - return "transitioning to single-user\n" - elif command == "multi" - g_runlevel_request = 0 // RUNLEVEL_MULTI - return "transitioning to multi-user\n" - elif command == "multiuser" - // Legacy alias for backward compatibility - g_runlevel_request = 0 - return "transitioning to multi-user\n" - elif command == "enable" - return cmd_enable(arg) - elif command == "disable" - return cmd_disable(arg) - elif command == "list" - // Parse optional PLAIN modifier - mut list_plain = 0 - if arg == "plain" - list_plain = 1 - end if - return cmd_list(table, list_plain) - elif command == "version" - return "zyginit " + version.VERSION() + "\n" - elif command == "help" - return cmd_help() - end if - - return "error: unknown command: " + command + "\n" -end dispatch_command - -// ============================================================================ -// Command implementations -// ============================================================================ - -fn cmd_status(table: supervisor.ServiceTable, arg: string, plain: int): string - let count = supervisor.service_count(table) - - if str.length(arg) > 0 - // Status of a single service — honor the plain flag from the caller - let idx = supervisor.find_service(table, arg) - if idx < 0 - return "error: unknown service: " + arg + "\n" - end if - return ui_render.ui_render_status_one(table, idx, plain) - end if - - // Status of all services - if count == 0 - return "no services loaded\n" - end if - - // plain=1: no banner, no escapes; plain=0: banner + colors via daemon's g_mode - return ui_render.ui_render_status(table, 1, plain) -end cmd_status - -fn cmd_start(table: supervisor.ServiceTable, name: string): string - if str.length(name) == 0 - return "error: usage: start <service>\n" - end if - - let idx = supervisor.find_service(table, name) - if idx < 0 - return "error: unknown service: " + name + "\n" - end if - - let rt = supervisor.get_runtime(table, idx) - let state = supervisor.rt_state(rt) - - if state == supervisor.STATE_RUNNING() - return name + " is already running\n" - end if - - let conflict_peer = supervisor.conflicting_running_peer(table, idx) - if conflict_peer >= 0 - let other = supervisor.service_name_at(table, conflict_peer) - return "error: cannot start " + name + ": conflicts with running service " + other + "\n" - end if - - if supervisor.start_service(table, idx) - return name + " started\n" - end if - - return "error: failed to start " + name + "\n" -end cmd_start - -fn cmd_stop(table: supervisor.ServiceTable, name: string): string - if str.length(name) == 0 - return "error: usage: stop <service>\n" - end if - - let idx = supervisor.find_service(table, name) - if idx < 0 - return "error: unknown service: " + name + "\n" - end if - - if supervisor.stop_service(table, idx) - return name + " stopping\n" - end if - - return name + " is not running\n" -end cmd_stop - -fn cmd_restart(table: supervisor.ServiceTable, name: string): string - if str.length(name) == 0 - return "error: usage: restart <service>\n" - end if - - let idx = supervisor.find_service(table, name) - if idx < 0 - return "error: unknown service: " + name + "\n" - end if - - let rt = supervisor.get_runtime(table, idx) - let state = supervisor.rt_state(rt) - - if state == supervisor.STATE_RUNNING() - supervisor.stop_service(table, idx) - end if - - // Note: actual restart happens via the event loop when the stop completes - // and restart policy triggers. For immediate restart of a stopped service: - if state != supervisor.STATE_RUNNING() - if supervisor.start_service(table, idx) - return name + " restarted\n" - end if - return "error: failed to restart " + name + "\n" - end if - - return name + " stop initiated (will restart via policy)\n" -end cmd_restart - -fn cmd_list(table: supervisor.ServiceTable, plain: int): string - return ui_render.ui_render_list(table, plain) -end cmd_list - -fn cmd_log(table: supervisor.ServiceTable, name: string): string - if str.length(name) == 0 - return "error: usage: log <service>\n" - end if - - let idx = supervisor.find_service(table, name) - if idx < 0 - return "error: unknown service: " + name + "\n" - end if - - return supervisor.get_service_log(name) -end cmd_log - -fn cmd_enable(name: string): string - if str.length(name) == 0 - return "error: usage: enable <service>\n" - end if - - let config_dir = env.get_env_or("ZYGINIT_CONFIG_DIR", "/etc/zyginit") - let toml_path = str.concat(str.concat(config_dir, "/"), str.concat(name, ".toml")) - - if not file.fileExists(toml_path) - return "error: no service definition found: " + toml_path + "\n" - end if - - let enabled_dir = str.concat(config_dir, "/enabled.d") - - if not dir.dir_exists(enabled_dir) - if not dir.create_dir(enabled_dir) - return "error: cannot create " + enabled_dir + "\n" - end if - end if - - let link_path = str.concat(str.concat(enabled_dir, "/"), name) - - if file.fileExists(link_path) - return name + " is already enabled\n" - end if - - let target = str.concat("../", str.concat(name, ".toml")) - let rc = zyginit_symlink(target, link_path) - if rc < 0 - return "error: failed to create symlink for " + name + "\n" - end if - - return name + " enabled\n" -end cmd_enable - -fn cmd_disable(name: string): string - if str.length(name) == 0 - return "error: usage: disable <service>\n" - end if - - let config_dir = env.get_env_or("ZYGINIT_CONFIG_DIR", "/etc/zyginit") - let enabled_dir = str.concat(config_dir, "/enabled.d") - let link_path = str.concat(str.concat(enabled_dir, "/"), name) - - if not file.fileExists(link_path) - return name + " is not enabled\n" - end if - - if file.deleteFile(link_path) - return name + " disabled\n" - end if - - return "error: failed to remove " + link_path + "\n" -end cmd_disable - -fn cmd_help(): string - return "commands: status [name], start <name>, stop <name>, restart <name>, enable <name>, disable <name>, reload, replace [--wait=N], halt, reboot, poweroff, single, multiuser, log <name>, list, version, help\n" -end cmd_help - -// ============================================================================ -// Local helper functions -// ============================================================================ - -// Convert an integer to a string -fn int_to_str(n: int): string - if n == 0 - return "0" - end if - - mut value = n - if n < 0 - value = 0 - n - end if - - mut result = "" - while value > 0 - let digit = value % 10 - result = str.concat(str.substring("0123456789", digit, 1), result) - value = value / 10 - end while - - if n < 0 - result = str.concat("-", result) - end if - - return result -end int_to_str - -// Local int parser for replace --wait=N. Negative numbers and non-numeric -// input both yield default_val. Empty string also yields default. -fn parse_replace_int_or(s: string, default_val: int): int - let n = str.length(s) - if n == 0 - return default_val - end if - mut result = 0 - mut i = 0 - while i < n - let c = str.char_at(s, i) - if c < '0' or c > '9' - return default_val - end if - result = result * 10 + (c - '0') - i = i + 1 - end while - return result -end parse_replace_int_or - -end module diff -r f716017622c4 -r d07e2e820232 src/supervisor.reef --- a/src/supervisor.reef Sat Jun 13 18:55:07 2026 -0500 +++ b/src/supervisor.reef Fri Jul 17 19:47:52 2026 -0500 @@ -31,6 +31,8 @@ import time.clock as clock import collections.hashmap as hashmap import core.str +import core.result as res +import core.option as opt export type ServiceRuntime @@ -279,16 +281,18 @@ end add_service fn find_service(table: ServiceTable, name: string): int - if table.name_map.has(name) - return table.name_map.get(name) + let idx: opt.Option[int] = table.name_map.get(name) + if opt.is_some(idx) + return opt.unwrap(idx) end if return 0 - 1 end find_service fn find_by_contract(table: ServiceTable, contract_id: int): int let key = int_to_str(contract_id) - if table.contract_map.has(key) - return table.contract_map.get(key) + let idx: opt.Option[int] = table.contract_map.get(key) + if opt.is_some(idx) + return opt.unwrap(idx) end if return 0 - 1 end find_by_contract @@ -1110,7 +1114,11 @@ if not file.fileExists(log_path) return "no log available for " + name + "\n" end if - let content = file.readFile(log_path) + let content_r = file.readFile(log_path) + if res.is_err(content_r) + return "no log available for " + name + "\n" + end if + let content = res.unwrap_ok(content_r) if str.length(content) == 0 return "(empty log)\n" end if diff -r f716017622c4 -r d07e2e820232 tools/zygctl/src/main.reef --- a/tools/zygctl/src/main.reef Sat Jun 13 18:55:07 2026 -0500 +++ b/tools/zygctl/src/main.reef Fri Jul 17 19:47:52 2026 -0500 @@ -24,6 +24,7 @@ import io.file import io.dir import core.str +import core.result as res import version extern "C" fn zyginit_symlink(target: string, linkpath: string): int @@ -103,15 +104,17 @@ req = str.concat(req, args.get(2)) end if req = str.concat(req, "\n") - let fd = unix.unix_connect(SOCKET_PATH()) - if fd < 0 + let connect_r = unix.unix_connect(SOCKET_PATH()) + if res.is_err(connect_r) println("zygctl: cannot connect to zyginit at " + SOCKET_PATH()) println("zygctl: is zyginit running?") return end if + let fd = res.unwrap_ok(connect_r) unix.unix_send(fd, req) - let resp = unix.unix_recv(fd, 4096) + let resp_r = unix.unix_recv(fd, 4096) unix.unix_close(fd) + let resp = res.unwrap_or(resp_r, "") if str.length(resp) > 0 print(resp) end if @@ -124,15 +127,17 @@ req = "list plain" end if req = str.concat(req, "\n") - let fd = unix.unix_connect(SOCKET_PATH()) - if fd < 0 + let connect_r = unix.unix_connect(SOCKET_PATH()) + if res.is_err(connect_r) println("zygctl: cannot connect to zyginit at " + SOCKET_PATH()) println("zygctl: is zyginit running?") return end if + let fd = res.unwrap_ok(connect_r) unix.unix_send(fd, req) - let resp = unix.unix_recv(fd, 4096) + let resp_r = unix.unix_recv(fd, 4096) unix.unix_close(fd) + let resp = res.unwrap_or(resp_r, "") if str.length(resp) > 0 print(resp) end if @@ -151,20 +156,22 @@ // Connect to zyginit socket let sock_path = SOCKET_PATH() - let fd = unix.unix_connect(sock_path) + let connect_r = unix.unix_connect(sock_path) - if fd < 0 + if res.is_err(connect_r) println("zygctl: cannot connect to zyginit at " + sock_path) println("zygctl: is zyginit running?") return end if + let fd = res.unwrap_ok(connect_r) // Send command unix.unix_send(fd, message) // Read response - let response = unix.unix_recv(fd, 4096) + let response_r = unix.unix_recv(fd, 4096) unix.unix_close(fd) + let response = res.unwrap_or(response_r, "") if str.length(response) > 0 print(response) @@ -188,7 +195,7 @@ // Create enabled.d/ if it does not exist if not dir.dir_exists(enabled_dir) - if not dir.create_dir(enabled_dir) + if res.is_err(dir.create_dir(enabled_dir)) println("error: cannot create " + enabled_dir) return end if @@ -222,7 +229,7 @@ return end if - if file.deleteFile(link_path) + if res.is_ok(file.deleteFile(link_path)) println(name + " disabled") else println("error: failed to remove " + link_path)