systemd-ml/lib/systemd.ml

234 lines
7.4 KiB
OCaml
Raw Permalink Normal View History

2024-09-24 08:23:56 +00:00
let is_systemd_booted env =
let fs = Eio.Stdenv.fs env in
Eio.Path.( / ) fs "/run/systemd/system/" |> Eio.Path.is_directory
let is_in_systemd () =
begin match Option.bind (Sys.getenv_opt "SYSTEMD_EXEC_PID") int_of_string_opt with
| None -> false
| Some(pid) ->
pid = Unix.getpid ()
end
let invocation_id () =
Sys.getenv "INVOCATION_ID"
let is_journald_attached () =
let stderr = Unix.fstat Unix.stderr in
let dev_ino = Format.sprintf "%d:%d" stderr.st_dev stderr.st_ino in
Sys.getenv_opt "JOURNAL_STREAM" = Some dev_ino
module Dirs = struct
let runtime_dir () =
Sys.getenv "RUNTIME_DIRECTORY"
let state_dir () =
Sys.getenv "STATE_DIRECTORY"
let cache_dir () =
Sys.getenv "CACHE_DIRECTORY"
let logs_dir () =
Sys.getenv "LOGS_DIRECTORY"
let configuration_dir () =
Sys.getenv "CONFIGURATION_DIRECTORY"
let credentials_dir () =
Sys.getenv "CREDENTIALS_DIRECTORY"
end
module Notify = struct
type t = {
sock : Eio_unix.Fd.t;
mono_clock : Eio.Time.Mono.ty Eio.Time.Mono.t;
}
module Private = struct
external unix_code_of_unix_error : Unix.error -> int = "ocaml_systemd_unix_code_of_unix_error"
end
let make ~sw ~env =
(* debatable whether we need a whole fd for this - reference code just opens and closes the socket
each time, and this avoids keeping the fds open the whole time. however, this also means that
if we run out of fds, notify will fail. currently, we just keep the fd open forever *)
if (Eio.Stdenv.backend_id env <> "linux") then failwith "Backend must be Eio_linux";
let orig_addr = Sys.getenv "NOTIFY_SOCKET" in
let addr = Bytes.of_string orig_addr in
begin if Bytes.starts_with ~prefix:("@"[@bytes]) addr then
Bytes.set addr 0 '\x00'
else if not (Bytes.starts_with ~prefix:("/"[@bytes]) addr) then
failwith "Invalid address family"
end;
let sock_unix = Unix.socket ~cloexec:true Unix.PF_UNIX Unix.SOCK_DGRAM 0 in
let sock = Eio_unix.Fd.of_unix ~sw ~seekable:false ~close_unix:true sock_unix in
Eio_linux.Low_level.connect sock (Unix.ADDR_UNIX (String.of_bytes addr));
{ sock = sock; mono_clock = Eio.Stdenv.mono_clock env; }
let notify t ?(fds=[]) msg =
let buf = [(Cstruct.of_string msg)] in
let sent = Eio_linux.Low_level.send_msg ~fds:fds t.sock buf in
assert (sent = Cstruct.lenv buf)
let ready ?(status=None) ?(mainpid=(-1)) t =
let buf = Buffer.create 32 in
Buffer.add_string buf "READY=1\n";
begin match status with
| Some status -> Printf.bprintf buf "STATUS=%s\n" status
| None -> ()
end;
if mainpid > -1 then Printf.bprintf buf "MAINPID=%u\n" mainpid;
notify t (Buffer.contents buf)
let send_status t status =
notify t ("STATUS=" ^ status ^ "\n")
let send_status_errno t status errno =
let errno = Private.unix_code_of_unix_error errno in
let msg = Format.sprintf "STATUS=%s\nERRNO=%d\n" status errno in
notify t msg
let reloading t =
(* this happens to use CLOCK_MONOTONIC on Eio_linux *)
let now = Eio.Time.Mono.now t.mono_clock in
let now_ns = Mtime.to_uint64_ns now in
let now = Int64.div now_ns 1000L in
let msg = Format.sprintf "RELOADING=1\nMONOTONIC_USEC=%Lu\n" now in
notify t msg
let stopping t =
notify t "STOPPING=1\n"
module NotifyAccess = struct
type t =
| None
| Main
| Exec
| All
let to_string = function
| None -> "none"
| Main -> "main"
| Exec -> "exec"
| All -> "all"
end
let update_access t acc =
notify t ("NOTIFYACCESS=" ^ (NotifyAccess.to_string acc) ^ "\n")
let extend_timeout t span =
let nsec = Mtime.Span.to_uint64_ns span in
let usec = Int64.div nsec 1000L in
let msg = Format.sprintf "EXTEND_TIMEOUT_USEC=%Lu\n" usec in
notify t msg
let barrier t =
Eio.Switch.run @@ fun sw ->
let (p0, p1) = Unix.pipe ~cloexec:true () in
let p0 = Eio_unix.Fd.of_unix ~sw ~seekable:false ~close_unix:true p0 in
let p1 = Eio_unix.Fd.of_unix ~sw ~seekable:false ~close_unix:true p1 in
notify t ~fds:[p1] "BARRIER=1\n";
Eio_unix.Fd.close p1;
(* the libsystemd source uses POLLHUP, which isn't exposed via Eio_linux at all, but POLLIN
works to the same end here - it won't return until the pipe is closed (and no one should
be writing anything to it) *)
Eio_linux.Low_level.await_readable p0;
Eio_unix.Fd.close p0
end
module Watchdog = struct
let get_interval_opt () =
match Option.bind (Sys.getenv_opt "WATCHDOG_USEC") Int64.of_string_opt with
| None -> None
| Some usec ->
begin match Option.bind (Sys.getenv_opt "WATCHDOG_PID") int_of_string_opt with
| None -> None
| Some pid ->
if pid = Unix.getpid () then
let nsec = Int64.mul usec 1000L in
Some (Mtime.Span.of_uint64_ns nsec)
else
None
end
let pet t =
Notify.notify t "WATCHDOG=1\n"
let trigger t =
Notify.notify t "WATCHDOG=trigger\n"
let set_watchdog_time t span =
let nsec = Mtime.Span.to_uint64_ns span in
let usec = Int64.div nsec 1000L in
let msg = Format.sprintf "WATCHDOG_USEC=%Lu\n" usec in
Notify.notify t msg
end
module Fdstore = struct
module Private = struct
let sd_listen_fds_start = 3
external unsafe_make_fd : int -> Unix.file_descr = "%identity"
end
let is_valid_fdname s =
let rec iter_chars i = begin
if i = String.length s then
true
else
let dec = String.get_utf_8_uchar s i in
let chr = Uchar.utf_decode_uchar dec |> Uchar.to_int in
let len = Uchar.utf_decode_length dec in
if chr >= 32 && chr <= 126 && chr != 58 then
iter_chars (i + len)
else
false
end in
iter_chars 0
let get_fd_limit () =
match Sys.getenv_opt "FDSTORE" with
| None -> 0
| Some limit -> int_of_string limit
let store t name ?(poll=true) fd =
if not (is_valid_fdname name) then failwith "Invalid fdstore name";
let msg = Format.sprintf ("FDSTORE=1\nFDNAME=%s\n" ^^ (if poll then "" else "FDPOLL=0\n")) name in
Notify.notify t ~fds:[fd] msg
let remove t name =
if not (is_valid_fdname name) then failwith "Invalid fdstore name";
Notify.notify t (Format.sprintf "FDSTOREREMOVE=1\nFDNAME=%s\n" name)
let listen_fds ~sw =
match Option.bind (Sys.getenv_opt "LISTEN_PID") int_of_string_opt with
| None -> []
| Some(pid) ->
if pid = Unix.getpid () then begin
let fd_count = Sys.getenv "LISTEN_FDS" |> int_of_string in
let fd_names = match Sys.getenv_opt "LISTEN_FDNAMES" with
| None -> List.init fd_count (fun _ -> None)
| Some names -> String.split_on_char ':' names |> List.map (fun a -> Some a) in
assert (fd_count = List.length fd_names);
Unix.putenv "LISTEN_FDS" "";
Unix.putenv "LISTEN_FDNAMES" "";
List.mapi (fun i name ->
let fd_num = Private.sd_listen_fds_start + i in
let fd = Private.unsafe_make_fd fd_num in
Unix.set_close_on_exec fd;
let fd = Eio_unix.Fd.of_unix ~sw ~seekable:false ~close_unix:true fd in
(name, fd)
) fd_names
end else []
end
(* we can't do memory pressure because of a lack of exposed pollpri :( *)
module MemPressure = struct
module Private = struct
external ext_memory_trim : unit -> unit = "ocaml_systemd_memory_trim"
end
let memory_trim () =
Gc.compact ();
Private.ext_memory_trim ()
end