1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
|
[package]
name = "replace_probe"
version = "0.1.0"
author = "Chris Tusa <chris.tusa@leafscale.com>"
description = "Unit-test probe for zyginit live-replace state-file format"
license = "CDDL-1.0"
[build]
entry = "src/main.reef"
output = "replace_probe"
output_dir = "build"
source_dirs = ["src", "../../src"]
# This probe round-trips state.toml serialization without needing a
# running zyginit. Models utils/contract_probe.
#
# NOTE: reefc does not use source_dirs from reef.toml for module resolution.
# Module resolution falls back to src/ in CWD. Build from the zyginit root:
#
# Linux (from zyginit/):
# clang -c src/helpers.c -o build/helpers.o
# clang -c src/contract_linux_stubs.c -o build/contract_linux_stubs.o
# reefc utils/replace_probe/src/main.reef \
# -o utils/replace_probe/build/replace_probe \
# --obj build/helpers.o --obj build/contract_linux_stubs.o
/******************************************************************************
__ ____ __
/ / ___ ____ _/ __/_____________ _/ /__
/ / / _ \/ __ `/ /_/ ___/ ___/ __ `/ / _ \
/ /___/ __/ /_/ / __(__ ) /__/ /_/ / / __/
/_____/\___/\__,_/_/ /____/\___/\__,_/_/\___/
(C)opyright 2025-2026, Leafscale, LLC - https://www.leafscale.com
Project: zyginit
Filename: main.reef
Authors: Chris Tusa <chris.tusa@leafscale.com>
License: <see LICENSE file included with this source code>
Description: Probe — round-trip the state.toml format through replace.reef
******************************************************************************/
import supervisor
import replace
import io.dir as dir
import io.file
import core.str
import sys.env as sysenv
import sys.process as process
mut g_pass: int = 0
mut g_fail: int = 0
proc check(label: string, ok: bool)
if ok
g_pass = g_pass + 1
println(" PASS: " + label)
else
g_fail = g_fail + 1
println(" FAIL: " + label)
end if
end check
fn build_synthetic_table(): supervisor.ServiceTable
let table = supervisor.new_service_table(8)
// sshd: RUNNING with restart_count=2 and last_exit_code=0
let def_sshd = supervisor.make_def_with_name("sshd")
let idx_sshd = supervisor.add_service(table, def_sshd)
supervisor.adopt_runtime(table, idx_sshd, supervisor.STATE_RUNNING(),
100, 23, 2, 0, 1746204051)
// syslogd: RUNNING with no restarts, last_exit_code = -1 (never exited)
let def_syslog = supervisor.make_def_with_name("syslogd")
let idx_syslog = supervisor.add_service(table, def_syslog)
supervisor.adopt_runtime(table, idx_syslog, supervisor.STATE_RUNNING(),
101, 17, 0, 0 - 1, 1746204047)
// cron: STOPPED — must NOT be serialized
let def_cron = supervisor.make_def_with_name("cron")
let idx_cron = supervisor.add_service(table, def_cron)
supervisor.adopt_runtime(table, idx_cron, supervisor.STATE_STOPPED(),
0 - 1, 0 - 1, 0, 0, 0)
return table
end build_synthetic_table
proc test_round_trip()
println("== round-trip ==")
let tmpdir = sysenv.get_env_or("TMPDIR", "/tmp")
let probe_dir = str.concat(tmpdir, "/replace_probe")
let _ = sysenv.set_env("ZYGINIT_RUN_DIR", probe_dir)
if not dir.dir_exists(probe_dir)
let _ = dir.create_dir_all(probe_dir)
end if
let table = build_synthetic_table()
let serialized = replace.serialize_state(table, 1746204000)
check("serialize returns true", serialized)
let recovered = replace.recover_state(1746204000)
check("recover boot_time matches", replace.rs_boot_time(recovered) == 1746204000)
check("recover count == 2 (running only)", replace.rs_count(recovered) == 2)
// Find services by name (order is implementation-defined)
mut found_sshd = false
mut found_syslog = false
mut i = 0
while i < replace.rs_count(recovered)
let svc = replace.rs_service(recovered, i)
let name = replace.svc_name(svc)
if name == "sshd"
found_sshd = true
check("sshd contract_id = 23", replace.svc_contract_id(svc) == 23)
check("sshd restart_count = 2", replace.svc_restart_count(svc) == 2)
check("sshd last_exit_code = 0", replace.svc_last_exit_code(svc) == 0)
check("sshd last_start_time = 1746204051", replace.svc_last_start_time(svc) == 1746204051)
elif name == "syslogd"
found_syslog = true
check("syslogd contract_id = 17", replace.svc_contract_id(svc) == 17)
check("syslogd last_exit_code = -1", replace.svc_last_exit_code(svc) == 0 - 1)
end if
i = i + 1
end while
check("found sshd", found_sshd)
check("found syslogd", found_syslog)
// After recover_state, state.toml is deleted
check("state.toml deleted after recovery", not file.fileExists(replace.STATE_FILE_PATH()))
end test_round_trip
proc test_stale_boot_time()
println("== stale state file ==")
let tmpdir = sysenv.get_env_or("TMPDIR", "/tmp")
let probe_dir = str.concat(tmpdir, "/replace_probe")
let _ = sysenv.set_env("ZYGINIT_RUN_DIR", probe_dir)
if not dir.dir_exists(probe_dir)
let _ = dir.create_dir_all(probe_dir)
end if
let table = build_synthetic_table()
let _ = replace.serialize_state(table, 1746204000)
// Recover with DIFFERENT boot_time -> stale -> empty + deleted
let recovered = replace.recover_state(1746999999)
check("stale file -> empty state", replace.rs_is_empty(recovered))
check("stale file deleted", not file.fileExists(replace.STATE_FILE_PATH()))
end test_stale_boot_time
proc test_missing_file()
println("== missing state file ==")
let tmpdir = sysenv.get_env_or("TMPDIR", "/tmp")
// Unique per-run dir to guarantee no leftover state.toml
let probe_dir = str.concat(tmpdir, "/replace_probe_missing")
let _ = sysenv.set_env("ZYGINIT_RUN_DIR", probe_dir)
if not dir.dir_exists(probe_dir)
let _ = dir.create_dir_all(probe_dir)
end if
// No need to unlink — the dir is fresh per invocation; if a prior
// run left a state.toml here, the boot_time will differ and recovery
// will treat it as stale (exercising a different code path). Either
// way the test asserts "empty state" which holds.
let recovered = replace.recover_state(1746204000)
check("missing-or-stale file -> empty state", replace.rs_is_empty(recovered))
end test_missing_file
proc test_preconditions()
println("== preconditions ==")
let table = supervisor.new_service_table(8)
// sshd RUNNING — stable
let def_sshd = supervisor.make_def_with_name("sshd")
let idx_sshd = supervisor.add_service(table, def_sshd)
supervisor.adopt_runtime(table, idx_sshd, supervisor.STATE_RUNNING(),
100, 23, 0, 0 - 1, 1746204051)
let r1 = replace.check_preconditions(table)
check("all-RUNNING -> empty report", str.length(r1) == 0)
// Add a STOPPING service
let def_cron = supervisor.make_def_with_name("cron")
let idx_cron = supervisor.add_service(table, def_cron)
supervisor.adopt_runtime(table, idx_cron, supervisor.STATE_STOPPING(),
101, 24, 0, 0 - 1, 1746204060)
let r2 = replace.check_preconditions(table)
check("STOPPING -> non-empty report", str.length(r2) > 0)
check("report mentions cron", str.index_of(r2, "cron") >= 0)
check("report mentions stopping", str.index_of(r2, "stopping") >= 0)
// Add a WAITING service — should also be flagged transient
let def_syslogd = supervisor.make_def_with_name("syslogd")
let idx_syslogd = supervisor.add_service(table, def_syslogd)
supervisor.adopt_runtime(table, idx_syslogd, supervisor.STATE_WAITING(),
0 - 1, 0 - 1, 1, 1, 0)
let r3 = replace.check_preconditions(table)
check("WAITING also flagged", str.index_of(r3, "syslogd") >= 0)
check("multiple entries comma-separated", str.index_of(r3, ", ") >= 0)
end test_preconditions
proc main()
test_round_trip()
test_stale_boot_time()
test_missing_file()
test_preconditions()
println("")
println("--- " + int_to_str(g_pass) + " passed, " + int_to_str(g_fail) + " failed ---")
if g_fail > 0
process.exit_now(1)
end if
end main
fn int_to_str(n: int): string
if n == 0
return "0"
end if
mut value = n
mut neg = false
if n < 0
value = 0 - n
neg = true
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 neg
result = str.concat("-", result)
end if
return result
end int_to_str
|