/****************************************************************************** __ ____ __ / / ___ ____ _/ __/_____________ _/ /__ / / / _ \/ __ `/ /_/ ___/ ___/ __ `/ / _ \ / /___/ __/ /_/ / __(__ ) /__/ /_/ / / __/ /_____/\___/\__,_/_/ /____/\___/\__,_/_/\___/ (C)opyright 2025-2026, Leafscale, LLC - https://www.leafscale.com Project: zyginit Filename: socket.reef Authors: Chris Tusa License: 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 " 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 \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 \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 \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 \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 \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 \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 , stop , restart , enable , disable , reload, replace [--wait=N], halt, reboot, poweroff, single, multiuser, log , 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