/****************************************************************************** __ ____ __ / / ___ ____ _/ __/_____________ _/ /__ / / / _ \/ __ `/ /_/ ___/ ___/ __ `/ / _ \ / /___/ __/ /_/ / __(__ ) /__/ /_/ / / __/ /_____/\___/\__,_/_/ /____/\___/\__,_/_/\___/ (C)opyright 2025-2026, Leafscale, LLC - https://www.leafscale.com Project: zyginit Filename: config.reef Authors: Chris Tusa License: Description: TOML service definition parser and ServiceDef types ******************************************************************************/ module config import encoding.toml import io.file import io.dir import core.str export type ServiceDef // Constants for service type fn SERVICE_TYPE_DAEMON(): int fn SERVICE_TYPE_ONESHOT(): int fn SERVICE_TYPE_TRANSIENT(): int // Constants for restart policy fn RESTART_ALWAYS(): int fn RESTART_FAILURE(): int fn RESTART_NEVER(): int // Constants for stop method fn STOP_METHOD_CONTRACT(): int fn STOP_METHOD_EXEC(): int // Constants for runlevel mode (which runlevel(s) include this service) fn RUNLEVEL_MULTI(): int fn RUNLEVEL_SINGLE(): int fn RUNLEVEL_ALWAYS(): int // Service definition constructor fn new_service_def(): ServiceDef // Accessors fn svc_name(svc: ServiceDef): string fn svc_description(svc: ServiceDef): string fn svc_type(svc: ServiceDef): int fn svc_start_cmd(svc: ServiceDef): string fn svc_stop_cmd(svc: ServiceDef): string fn svc_working_dir(svc: ServiceDef): string fn svc_environment(svc: ServiceDef): [string] fn svc_environment_count(svc: ServiceDef): int fn svc_user(svc: ServiceDef): string fn svc_group(svc: ServiceDef): string fn svc_stop_method(svc: ServiceDef): int fn svc_stop_signal(svc: ServiceDef): string fn svc_stop_timeout(svc: ServiceDef): int fn svc_requires(svc: ServiceDef): [string] fn svc_requires_count(svc: ServiceDef): int fn svc_after(svc: ServiceDef): [string] fn svc_after_count(svc: ServiceDef): int fn svc_contract_param(svc: ServiceDef): [string] fn svc_contract_param_count(svc: ServiceDef): int fn svc_contract_fatal(svc: ServiceDef): [string] fn svc_contract_fatal_count(svc: ServiceDef): int fn svc_restart_policy(svc: ServiceDef): int fn svc_restart_delay(svc: ServiceDef): int fn svc_max_retries(svc: ServiceDef): int fn svc_runlevel_mode(svc: ServiceDef): int // Human-readable names for type/policy constants fn service_type_name(t: int): string fn restart_policy_name(p: int): string fn stop_method_name(m: int): string fn runlevel_mode_name(m: int): string // Parsing fn parse_service(filepath: string, svc: ServiceDef): bool fn scan_enabled(config_dir: string, paths: [string], max_paths: 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 // ============================================================================ // Constants // ============================================================================ fn SERVICE_TYPE_DAEMON(): int return 1 end SERVICE_TYPE_DAEMON fn SERVICE_TYPE_ONESHOT(): int return 2 end SERVICE_TYPE_ONESHOT fn SERVICE_TYPE_TRANSIENT(): int return 3 end SERVICE_TYPE_TRANSIENT fn RESTART_ALWAYS(): int return 1 end RESTART_ALWAYS fn RESTART_FAILURE(): int return 2 end RESTART_FAILURE fn RESTART_NEVER(): int return 3 end RESTART_NEVER fn STOP_METHOD_CONTRACT(): int return 1 end STOP_METHOD_CONTRACT fn STOP_METHOD_EXEC(): int return 2 end STOP_METHOD_EXEC // Runlevel modes — which runlevel(s) include this service in the active // set. Default is MULTI, matching legacy zyginit behavior (services run // in multi-user mode, are stopped on transitions to single-user). fn RUNLEVEL_MULTI(): int return 0 end RUNLEVEL_MULTI fn RUNLEVEL_SINGLE(): int return 1 end RUNLEVEL_SINGLE fn RUNLEVEL_ALWAYS(): int return 2 end RUNLEVEL_ALWAYS // ============================================================================ // ServiceDef type // ============================================================================ type ServiceDef = struct name: string description: string service_type: int start_cmd: string stop_cmd: string working_dir: string environment: [string] environment_count: int user: string group: string stop_method: int stop_signal: string stop_timeout: int requires: [string] requires_count: int after: [string] after_count: int contract_param: [string] contract_param_count: int contract_fatal: [string] contract_fatal_count: int restart_policy: int restart_delay: int max_retries: int // Runlevel mode: RUNLEVEL_MULTI (default), RUNLEVEL_SINGLE, or // RUNLEVEL_ALWAYS. Determines whether the service is in the active // set for single-user mode, multi-user mode, or both. runlevel_mode: int end ServiceDef fn new_service_def(): ServiceDef return ServiceDef{ name: "", description: "", service_type: SERVICE_TYPE_DAEMON(), start_cmd: "", stop_cmd: "", working_dir: "", environment: new [string](16), environment_count: 0, user: "", group: "", stop_method: STOP_METHOD_CONTRACT(), stop_signal: "TERM", stop_timeout: 60, requires: new [string](16), requires_count: 0, after: new [string](16), after_count: 0, contract_param: new [string](8), contract_param_count: 0, contract_fatal: new [string](8), contract_fatal_count: 0, restart_policy: RESTART_FAILURE(), restart_delay: 5, max_retries: 3, runlevel_mode: RUNLEVEL_MULTI() } end new_service_def // ============================================================================ // Accessors // ============================================================================ fn svc_name(svc: ServiceDef): string return svc.name end svc_name fn svc_description(svc: ServiceDef): string return svc.description end svc_description fn svc_type(svc: ServiceDef): int return svc.service_type end svc_type fn svc_start_cmd(svc: ServiceDef): string return svc.start_cmd end svc_start_cmd fn svc_stop_cmd(svc: ServiceDef): string return svc.stop_cmd end svc_stop_cmd fn svc_working_dir(svc: ServiceDef): string return svc.working_dir end svc_working_dir fn svc_environment(svc: ServiceDef): [string] return svc.environment end svc_environment fn svc_environment_count(svc: ServiceDef): int return svc.environment_count end svc_environment_count fn svc_user(svc: ServiceDef): string return svc.user end svc_user fn svc_group(svc: ServiceDef): string return svc.group end svc_group fn svc_stop_method(svc: ServiceDef): int return svc.stop_method end svc_stop_method fn svc_stop_signal(svc: ServiceDef): string return svc.stop_signal end svc_stop_signal fn svc_stop_timeout(svc: ServiceDef): int return svc.stop_timeout end svc_stop_timeout fn svc_requires(svc: ServiceDef): [string] return svc.requires end svc_requires fn svc_requires_count(svc: ServiceDef): int return svc.requires_count end svc_requires_count fn svc_after(svc: ServiceDef): [string] return svc.after end svc_after fn svc_after_count(svc: ServiceDef): int return svc.after_count end svc_after_count fn svc_contract_param(svc: ServiceDef): [string] return svc.contract_param end svc_contract_param fn svc_contract_param_count(svc: ServiceDef): int return svc.contract_param_count end svc_contract_param_count fn svc_contract_fatal(svc: ServiceDef): [string] return svc.contract_fatal end svc_contract_fatal fn svc_contract_fatal_count(svc: ServiceDef): int return svc.contract_fatal_count end svc_contract_fatal_count fn svc_restart_policy(svc: ServiceDef): int return svc.restart_policy end svc_restart_policy fn svc_restart_delay(svc: ServiceDef): int return svc.restart_delay end svc_restart_delay fn svc_max_retries(svc: ServiceDef): int return svc.max_retries end svc_max_retries fn svc_runlevel_mode(svc: ServiceDef): int return svc.runlevel_mode end svc_runlevel_mode // ============================================================================ // Human-readable names // ============================================================================ fn service_type_name(t: int): string if t == SERVICE_TYPE_DAEMON() return "daemon" elif t == SERVICE_TYPE_ONESHOT() return "oneshot" elif t == SERVICE_TYPE_TRANSIENT() return "transient" end if return "unknown" end service_type_name fn restart_policy_name(p: int): string if p == RESTART_ALWAYS() return "always" elif p == RESTART_FAILURE() return "failure" elif p == RESTART_NEVER() return "never" end if return "unknown" end restart_policy_name fn stop_method_name(m: int): string if m == STOP_METHOD_CONTRACT() return "contract" elif m == STOP_METHOD_EXEC() return "exec" end if return "unknown" end stop_method_name fn runlevel_mode_name(m: int): string if m == RUNLEVEL_MULTI() return "multi" elif m == RUNLEVEL_SINGLE() return "single" elif m == RUNLEVEL_ALWAYS() return "always" end if return "unknown" end runlevel_mode_name // Parse runlevel mode string to constant fn parse_runlevel_mode(mode_str: string): int if mode_str == "multi" return RUNLEVEL_MULTI() elif mode_str == "single" return RUNLEVEL_SINGLE() elif mode_str == "always" return RUNLEVEL_ALWAYS() end if // Unknown/empty — default to multi (legacy behavior) return RUNLEVEL_MULTI() end parse_runlevel_mode // ============================================================================ // Parsing helpers // ============================================================================ // Parse service type string to constant fn parse_service_type(type_str: string): int if type_str == "daemon" return SERVICE_TYPE_DAEMON() elif type_str == "oneshot" return SERVICE_TYPE_ONESHOT() elif type_str == "transient" return SERVICE_TYPE_TRANSIENT() end if return SERVICE_TYPE_DAEMON() end parse_service_type // Parse restart policy string to constant fn parse_restart_policy(policy_str: string): int if policy_str == "always" return RESTART_ALWAYS() elif policy_str == "failure" return RESTART_FAILURE() elif policy_str == "never" return RESTART_NEVER() end if return RESTART_FAILURE() end parse_restart_policy // Parse stop method string to constant fn parse_stop_method(method_str: string): int if method_str == "contract" return STOP_METHOD_CONTRACT() elif method_str == "exec" return STOP_METHOD_EXEC() end if 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] != '[' 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 end if i = i + 1 end while return count end parse_toml_array // ============================================================================ // Service parsing // ============================================================================ // Parse a single TOML service definition file into a ServiceDef. // Returns true on success. fn parse_service(filepath: string, svc: ServiceDef): bool if not file.fileExists(filepath) return false end if let content = file.readFile(filepath) 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) if count == 0 return false end if // [service] section — required fields if not toml.toml_has_key(keys, vals, count, "service.name") return false end if svc.name = toml.toml_get(keys, vals, count, "service.name") if toml.toml_has_key(keys, vals, count, "service.description") svc.description = toml.toml_get(keys, vals, count, "service.description") end if if toml.toml_has_key(keys, vals, count, "service.type") svc.service_type = parse_service_type(toml.toml_get(keys, vals, count, "service.type")) end if // [exec] section — start is required if not toml.toml_has_key(keys, vals, count, "exec.start") return false end if svc.start_cmd = toml.toml_get(keys, vals, count, "exec.start") if toml.toml_has_key(keys, vals, count, "exec.stop") svc.stop_cmd = toml.toml_get(keys, vals, count, "exec.stop") end if if toml.toml_has_key(keys, vals, count, "exec.working_directory") 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 if toml.toml_has_key(keys, vals, count, "exec.user") svc.user = toml.toml_get(keys, vals, count, "exec.user") end if if toml.toml_has_key(keys, vals, count, "exec.group") svc.group = toml.toml_get(keys, vals, count, "exec.group") end if // [stop] section — all optional with defaults if toml.toml_has_key(keys, vals, count, "stop.method") svc.stop_method = parse_stop_method(toml.toml_get(keys, vals, count, "stop.method")) end if if toml.toml_has_key(keys, vals, count, "stop.signal") svc.stop_signal = toml.toml_get(keys, vals, count, "stop.signal") end if if toml.toml_has_key(keys, vals, count, "stop.timeout") svc.stop_timeout = toml.toml_get_int(keys, vals, count, "stop.timeout") 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 // [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 // [restart] section — all optional with defaults if toml.toml_has_key(keys, vals, count, "restart.on") svc.restart_policy = parse_restart_policy(toml.toml_get(keys, vals, count, "restart.on")) end if if toml.toml_has_key(keys, vals, count, "restart.delay") svc.restart_delay = toml.toml_get_int(keys, vals, count, "restart.delay") end if if toml.toml_has_key(keys, vals, count, "restart.max_retries") svc.max_retries = toml.toml_get_int(keys, vals, count, "restart.max_retries") end if // [runlevel] section — optional, default mode="multi" if toml.toml_has_key(keys, vals, count, "runlevel.mode") svc.runlevel_mode = parse_runlevel_mode( toml.toml_get(keys, vals, count, "runlevel.mode")) end if return true end parse_service // ============================================================================ // Service scanning // ============================================================================ // Scan the enabled.d/ directory and return paths to enabled service TOML files. // Each entry in enabled.d/ is a symlink named after the service. // We construct the TOML path as: config_dir/.toml // Returns the number of paths written. fn scan_enabled(config_dir: string, paths: [string], max_paths: int): int let enabled_dir = str.concat(config_dir, "/enabled.d") if not dir.dir_exists(enabled_dir) return 0 end if let entries = new [string](128) let entry_count = dir.list_dir(enabled_dir, entries, 128) mut path_count = 0 mut i = 0 while i < entry_count and path_count < max_paths let name = entries[i] let toml_path = str.concat(str.concat(config_dir, "/"), str.concat(name, ".toml")) if file.fileExists(toml_path) paths[path_count] = toml_path path_count = path_count + 1 end if i = i + 1 end while return path_count end scan_enabled // Load all enabled services from config_dir. // Returns the number of services loaded into the services array. fn load_enabled_services(config_dir: string, services: [ServiceDef], max_services: int): int let paths = new [string](128) let path_count = scan_enabled(config_dir, paths, 128) mut loaded = 0 mut i = 0 while i < path_count and loaded < max_services mut svc = new_service_def() if parse_service(paths[i], svc) services[loaded] = svc loaded = loaded + 1 end if i = i + 1 end while return loaded end load_enabled_services end module