// The native half of the spill owner protocol: the facts the verdict needs.
//
// Everything here reads the machine and nothing here decides anything — that is
// `spill_owner.mbt`, where the rules are pure and testable. `/proc` is the whole
// source of truth, which is why a platform without it (macOS) gets the previous
// behaviour: no marker, no reclaiming, no guessing.
///|
/// Small ceiling for the pseudo-files read here. `stat` is a few hundred bytes
/// and a boot id is 36; anything longer means the file is not what was expected.
const PROC_READ_CAP : Int32 = 4096
///|
/// This machine's boot id, or `""` when there is no `/proc` to read it from.
/// Comparing boot ids is what makes "the owner died before the reboot" a fact.
fn boot_id_here() -> String {
match read_proc("/proc/sys/kernel/random/boot_id") {
Some(text) => text.trim().to_owned() // the file ends with a newline
None => ""
}
}
///|
fn read_proc(path : String) -> String? {
@io.read_small_file(path, PROC_READ_CAP)
}
///|
/// What `/proc` says about `pid`, including its start time (field 22 of
/// `/proc//stat`), which is what tells a live owner from a process that
/// merely inherited its number.
fn probe_pid(pid : Int32) -> PidProbe {
if read_proc("/proc/self/stat") is None {
return PidOpaque("there is no /proc on this platform")
}
match proc_starttime(pid) {
Some(starttime) => PidRuns(starttime)
None =>
if @fs.path_exists("/proc/\{pid}") {
PidOpaque("cannot read /proc/\{pid}/stat")
} else {
PidGone
}
}
}
///|
/// Field 22 of `/proc//stat`, as written: an integer count of clock ticks
/// since boot, comparable only with another value from the same boot.
///
/// The second field (`comm`) is parenthesised and may itself contain spaces and
/// parentheses, so the fields after it start after the LAST `)`, not the first.
fn proc_starttime(pid : Int32) -> String? {
let text = match read_proc("/proc/\{pid}/stat") {
Some(t) => t
None => return None
}
let parts : Array[String] = []
for part in text.split(")") {
parts.push(part.to_owned())
}
if parts.length() == 0 {
return None
}
let rest = parts[parts.length() - 1]
// `state` is field 3 and sits where `rest` starts, so field 22 is the 20th
// non-empty piece after it.
let mut index = 0
for field in rest.split(" ") {
let f = field.trim().to_owned()
if f == "" {
continue
}
if index == 19 {
return Some(f)
}
index += 1
}
None
}
///|
/// This process's marker, or `None` when there is no boot id to record (a
/// directory with no marker is kept, so writing a useless one would only add
/// noise to every directory).
fn self_mark() -> OwnerMark? {
match read_proc("/proc/self/stat") {
None => None
Some(_) => {
let boot_id = boot_id_here()
if boot_id == "" {
return None
}
let pid = @io.process_id()
match proc_starttime(pid) {
None => None
Some(starttime) =>
Some({
pid,
boot_id,
starttime,
nonce: self_nonce(),
created_at: @io.unix_now(),
})
}
}
}
}
///|
/// `-`: unique per claim, safe in a file name, and readable in a
/// postmortem. Nothing depends on it being unguessable.
fn self_nonce() -> String {
"\{@io.process_id()}-\{@io.unix_now()}"
}
///|
fn read_marker(path : String) -> String? {
@io.read_small_file(path, PROC_READ_CAP)
}
///|
/// Write a marker, unless the name is already taken.
///
/// A taken name is left as it is — whoever claimed the directory first owns it,
/// and that is not an error worth a line on stderr. Real write errors are
/// reported and swallowed: the marker is bookkeeping, and a query must not fail
/// because of it.
fn write_marker(path : String, text : String) -> Unit {
let _ = @io.write_new_file(path, @utf8.encode(text)) catch {
@io.IOFailure(msg) => {
eprintln("moonlake: warning: cannot write \{path}: \{msg}")
false
}
}
}
///|
fn rename_owner(from : String, to : String) -> Bool {
@io.rename_file(from, to) catch {
@io.IOFailure(msg) => {
eprintln("moonlake: warning: cannot move \{from}: \{msg}")
false
}
}
}