this repo has no description

Compare changes

Choose any two refs to compare.

+4
.gitignore
···
_build
+
*.sh
+
*.sjson
+
*.json
+
*.shl
+20
README.md
···
shelter
-------
+
A shell session shim that makes exploring from the terminal a little bit easier.
+
## Up and running
+
+
To test shelter locally you feel need a ZFS pool, for now you must name it `shelter`.
+
+
```
+
$ truncate --size=10G /var/shelter.img
+
$ sudo zpool create shelter /var/shelter.img
+
$ sudo -E dune exec -- shelter
+
```
+
+
Sometimes you want to just restart the world.
+
+
```
+
$ sudo zpool destroy shelter && sudo zpool create shelter /var/shelter.img && sudo rm -rf ~/.cache/shelter
+
```
+
+
## Shl files
+
+
You can run both the main shelter program and the passthrough mode via a series of actions in a `.shl` file.
-31
cshell.opam
···
-
# This file is generated by dune, edit dune-project instead
-
opam-version: "2.0"
-
synopsis: "A short synopsis"
-
description: "A longer description"
-
maintainer: ["Maintainer Name <maintainer@example.com>"]
-
authors: ["Author Name <author@example.com>"]
-
license: "LICENSE"
-
tags: ["add topics" "to describe" "your" "project"]
-
homepage: "https://github.com/username/reponame"
-
doc: "https://url/to/documentation"
-
bug-reports: "https://github.com/username/reponame/issues"
-
depends: [
-
"dune" {>= "3.17"}
-
"ocaml"
-
"odoc" {with-doc}
-
]
-
build: [
-
["dune" "subst"] {dev}
-
[
-
"dune"
-
"build"
-
"-p"
-
name
-
"-j"
-
jobs
-
"@install"
-
"@runtest" {with-test}
-
"@doc" {with-doc}
-
]
-
]
-
dev-repo: "git+https://github.com/username/reponame.git"
+20 -11
dune-project
···
(lang dune 3.17)
-
(name cshell)
+
(name shelter)
(generate_opam_files true)
(source
(github username/reponame))
-
(authors "Author Name <author@example.com>")
+
(authors "Patrick Ferris <patrick@sirref.org>")
-
(maintainers "Maintainer Name <maintainer@example.com>")
+
(maintainers "Patrick Ferris <patrick@sirref.org>")
-
(license LICENSE)
+
(license ISC)
-
(documentation https://url/to/documentation)
(package
-
(name cshell)
-
(synopsis "A short synopsis")
-
(description "A longer description")
-
(depends ocaml)
+
(name shelter)
+
(synopsis "Shelter from the Storm")
+
(description "A shell session shim to help you explore!")
+
(depends
+
(ocaml (< "5.3.0~~")) ; for the irmin pin only
+
(ctypes (< "0.23.0")) ; for a const ptr mismatch with zfs from https://github.com/yallop/ocaml-ctypes/pull/782
+
eio_posix
+
zfs
+
cid
+
ppx_repr
+
irmin-git
+
morbig
+
ppx_blob
+
cmdliner
+
)
(tags
-
("add topics" "to describe" your project)))
+
("shell")))
-
; See the complete stanza docs at https://dune.readthedocs.io/en/stable/reference/dune-project/index.html
+47
shelter.opam
···
+
# This file is generated by dune, edit dune-project instead
+
opam-version: "2.0"
+
synopsis: "Shelter from the Storm"
+
description: "A shell session shim to help you explore!"
+
maintainer: ["Patrick Ferris <patrick@sirref.org>"]
+
authors: ["Patrick Ferris <patrick@sirref.org>"]
+
license: "ISC"
+
tags: ["shell"]
+
homepage: "https://github.com/username/reponame"
+
bug-reports: "https://github.com/username/reponame/issues"
+
depends: [
+
"dune" {>= "3.17"}
+
"ocaml" {< "5.3.0~~"}
+
"ctypes" {< "0.23.0"}
+
"eio_posix"
+
"zfs"
+
"cid"
+
"ppx_repr"
+
"irmin-git"
+
"morbig"
+
"ppx_blob"
+
"cmdliner"
+
"odoc" {with-doc}
+
]
+
build: [
+
["dune" "subst"] {dev}
+
[
+
"dune"
+
"build"
+
"-p"
+
name
+
"-j"
+
jobs
+
"@install"
+
"@runtest" {with-test}
+
"@doc" {with-doc}
+
]
+
]
+
dev-repo: "git+https://github.com/username/reponame.git"
+
pin-depends:[
+
[ "zfs.dev" "git+https://github.com/patricoferris/ocaml-zfs" ]
+
[ "void.dev" "git+https://github.com/quantifyearth/void" ]
+
[ "irmin.dev" "git+https://github.com/mirage/irmin#eio" ]
+
[ "ppx_irmin.dev" "git+https://github.com/mirage/irmin#eio" ]
+
[ "irmin-git.dev" "git+https://github.com/mirage/irmin#eio" ]
+
[ "irmin-watcher.dev" "git+https://github.com/patricoferris/irmin-watcher#675125d9e95cd09ef0c18ab1d9d6d69a26856b9f" ]
+
]
+8
shelter.opam.template
···
+
pin-depends:[
+
[ "zfs.dev" "git+https://github.com/patricoferris/ocaml-zfs" ]
+
[ "void.dev" "git+https://github.com/quantifyearth/void" ]
+
[ "irmin.dev" "git+https://github.com/mirage/irmin#eio" ]
+
[ "ppx_irmin.dev" "git+https://github.com/mirage/irmin#eio" ]
+
[ "irmin-git.dev" "git+https://github.com/mirage/irmin#eio" ]
+
[ "irmin-watcher.dev" "git+https://github.com/patricoferris/irmin-watcher#675125d9e95cd09ef0c18ab1d9d6d69a26856b9f" ]
+
]
+3 -3
src/bin/dune
···
(executable
-
(public_name cshell)
-
(package cshell)
+
(public_name shelter)
+
(package shelter)
(name main)
-
(libraries cshell fmt.tty cshell.shelter cshell.passthrough eio void))
+
(libraries shelter fmt.tty shelter.main shelter.passthrough eio void))
+67 -11
src/bin/main.ml
···
let merge = Irmin.Merge.default (Repr.option t)
end
-
module Pass = Cshell.Make (History) (Cshell_passthrough)
-
module Shelter = Cshell.Make (Shelter.History) (Shelter)
+
module Pass = Shelter.Make (History) (Shelter_passthrough)
+
module Main = Shelter.Make (Shelter_main.History) (Shelter_main)
let home = Unix.getenv "HOME"
let state_dir fs type' =
-
let path = Eio.Path.(fs / home / ".cache/cshell" / type') in
+
let path = Eio.Path.(fs / home / ".cache/shelter" / type') in
Eio.Path.mkdirs ~exists_ok:true ~perm:0o755 path;
path
+
module Eventloop = struct
+
let run fn =
+
Eio_posix.run @@ fun env ->
+
Lwt_eio.with_event_loop ~clock:env#clock @@ fun _token -> fn env
+
end
+
+
(* Command Line *)
+
open Cmdliner
+
+
let cmd_file =
+
let doc = "Path to a file containing a series of commands." in
+
Arg.(
+
value
+
& opt (some file) None
+
& info [ "f"; "file" ] ~docv:"COMMAND_FILE" ~doc)
+
+
let main =
+
let run config cmd_file =
+
Eventloop.run @@ fun env ->
+
let cmd_file = Option.map (Eio.Path.( / ) env#fs) cmd_file in
+
let dir = state_dir env#fs "shelter" in
+
let stdout = (env#stdout :> Eio.Flow.sink_ty Eio.Flow.sink) in
+
Main.main config ~stdout env#fs env#clock env#process_mgr dir cmd_file
+
in
+
let t = Term.(const run $ Shelter_main.config_term $ cmd_file) in
+
let man =
+
[
+
`P
+
"Shelter is a shell session shim to help control uncertainty when \
+
working from the terminal";
+
]
+
in
+
let doc = "Shelter: version-controlled shell sessions" in
+
let info = Cmd.info ~man ~doc "main" in
+
(Cmd.v info t, t, info)
+
+
let passthrough =
+
let run config cmd_file =
+
Eventloop.run @@ fun env ->
+
let cmd_file = Option.map (Eio.Path.( / ) env#fs) cmd_file in
+
let dir = state_dir env#fs "passthrough" in
+
let stdout = (env#stdout :> Eio.Flow.sink_ty Eio.Flow.sink) in
+
Pass.main config ~stdout env#fs env#clock env#process_mgr dir cmd_file
+
in
+
let t = Term.(const run $ Shelter_passthrough.config_term $ cmd_file) in
+
let info = Cmd.info "passthrough" in
+
Cmd.v info t
+
+
let extract_commands =
+
let run cmd_file =
+
Eventloop.run @@ fun env ->
+
let cmd_file = Eio.Path.( / ) env#fs (Option.get cmd_file) in
+
Shelter.Script.to_commands cmd_file |> List.iter (Fmt.pr "%s\n")
+
in
+
let t = Term.(const run $ cmd_file) in
+
let info = Cmd.info "extract" in
+
Cmd.v info t
+
+
let cmds =
+
let cmd, term, info = main in
+
let cmds = [ cmd; passthrough; extract_commands ] in
+
Cmd.group ~default:term info cmds
+
let () =
-
Eio_posix.run @@ fun env ->
Fmt_tty.setup_std_outputs ();
-
match Sys.argv.(1) with
-
| "shelter" ->
-
let dir = state_dir env#fs "shelter" in
-
Shelter.main env#fs env#clock env#process_mgr dir
-
| _ | (exception Invalid_argument _) ->
-
let dir = state_dir env#fs "passthrough" in
-
Pass.main env#fs env#clock env#process_mgr dir
+
exit (Cmd.eval cmds)
-33
src/lib/cshell.ml
···
-
module History = History
-
module Engine = Engine
-
-
module Make (H : History.S) (Engine : Engine.S with type entry = H.t) = struct
-
module Store = Irmin_fs_unix.KV.Make (H)
-
-
let run fs clock proc store =
-
let store = History.Store ((module Store), store) in
-
let initial_ctx = Engine.init fs proc store in
-
let rec loop store ctx exit_code =
-
let prompt = Engine.prompt exit_code store in
-
match LNoise.linenoise prompt with
-
| None -> ()
-
| Some input -> (
-
let action = Engine.action_of_command input in
-
match Engine.run fs clock proc (store, ctx) action with
-
| Error (Eio.Process.Child_error exit_code) ->
-
Fmt.epr "%a\n%!" Eio.Process.pp_status exit_code;
-
loop store ctx exit_code
-
| Error (Eio.Process.Executable_not_found m) ->
-
Fmt.epr "cshell: excutable not found %s\n%!" m;
-
loop store ctx (`Exited 127)
-
| Ok (store, ctx) -> loop store ctx (`Exited 0))
-
in
-
loop store initial_ctx (`Exited 0)
-
-
let main fs clock proc directory =
-
Irmin_fs.run directory @@ fun () ->
-
let conf = Irmin_fs.config (Eio.Path.native_exn directory) in
-
let repo = Store.Repo.v conf in
-
let store = Store.main repo in
-
run fs clock proc store
-
end
+3 -3
src/lib/dune
···
(library
-
(name cshell)
-
(public_name cshell)
-
(libraries irmin-fs.unix eio.unix eio linenoise void repr))
+
(name shelter)
+
(public_name shelter)
+
(libraries cmdliner irmin-git.unix eio.unix eio linenoise void repr morbig))
+17 -2
src/lib/engine.ml
···
module type S = sig
+
type config
+
(** A configuration *)
+
+
val config_term : config Cmdliner.Term.t
+
(** A cmdliner term for constructing a config *)
+
type action
(** An action to run *)
···
(** A context that is not persisted, but is passed through each loop of the
shell *)
+
type error
+
(** Shell specific errors *)
+
+
val pp_error : error Fmt.t
+
val init :
_ Eio.Path.t ->
Eio_unix.Process.mgr_ty Eio_unix.Process.mgr ->
···
to setup history completions etc. with LNoise. *)
val run :
-
_ Eio.Path.t ->
+
config ->
+
stdout:Eio.Flow.sink_ty Eio.Flow.sink ->
+
Eio.Fs.dir_ty Eio.Path.t ->
_ Eio.Time.clock ->
Eio_unix.Process.mgr_ty Eio_unix.Process.mgr ->
entry History.t * ctx ->
action ->
-
(entry History.t * ctx, Eio.Process.error) result
+
( entry History.t * ctx,
+
[ `Process of Eio.Process.error | `Shell of error ] )
+
result
(** [run history action] runs the action in [history]. Return a new [history]
that can be persisted *)
-56
src/lib/passthrough/cshell_passthrough.ml
···
-
open Eio
-
-
type action = Exec of string [@@deriving repr]
-
-
let action = action_t
-
let action_of_command cmd = Exec cmd
-
-
type entry = string [@@derviving repr]
-
-
let () = Fmt.set_style_renderer Format.str_formatter `Ansi_tty
-
-
let prompt _ _ =
-
Fmt.(styled (`Fg `Red) string) Format.str_formatter "cshell> ";
-
Format.flush_str_formatter ()
-
-
let history_key = [ "history" ]
-
let key () = history_key @ [ string_of_float @@ Unix.gettimeofday () ]
-
-
type ctx = unit
-
-
let init _ _ (Cshell.History.Store ((module S), store) : entry Cshell.History.t)
-
=
-
match S.list store history_key with
-
| [] -> ()
-
| xs ->
-
let rec loop acc = function
-
| `Contents (v, _meta) :: next -> loop (v :: acc) next
-
| _ :: next -> loop acc next
-
| [] -> List.rev acc
-
in
-
let entries =
-
loop [] (List.map (fun (_, tree) -> S.Tree.to_concrete tree) xs)
-
in
-
List.iter (fun v -> LNoise.history_add v |> ignore) entries
-
-
let run _fs clock proc
-
( ((Cshell.History.Store ((module S), store) : entry Cshell.History.t) as
-
full_store),
-
() ) (Exec command) =
-
let info () =
-
S.Info.v ~message:"cshell" (Eio.Time.now clock |> Int64.of_float)
-
in
-
let cmd =
-
String.split_on_char ' ' command
-
|> List.filter (fun v -> not (String.equal "" v))
-
in
-
Switch.run @@ fun sw ->
-
try
-
let proc = Eio.Process.spawn ~sw proc cmd in
-
let res = Eio.Process.await proc in
-
if res = `Exited 0 then (
-
S.set_exn ~info store (key ()) command;
-
let _ : (unit, string) result = LNoise.history_add command in
-
Ok (full_store, ()))
-
else Error (Eio.Process.Child_error res)
-
with Eio.Exn.Io (Eio.Process.E e, _) -> Error e
-1
src/lib/passthrough/cshell_passthrough.mli
···
-
include Cshell.Engine.S with type entry = string
+3 -3
src/lib/passthrough/dune
···
(library
-
(name cshell_passthrough)
-
(public_name cshell.passthrough)
+
(name shelter_passthrough)
+
(public_name shelter.passthrough)
(preprocess
(pps ppx_repr))
-
(libraries cshell))
+
(libraries shelter))
+64
src/lib/passthrough/shelter_passthrough.ml
···
+
open Eio
+
+
type error = string
+
+
let pp_error = Fmt.string
+
+
type config = unit
+
+
let config_term = Cmdliner.Term.const ()
+
+
type action = Exec of string [@@deriving repr]
+
+
let action = action_t
+
let action_of_command cmd = Exec cmd
+
+
type entry = string [@@derviving repr]
+
+
let () = Fmt.set_style_renderer Format.str_formatter `Ansi_tty
+
+
let prompt _ _ =
+
Fmt.(styled (`Fg `Red) string) Format.str_formatter "shelter-p> ";
+
Format.flush_str_formatter ()
+
+
let history_key = [ "history" ]
+
let key () = history_key @ [ string_of_float @@ Unix.gettimeofday () ]
+
+
type ctx = unit
+
+
let init _ _
+
(Shelter.History.Store ((module S), store) : entry Shelter.History.t) =
+
match S.list store history_key with
+
| [] -> ()
+
| xs ->
+
let rec loop acc = function
+
| `Contents (v, _meta) :: next -> loop (v :: acc) next
+
| _ :: next -> loop acc next
+
| [] -> List.rev acc
+
in
+
let entries =
+
loop [] (List.map (fun (_, tree) -> S.Tree.to_concrete tree) xs)
+
in
+
List.iter (fun v -> LNoise.history_add v |> ignore) entries
+
+
let run (() : config) ~stdout:_ _fs clock proc
+
( ((Shelter.History.Store ((module S), store) : entry Shelter.History.t) as
+
full_store),
+
() ) (Exec command) =
+
let info () =
+
S.Info.v ~message:"shelter" (Eio.Time.now clock |> Int64.of_float)
+
in
+
let cmd =
+
String.split_on_char ' ' command
+
|> List.filter (fun v -> not (String.equal "" v))
+
in
+
Switch.run @@ fun sw ->
+
try
+
let proc = Eio.Process.spawn ~sw proc cmd in
+
let res = Eio.Process.await proc in
+
if res = `Exited 0 then (
+
S.set_exn ~info store (key ()) command;
+
let _ : (unit, string) result = LNoise.history_add command in
+
Ok (full_store, ()))
+
else Shelter.process_error (Eio.Process.Child_error res)
+
with Eio.Exn.Io (Eio.Process.E e, _) -> Shelter.process_error e
+1
src/lib/passthrough/shelter_passthrough.mli
···
+
include Shelter.Engine.S with type entry = string
+57
src/lib/script.ml
···
+
module Cst = Morbig.CST
+
+
let redirect_to_string = function
+
| Cst.IoRedirect_IoFile { value = io_file; _ } -> (
+
match io_file with
+
| Cst.IoFile_Great_FileName
+
{ value = Cst.Filename_Word { value = Cst.Word (w, _); _ }; _ } ->
+
Fmt.str "> %s" w
+
| Cst.IoFile_DGreat_FileName
+
{ value = Cst.Filename_Word { value = Cst.Word (w, _); _ }; _ } ->
+
Fmt.str ">> %s" w
+
| _ -> failwith "Redirect Unsupported")
+
| _ -> failwith "IO Redirect Unsupported"
+
+
let cmd_suffix_to_list s =
+
let rec loop = function
+
| Cst.CmdSuffix_Word { value = Cst.Word (s, _); _ } -> [ s ]
+
| Cst.CmdSuffix_CmdSuffix_Word (suff, { value = Cst.Word (s, _); _ }) ->
+
s :: loop suff.value
+
| Cst.CmdSuffix_CmdSuffix_IoRedirect (suff, { value = redirect; _ }) ->
+
let sf = loop suff.value in
+
redirect_to_string redirect :: sf
+
| _ -> failwith "Unsupported!"
+
in
+
loop s |> List.rev |> String.concat " "
+
+
let of_cmd (c : Cst.command) =
+
match c with
+
| Cst.Command_SimpleCommand simple -> (
+
match simple.value with
+
| Cst.SimpleCommand_CmdName
+
{ value = Cst.CmdName_Word { value = Cst.Word (w, _); _ }; _ } ->
+
w
+
| Cst.SimpleCommand_CmdName_CmdSuffix
+
( { value = Cst.CmdName_Word { value = Cst.Word (w, _); _ }; _ },
+
{ value = suff; _ } ) ->
+
let s = cmd_suffix_to_list suff in
+
w ^ " " ^ s
+
| _ -> failwith "Unsupported")
+
| _ -> failwith "Unsupported"
+
+
let cmds_to_strings =
+
let v =
+
object
+
inherit [_] Morbig.CSTVisitors.reduce
+
method zero = []
+
method plus = List.append
+
method! visit_command acc c = of_cmd c :: acc
+
end
+
in
+
v#visit_program []
+
+
let to_commands file =
+
let contents = Eio.Path.load file in
+
let name = Eio.Path.native_exn file |> Filename.basename in
+
let ast = Morbig.parse_string name contents in
+
cmds_to_strings ast
+34
src/lib/shelter/config.ml
···
+
type t = {
+
no_diffing : bool;
+
no_ebpf : bool;
+
no_runc : bool;
+
image : string;
+
shell : string;
+
}
+
+
let cmdliner =
+
let open Cmdliner in
+
let no_diffing =
+
let doc = "Disable diffing." in
+
Arg.(value & flag & info [ "no-diffing" ] ~doc)
+
in
+
let no_ebpf =
+
let doc = "Disable eBPF." in
+
Arg.(value & flag & info [ "no-ebpf" ] ~doc)
+
in
+
let image =
+
let doc = "Base image to start Shelter with." in
+
Arg.(value & opt string "alpine" & info [ "image" ] ~doc)
+
in
+
let shell =
+
let doc = "Path to the shell (e.g. /bin/ash)" in
+
Arg.(value & opt string "/bin/ash" & info [ "shell" ] ~doc)
+
in
+
let no_runc =
+
let doc = "Disable RUNC and use Void processes." in
+
Arg.(value & flag & info [ "no-runc" ] ~doc)
+
in
+
Term.(
+
const (fun no_diffing no_ebpf no_runc image shell ->
+
{ no_diffing; no_ebpf; no_runc; image; shell })
+
$ no_diffing $ no_ebpf $ no_runc $ image $ shell)
+11 -4
src/lib/shelter/dune
···
+
; (rule
+
; (target opentrace)
+
; (action
+
; (with-stdout-to opentrace (run echo hello))))
+
(library
-
(name shelter)
-
(public_name cshell.shelter)
+
(name shelter_main)
+
(public_name shelter.main)
+
(preprocessor_deps
+
(file opentrace))
(preprocess
-
(pps ppx_repr))
-
(libraries cshell cid void zfs))
+
(pps ppx_repr ppx_blob))
+
(libraries shelter cid void zfs))
+29 -2
src/lib/shelter/fetch.ml
···
let ( / ) = Eio.Path.( / )
+
let replace_slash s = String.split_on_char '/' s |> String.concat "-"
+
+
let get_user proc image =
+
Eio.Process.parse_out proc Eio.Buf_read.take_all
+
[
+
"docker";
+
"image";
+
"inspect";
+
"--format";
+
{|{{.Config.User}}|};
+
"--";
+
image;
+
]
+
|> String.trim
+
+
let get_env proc image =
+
Eio.Process.parse_out proc Eio.Buf_read.take_all
+
[
+
"docker";
+
"image";
+
"inspect";
+
"--format";
+
{|{{range .Config.Env}}{{print . "\x00"}}{{end}}|};
+
"--";
+
image;
+
]
+
|> String.split_on_char '\x00'
let get_image ~dir ~proc image =
let container_id =
···
[ "docker"; "run"; "-d"; image ]
|> String.trim
in
-
let tar = image ^ ".tar.gz" in
+
let tar = replace_slash image ^ ".tar.gz" in
let dir_s = Eio.Path.native_exn dir in
let () =
Eio.Process.run proc
···
[
"tar";
"-xf";
-
Filename.concat dir_s "alpine.tar.gz";
+
Filename.concat dir_s tar;
"-C";
Filename.concat dir_s "rootfs";
]
src/lib/shelter/opentrace

This is a binary file and will not be displayed.

+498
src/lib/shelter/runc.ml
···
+
(* From Obuilder see License file at end of document *)
+
let ( / ) = Eio.Path.( / )
+
let ( // ) = Filename.concat
+
+
type config = { fast_sync : bool }
+
+
let get_machine () =
+
let ch = Unix.open_process_in "uname -m" in
+
let arch = input_line ch in
+
match Unix.close_process_in ch with
+
| Unix.WEXITED 0 -> String.trim arch
+
| _ -> failwith "Failed to get arch with 'uname -m'"
+
+
let get_arches () =
+
if Sys.unix then
+
match get_machine () with
+
| "x86_64" -> [ "SCMP_ARCH_X86_64"; "SCMP_ARCH_X86"; "SCMP_ARCH_X32" ]
+
| "aarch64" -> [ "SCMP_ARCH_AARCH64"; "SCMP_ARCH_ARM" ]
+
| _ -> []
+
else []
+
+
let secret_file id = "secret-" ^ string_of_int id
+
+
module Json_config = struct
+
let mount ?(options = []) ~ty ~src dst =
+
`Assoc
+
[
+
("destination", `String dst);
+
("type", `String ty);
+
("source", `String src);
+
("options", `List (List.map (fun x -> `String x) options));
+
]
+
+
type mount = { ty : [ `Bind ]; src : string; dst : string; readonly : bool }
+
+
let user_mounts =
+
List.map @@ fun { ty; src; dst; readonly } ->
+
assert (ty = `Bind);
+
let options = [ "bind"; "nosuid"; "nodev" ] in
+
mount ~ty:"bind" ~src dst
+
~options:(if readonly then "ro" :: options else options)
+
+
let strings xs = `List (List.map (fun x -> `String x) xs)
+
let namespace x = `Assoc [ ("type", `String x) ]
+
+
(* This is a subset of the capabilities that Docker uses by default.
+
These control what root can do in the container.
+
If the init process is non-root, permitted, effective and ambient sets are cleared.
+
See capabilities(7) for full details. *)
+
let default_linux_caps =
+
[
+
"CAP_CHOWN";
+
(* Make arbitrary changes to file UIDs and GIDs *)
+
"CAP_DAC_OVERRIDE";
+
(* Bypass file read, write, and execute permission checks. *)
+
"CAP_FSETID";
+
(* Set SUID/SGID bits. *)
+
"CAP_FOWNER";
+
(* Bypass permission checks. *)
+
"CAP_MKNOD";
+
(* Create special files using mknod. *)
+
"CAP_SETGID";
+
(* Make arbitrary manipulations of process GIDs. *)
+
"CAP_SETUID";
+
(* Make arbitrary manipulations of process UIDs. *)
+
"CAP_SETFCAP";
+
(* Set arbitrary capabilities on a file. *)
+
"CAP_SETPCAP";
+
(* Add any capability from bounding set to inheritable set. *)
+
"CAP_SYS_CHROOT";
+
(* Use chroot. *)
+
"CAP_KILL";
+
(* Bypass permission checks for sending signals. *)
+
"CAP_AUDIT_WRITE"
+
(* Write records to kernel auditing log. *)
+
(* Allowed by Docker, but disabled here (because we use host networking):
+
"CAP_NET_RAW"; (* Use RAW and PACKET sockets / bind to any address *)
+
"CAP_NET_BIND_SERVICE"; (* Bind a socket to Internet domain privileged ports. *)
+
*);
+
]
+
+
let seccomp_syscalls ~fast_sync =
+
if fast_sync then
+
[
+
`Assoc
+
[
+
(* Sync calls are pointless for the builder, because if the computer crashes then we'll
+
just throw the build dir away and start again. And btrfs sync is really slow.
+
Based on https://bblank.thinkmo.de/using-seccomp-to-filter-sync-operations.html
+
Note: requires runc >= v1.0.0-rc92. *)
+
( "names",
+
strings
+
[
+
"fsync";
+
"fdatasync";
+
"msync";
+
"sync";
+
"syncfs";
+
"sync_file_range";
+
] );
+
("action", `String "SCMP_ACT_ERRNO");
+
("errnoRet", `Int 0);
+
(* Return error "success" *)
+
];
+
]
+
else []
+
+
let seccomp_policy =
+
let fields =
+
[
+
("defaultAction", `String "SCMP_ACT_ALLOW");
+
("syscalls", `List (seccomp_syscalls ~fast_sync:true));
+
]
+
in
+
`Assoc fields
+
+
type config = {
+
cwd : string;
+
argv : string list;
+
hostname : string;
+
network : string list;
+
user : int * int;
+
env : string list;
+
mounts : mount list;
+
entrypoint : string option;
+
}
+
+
let make { cwd; argv; hostname; network; user; env; mounts; entrypoint }
+
~config_dir ~results_dir : Yojson.Safe.t =
+
assert (entrypoint = None);
+
let user =
+
let uid, gid = user in
+
`Assoc [ ("uid", `Int uid); ("gid", `Int gid) ]
+
in
+
let network_ns =
+
match network with
+
| [ "host" ] -> []
+
| [] -> [ "network" ]
+
| xs ->
+
Fmt.failwith "Unsupported network configuration %a"
+
Fmt.Dump.(list string)
+
xs
+
in
+
let namespaces = network_ns @ [ "pid"; "ipc"; "uts"; "mount" ] in
+
`Assoc
+
[
+
("ociVersion", `String "1.0.1-dev");
+
( "process",
+
`Assoc
+
[
+
("terminal", `Bool false);
+
("user", user);
+
("args", strings argv);
+
("env", strings env);
+
("cwd", `String cwd);
+
( "capabilities",
+
`Assoc
+
[
+
("bounding", strings default_linux_caps);
+
(* Limits capabilities gained on execve. *)
+
("effective", strings default_linux_caps);
+
(* Checked by kernel to decide access *)
+
("inheritable", strings default_linux_caps);
+
(* Preserved across an execve (if root, or cap in ambient set) *)
+
("permitted", strings default_linux_caps);
+
(* Limiting superset for the effective capabilities *)
+
] );
+
( "rlimits",
+
`List
+
[
+
`Assoc
+
[
+
("type", `String "RLIMIT_NOFILE");
+
("hard", `Int 10_024);
+
("soft", `Int 10_024);
+
];
+
`Assoc
+
[
+
("type", `String "RLIMIT_MEMLOCK");
+
("hard", `Int 1_000_000);
+
("soft", `Int 1_000_000);
+
];
+
] );
+
("noNewPrivileges", `Bool false);
+
] );
+
( "root",
+
`Assoc
+
[
+
("path", `String (results_dir // "rootfs"));
+
("readonly", `Bool false);
+
] );
+
("hostname", `String hostname);
+
( "mounts",
+
`List
+
(mount "/proc"
+
~options:
+
[ (* TODO: copy to others? *) "nosuid"; "noexec"; "nodev" ]
+
~ty:"proc" ~src:"proc"
+
:: mount "/dev" ~ty:"tmpfs" ~src:"tmpfs"
+
~options:
+
[ "nosuid"; "strictatime"; "mode=755"; "size=65536k" ]
+
:: mount "/dev/pts" ~ty:"devpts" ~src:"devpts"
+
~options:
+
[
+
"nosuid";
+
"noexec";
+
"newinstance";
+
"ptmxmode=0666";
+
"mode=0620";
+
"gid=5";
+
(* tty *)
+
]
+
:: mount
+
"/sys"
+
(* This is how Docker does it. runc's default is a bit different. *)
+
~ty:"sysfs" ~src:"sysfs"
+
~options:[ "nosuid"; "noexec"; "nodev"; "ro" ]
+
:: mount "/sys/fs/cgroup" ~ty:"cgroup" ~src:"cgroup"
+
~options:[ "ro"; "nosuid"; "noexec"; "nodev" ]
+
:: mount "/dev/shm" ~ty:"tmpfs" ~src:"shm"
+
~options:
+
[ "nosuid"; "noexec"; "nodev"; "mode=1777"; "size=65536k" ]
+
:: mount "/dev/mqueue" ~ty:"mqueue" ~src:"mqueue"
+
~options:[ "nosuid"; "noexec"; "nodev" ]
+
:: mount "/etc/hosts" ~ty:"bind" ~src:(config_dir // "hosts")
+
~options:[ "ro"; "rbind"; "rprivate" ]
+
::
+
(if network = [ "host" ] then
+
[
+
mount "/etc/resolv.conf" ~ty:"bind" ~src:"/etc/resolv.conf"
+
~options:[ "ro"; "rbind"; "rprivate" ];
+
]
+
else [])
+
@ user_mounts mounts) );
+
( "linux",
+
`Assoc
+
[
+
("namespaces", `List (List.map namespace namespaces));
+
( "maskedPaths",
+
strings
+
[
+
"/proc/acpi";
+
"/proc/asound";
+
"/proc/kcore";
+
"/proc/keys";
+
"/proc/latency_stats";
+
"/proc/timer_list";
+
"/proc/timer_stats";
+
"/proc/sched_debug";
+
"/sys/firmware";
+
"/proc/scsi";
+
] );
+
( "readonlyPaths",
+
strings
+
[
+
"/proc/bus";
+
"/proc/fs";
+
"/proc/irq";
+
"/proc/sys";
+
"/proc/sysrq-trigger";
+
] );
+
("seccomp", seccomp_policy);
+
] );
+
]
+
end
+
+
let next_id = ref 0
+
+
let to_other_sink_as_well ~other
+
(Eio.Resource.T (t, handler) : Eio.Flow.sink_ty Eio.Flow.sink) =
+
let module Sink = (val Eio.Resource.get handler Eio.Flow.Pi.Sink) in
+
let copy_buf = Buffer.create 128 in
+
let copy () ~src =
+
Eio.Flow.copy src (Eio.Flow.buffer_sink copy_buf);
+
Eio.Flow.copy_string (Buffer.contents copy_buf) other;
+
Sink.copy t ~src:(Buffer.contents copy_buf |> Eio.Flow.string_source);
+
Buffer.clear copy_buf
+
in
+
let single_write () x =
+
let _ : int = Eio.Flow.single_write other x in
+
Sink.single_write t x
+
in
+
let module T = struct
+
type t = unit
+
+
let single_write = single_write
+
let copy = copy
+
end in
+
Eio.Resource.T ((), Eio.Flow.Pi.sink (module T))
+
+
let spawn ~sw log env config dir =
+
let tmp = Filename.temp_dir ~perms:0o700 "shelter-run-" "" in
+
let eio_tmp = Eio.Path.(env#fs / tmp) in
+
let json_config = Json_config.make config ~config_dir:tmp ~results_dir:dir in
+
Eio.Path.save ~create:(`If_missing 0o644) (eio_tmp / "config.json")
+
(Yojson.Safe.pretty_to_string json_config ^ "\n");
+
Eio.Path.save ~create:(`If_missing 0o644) (eio_tmp / "hosts")
+
"127.0.0.1 localhost builder";
+
let id = string_of_int !next_id in
+
incr next_id;
+
let cmd = [ "runc"; "--root"; "runc"; "run"; id ] in
+
let stdout =
+
to_other_sink_as_well ~other:env#stdout
+
(log :> Eio.Flow.sink_ty Eio.Flow.sink)
+
in
+
Eio.Process.spawn ~sw ~stdout env#proc ~cwd:eio_tmp cmd
+
+
(*
+
Apache License
+
Version 2.0, January 2004
+
https://www.apache.org/licenses/
+
+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+
1. Definitions.
+
+
"License" shall mean the terms and conditions for use, reproduction,
+
and distribution as defined by Sections 1 through 9 of this document.
+
+
"Licensor" shall mean the copyright owner or entity authorized by
+
the copyright owner that is granting the License.
+
+
"Legal Entity" shall mean the union of the acting entity and all
+
other entities that control, are controlled by, or are under common
+
control with that entity. For the purposes of this definition,
+
"control" means (i) the power, direct or indirect, to cause the
+
direction or management of such entity, whether by contract or
+
otherwise, or (ii) ownership of fifty percent (50%) or more of the
+
outstanding shares, or (iii) beneficial ownership of such entity.
+
+
"You" (or "Your") shall mean an individual or Legal Entity
+
exercising permissions granted by this License.
+
+
"Source" form shall mean the preferred form for making modifications,
+
including but not limited to software source code, documentation
+
source, and configuration files.
+
+
"Object" form shall mean any form resulting from mechanical
+
transformation or translation of a Source form, including but
+
not limited to compiled object code, generated documentation,
+
and conversions to other media types.
+
+
"Work" shall mean the work of authorship, whether in Source or
+
Object form, made available under the License, as indicated by a
+
copyright notice that is included in or attached to the work
+
(an example is provided in the Appendix below).
+
+
"Derivative Works" shall mean any work, whether in Source or Object
+
form, that is based on (or derived from) the Work and for which the
+
editorial revisions, annotations, elaborations, or other modifications
+
represent, as a whole, an original work of authorship. For the purposes
+
of this License, Derivative Works shall not include works that remain
+
separable from, or merely link (or bind by name) to the interfaces of,
+
the Work and Derivative Works thereof.
+
+
"Contribution" shall mean any work of authorship, including
+
the original version of the Work and any modifications or additions
+
to that Work or Derivative Works thereof, that is intentionally
+
submitted to Licensor for inclusion in the Work by the copyright owner
+
or by an individual or Legal Entity authorized to submit on behalf of
+
the copyright owner. For the purposes of this definition, "submitted"
+
means any form of electronic, verbal, or written communication sent
+
to the Licensor or its representatives, including but not limited to
+
communication on electronic mailing lists, source code control systems,
+
and issue tracking systems that are managed by, or on behalf of, the
+
Licensor for the purpose of discussing and improving the Work, but
+
excluding communication that is conspicuously marked or otherwise
+
designated in writing by the copyright owner as "Not a Contribution."
+
+
"Contributor" shall mean Licensor and any individual or Legal Entity
+
on behalf of whom a Contribution has been received by Licensor and
+
subsequently incorporated within the Work.
+
+
2. Grant of Copyright License. Subject to the terms and conditions of
+
this License, each Contributor hereby grants to You a perpetual,
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+
copyright license to reproduce, prepare Derivative Works of,
+
publicly display, publicly perform, sublicense, and distribute the
+
Work and such Derivative Works in Source or Object form.
+
+
3. Grant of Patent License. Subject to the terms and conditions of
+
this License, each Contributor hereby grants to You a perpetual,
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+
(except as stated in this section) patent license to make, have made,
+
use, offer to sell, sell, import, and otherwise transfer the Work,
+
where such license applies only to those patent claims licensable
+
by such Contributor that are necessarily infringed by their
+
Contribution(s) alone or by combination of their Contribution(s)
+
with the Work to which such Contribution(s) was submitted. If You
+
institute patent litigation against any entity (including a
+
cross-claim or counterclaim in a lawsuit) alleging that the Work
+
or a Contribution incorporated within the Work constitutes direct
+
or contributory patent infringement, then any patent licenses
+
granted to You under this License for that Work shall terminate
+
as of the date such litigation is filed.
+
+
4. Redistribution. You may reproduce and distribute copies of the
+
Work or Derivative Works thereof in any medium, with or without
+
modifications, and in Source or Object form, provided that You
+
meet the following conditions:
+
+
(a) You must give any other recipients of the Work or
+
Derivative Works a copy of this License; and
+
+
(b) You must cause any modified files to carry prominent notices
+
stating that You changed the files; and
+
+
(c) You must retain, in the Source form of any Derivative Works
+
that You distribute, all copyright, patent, trademark, and
+
attribution notices from the Source form of the Work,
+
excluding those notices that do not pertain to any part of
+
the Derivative Works; and
+
+
(d) If the Work includes a "NOTICE" text file as part of its
+
distribution, then any Derivative Works that You distribute must
+
include a readable copy of the attribution notices contained
+
within such NOTICE file, excluding those notices that do not
+
pertain to any part of the Derivative Works, in at least one
+
of the following places: within a NOTICE text file distributed
+
as part of the Derivative Works; within the Source form or
+
documentation, if provided along with the Derivative Works; or,
+
within a display generated by the Derivative Works, if and
+
wherever such third-party notices normally appear. The contents
+
of the NOTICE file are for informational purposes only and
+
do not modify the License. You may add Your own attribution
+
notices within Derivative Works that You distribute, alongside
+
or as an addendum to the NOTICE text from the Work, provided
+
that such additional attribution notices cannot be construed
+
as modifying the License.
+
+
You may add Your own copyright statement to Your modifications and
+
may provide additional or different license terms and conditions
+
for use, reproduction, or distribution of Your modifications, or
+
for any such Derivative Works as a whole, provided Your use,
+
reproduction, and distribution of the Work otherwise complies with
+
the conditions stated in this License.
+
+
5. Submission of Contributions. Unless You explicitly state otherwise,
+
any Contribution intentionally submitted for inclusion in the Work
+
by You to the Licensor shall be under the terms and conditions of
+
this License, without any additional terms or conditions.
+
Notwithstanding the above, nothing herein shall supersede or modify
+
the terms of any separate license agreement you may have executed
+
with Licensor regarding such Contributions.
+
+
6. Trademarks. This License does not grant permission to use the trade
+
names, trademarks, service marks, or product names of the Licensor,
+
except as required for reasonable and customary use in describing the
+
origin of the Work and reproducing the content of the NOTICE file.
+
+
7. Disclaimer of Warranty. Unless required by applicable law or
+
agreed to in writing, Licensor provides the Work (and each
+
Contributor provides its Contributions) on an "AS IS" BASIS,
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+
implied, including, without limitation, any warranties or conditions
+
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+
PARTICULAR PURPOSE. You are solely responsible for determining the
+
appropriateness of using or redistributing the Work and assume any
+
risks associated with Your exercise of permissions under this License.
+
+
8. Limitation of Liability. In no event and under no legal theory,
+
whether in tort (including negligence), contract, or otherwise,
+
unless required by applicable law (such as deliberate and grossly
+
negligent acts) or agreed to in writing, shall any Contributor be
+
liable to You for damages, including any direct, indirect, special,
+
incidental, or consequential damages of any character arising as a
+
result of this License or out of the use or inability to use the
+
Work (including but not limited to damages for loss of goodwill,
+
work stoppage, computer failure or malfunction, or any and all
+
other commercial damages or losses), even if such Contributor
+
has been advised of the possibility of such damages.
+
+
9. Accepting Warranty or Additional Liability. While redistributing
+
the Work or Derivative Works thereof, You may choose to offer,
+
and charge a fee for, acceptance of support, warranty, indemnity,
+
or other liability obligations and/or rights consistent with this
+
License. However, in accepting such obligations, You may act only
+
on Your own behalf and on Your sole responsibility, not on behalf
+
of any other Contributor, and only if You agree to indemnify,
+
defend, and hold each Contributor harmless for any liability
+
incurred by, or claims asserted against, such Contributor by reason
+
of your accepting any such warranty or additional liability.
+
+
END OF TERMS AND CONDITIONS
+
+
Copyright 2020 Thomas Leonard
+
+
Licensed under the Apache License, Version 2.0 (the "License");
+
you may not use this file except in compliance with the License.
+
You may obtain a copy of the License at
+
+
https://www.apache.org/licenses/LICENSE-2.0
+
+
Unless required by applicable law or agreed to in writing, software
+
distributed under the License is distributed on an "AS IS" BASIS,
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+
See the License for the specific language governing permissions and
+
limitations under the License. *)
-292
src/lib/shelter/shelter.ml
···
-
open Eio
-
module Store = Store
-
module H = Cshell.History
-
-
module History = struct
-
type mode = Void.mode
-
-
let mode_t =
-
Repr.map Repr.string
-
(function
-
| "R" -> Void.R | "RW" -> Void.RW | _ -> failwith "Malformed Void.mode")
-
(function Void.R -> "R" | Void.RW -> "RW")
-
-
type t = {
-
mode : mode;
-
build : Store.Build.t;
-
args : string list;
-
time : int64;
-
diff : Diff.t;
-
}
-
[@@deriving repr]
-
-
let merge = Irmin.Merge.(default (Repr.option t))
-
end
-
-
type entry = History.t
-
-
type action =
-
| Set_mode of History.mode
-
| Set_session of string
-
| Exec of string list
-
| Info
-
| Undo
-
| Fork of string
-
| Replay of string
-
| Unknown of string list
-
| History
-
[@@deriving repr]
-
-
let split_and_remove_empty s =
-
String.split_on_char ' ' s |> List.filter (fun v -> not (String.equal "" v))
-
-
let action = action_t
-
-
let shelter_action = function
-
| "mode" :: [ "r" ] -> Set_mode R
-
| "mode" :: [ "rw" ] -> Set_mode R
-
| "session" :: [ m ] -> Set_session m
-
| "fork" :: [ m ] -> Fork m
-
| "replay" :: [ onto ] -> Replay onto
-
| [ "info" ] -> Info
-
| [ "undo" ] -> Undo
-
| [ "history" ] -> History
-
| other -> Unknown other
-
-
let action_of_command cmd =
-
match split_and_remove_empty cmd with
-
| "@" :: rest -> shelter_action rest
-
| args -> Exec args
-
-
let () = Fmt.set_style_renderer Format.str_formatter `Ansi_tty
-
let history_key = [ "history" ]
-
let key clock = history_key @ [ string_of_float @@ Eio.Time.now clock ]
-
-
let list (H.Store ((module S), store) : entry H.t) =
-
match S.list store history_key with
-
| [] -> []
-
| xs ->
-
let rec loop acc = function
-
| (s, `Contents (v, _meta)) :: next -> loop ((s, v) :: acc) next
-
| _ :: next -> loop acc next
-
| [] -> List.rev acc
-
in
-
loop [] (List.map (fun (v, tree) -> (v, S.Tree.to_concrete tree)) xs)
-
|> List.stable_sort (fun (s1, _) (s2, _) ->
-
Float.compare (Float.of_string s1) (Float.of_string s2))
-
|> List.rev
-
-
let with_latest ~default s f =
-
match list s with [] -> default () | hd :: _ -> f hd
-
-
let text c = Fmt.(styled (`Fg c) string)
-
-
let sessions (H.Store ((module S), store) : entry H.t) =
-
S.Branch.list (S.repo store)
-
-
let commit ~message clock (H.Store ((module S), store) : entry H.t) v =
-
let info () = S.Info.v ~message (Eio.Time.now clock |> Int64.of_float) in
-
S.set_exn ~info store (key clock) v
-
-
let which_branch ((H.Store ((module S), session) : entry H.t) as s) =
-
let branches = sessions s in
-
let repo = S.repo session in
-
let heads = List.map (fun b -> (S.Branch.find repo b, b)) branches in
-
let head = S.Head.find session in
-
List.assoc_opt head heads
-
-
(* Reset the head of the current session by one commit *)
-
let reset_hard ((H.Store ((module S), session) : entry H.t) as s) =
-
match
-
List.filter_map (S.Commit.of_hash (S.repo session))
-
@@ S.Commit.parents (S.Head.get session)
-
with
-
| [] -> s
-
| p :: _ ->
-
S.Head.set session p;
-
s
-
-
(* Fork a new session from an existing one *)
-
let fork (H.Store ((module S), session) : entry H.t) new_branch =
-
let repo = S.repo session in
-
match (S.Head.find session, S.Branch.find repo new_branch) with
-
| _, Some _ ->
-
Error (new_branch ^ " already exists, try @ session " ^ new_branch)
-
| None, _ -> Error "Current branch needs at least one commit"
-
| Some commit, None ->
-
let new_store = S.of_branch (S.repo session) new_branch in
-
S.Branch.set repo new_branch commit;
-
let store = H.Store ((module S), new_store) in
-
Ok store
-
-
(* Fork a new session from an existing one *)
-
let display_history (H.Store ((module S), session) : entry H.t) =
-
let history = S.history ~depth:max_int session in
-
let content c =
-
H.Store ((module S), S.of_commit c) |> list |> List.hd |> snd
-
in
-
let pp_diff fmt d =
-
if d = [] then () else Fmt.pf fmt "\n %a" (Repr.pp Diff.t) d
-
in
-
let pp_entry fmt (e : entry) =
-
Fmt.pf fmt "%-10s %s%a"
-
Fmt.(str "%a" (styled (`Fg `Yellow) uint64_ns_span) e.time)
-
(String.concat " " e.args) pp_diff e.diff
-
in
-
let linearize =
-
S.History.fold_vertex (fun c v -> content c :: v) history [] |> List.rev
-
in
-
List.iter (fun c -> Fmt.pr "%a\n%!" pp_entry c) linearize
-
-
let prompt status ((H.Store ((module S), _session) : entry H.t) as store) =
-
let sesh = Option.value ~default:"main" (which_branch store) in
-
let prompt () =
-
Fmt.(styled (`Fg `Yellow) string) Format.str_formatter "shelter> ";
-
Format.flush_str_formatter ()
-
in
-
let pp_sesh fmt sesh = Fmt.pf fmt "[%a]" (text `Green) sesh in
-
let pp_status fmt = function
-
| `Exited 0 -> Fmt.nop fmt ()
-
| `Exited n -> Fmt.pf fmt "%a " (text `Red) (string_of_int n)
-
| _ -> Fmt.nop fmt ()
-
in
-
let prompt_entry (_, (e : entry)) =
-
Fmt.pf Format.str_formatter "%a%a%a : { mode: %a }> " pp_status status
-
(text `Yellow) "shelter" pp_sesh sesh (text `Red)
-
(if e.mode = R then "r" else "rw");
-
Format.flush_str_formatter ()
-
in
-
with_latest store ~default:prompt prompt_entry
-
-
type ctx = Store.t
-
-
let init fs proc s =
-
let store = Store.init fs proc "test-pool" in
-
List.iter
-
(fun (_, { History.args; _ }) ->
-
LNoise.history_add (String.concat " " args) |> ignore)
-
(list s);
-
store
-
-
let run _fs clock _proc (((H.Store ((module S), store) : entry H.t) as s), ctx)
-
= function
-
| Set_mode mode ->
-
with_latest ~default:(fun _ -> Ok (s, ctx)) s @@ fun (_, entry) ->
-
commit ~message:"mode change" clock s { entry with mode };
-
Ok (s, ctx)
-
| Set_session m ->
-
with_latest ~default:(fun _ -> Ok (s, ctx)) s @@ fun (_, entry) ->
-
let new_store = S.of_branch (S.repo store) m in
-
let new_full_store = H.Store ((module S), new_store) in
-
commit ~message:"new session" clock new_full_store entry;
-
Ok (new_full_store, ctx)
-
| Unknown args ->
-
Fmt.epr "%a: %s\n%!" (text `Red) "Unknown Shelter Action"
-
(String.concat " " args);
-
Ok (s, ctx)
-
| Info ->
-
let sessions = sessions s in
-
let sesh = Option.value ~default:"main" (which_branch s) in
-
let history = S.history store in
-
let pp_commit fmt (hash, msg) =
-
Fmt.pf fmt "[%a]: %s" (text `Yellow) hash msg
-
in
-
let commits =
-
S.History.fold_vertex
-
(fun commit acc ->
-
let info = S.Commit.info commit |> S.Info.message in
-
let hash = S.Commit.hash commit |> Repr.to_string S.Hash.t in
-
(String.sub hash 0 7, info) :: acc)
-
history []
-
in
-
let latest =
-
with_latest
-
~default:(fun () -> None)
-
s
-
(fun (_, e) -> Some (Repr.to_string Store.Build.t e.build))
-
in
-
Fmt.pr "Sessions: %a\nCurrent: %a\nHash: %a\nCommits:@. %a\n%!"
-
Fmt.(list ~sep:(Fmt.any ", ") string)
-
sessions (text `Green) sesh
-
Fmt.(option string)
-
latest
-
Fmt.(vbox ~indent:2 @@ list pp_commit)
-
commits;
-
Ok (s, ctx)
-
| Exec [] -> Ok (s, ctx)
-
| Undo -> Ok (reset_hard s, ctx)
-
| Fork new_branch -> (
-
match fork s new_branch with
-
| Error err ->
-
Fmt.pr "[fork]: %a\n%!" (text `Red) err;
-
Ok (s, ctx)
-
| Ok store -> Ok (store, ctx))
-
| Replay _ -> Ok (s, ctx)
-
| History ->
-
display_history s;
-
Ok (s, ctx)
-
| Exec command -> (
-
let entry =
-
with_latest
-
~default:(fun () ->
-
History.
-
{
-
mode = Void.RW;
-
build = Store.Build.Image "alpine";
-
args = command;
-
time = 0L;
-
diff = [];
-
})
-
s
-
@@ fun (_, e) -> e
-
in
-
let build =
-
match entry.build with
-
| Store.Build.Image img -> Store.fetch ctx img
-
| Store.Build.Build cid -> cid
-
in
-
let hash_entry = { entry with build = Build build; args = command } in
-
let new_cid = Store.cid (Repr.to_string History.t hash_entry) in
-
let with_rootfs fn =
-
if entry.mode = R then (Store.Run.with_build ctx build fn, [])
-
else Store.Run.with_clone ctx ~src:build new_cid fn
-
in
-
try
-
let new_entry, diff =
-
with_rootfs @@ fun rootfs ->
-
let void =
-
Void.empty
-
|> Void.rootfs ~mode:entry.mode rootfs
-
|> Void.exec [ "/bin/ash"; "-c"; String.concat " " command ]
-
in
-
Switch.run @@ fun sw ->
-
let start = Mtime_clock.now () in
-
let proc = Void.spawn ~sw void in
-
let res =
-
Void.exit_status proc |> Eio.Promise.await |> Void.to_eio_status
-
in
-
let stop = Mtime_clock.now () in
-
let span = Mtime.span start stop in
-
let time = Mtime.Span.to_uint64_ns span in
-
(* Add command to history regardless of exit status *)
-
let _ : (unit, string) result =
-
LNoise.history_add (String.concat " " command)
-
in
-
if res = `Exited 0 then
-
if entry.mode = RW then
-
Ok { hash_entry with build = Build new_cid; time }
-
else Ok hash_entry
-
else Error (Eio.Process.Child_error res)
-
in
-
match new_entry with
-
| Error e -> Error e
-
| Ok entry ->
-
(* Set diff *)
-
let entry = { entry with diff } in
-
(* Commit if RW *)
-
if entry.mode = RW then
-
commit
-
~message:("exec " ^ String.concat " " command)
-
clock s entry;
-
Ok (s, ctx)
-
with Eio.Exn.Io (Eio.Process.E e, _) -> Error e)
-16
src/lib/shelter/shelter.mli
···
-
module Store = Store
-
-
module History : sig
-
type t = {
-
mode : Void.mode;
-
build : Store.Build.t;
-
args : string list;
-
time : int64;
-
diff : Diff.t;
-
}
-
[@@deriving repr]
-
-
include Irmin.Contents.S with type t := t
-
end
-
-
include Cshell.Engine.S with type entry = History.t
+560
src/lib/shelter/shelter_main.ml
···
+
open Eio
+
module Store = Store
+
module H = Shelter.History
+
+
type error = string
+
+
let pp_error = Fmt.string
+
+
module History = struct
+
type mode = Void.mode
+
+
let mode_t =
+
Repr.map Repr.string
+
(function
+
| "R" -> Void.R | "RW" -> Void.RW | _ -> failwith "Malformed Void.mode")
+
(function Void.R -> "R" | Void.RW -> "RW")
+
+
type post = { diff : Diff.t; time : int64 } [@@deriving repr]
+
+
type pre = {
+
mode : mode;
+
build : Store.Build.t;
+
args : string list;
+
env : string list;
+
cwd : string;
+
user : int * int;
+
}
+
[@@deriving repr]
+
(** Needed for execution *)
+
+
type t = { pre : pre; post : post } [@@deriving repr]
+
+
let merge = Irmin.Merge.(default (Repr.option t))
+
end
+
+
type config = Config.t
+
+
let config_term = Config.cmdliner
+
+
type entry = History.t
+
+
type action =
+
(* Change modes *)
+
| Set_mode of History.mode
+
(* Fork a new branch from an existing one,
+
or switch to a branch if it exists *)
+
| Set_session of string
+
(* Run a command *)
+
| Exec of string list
+
(* Undo the last command *)
+
| Undo
+
(* Replay the current branch onto another *)
+
| Replay of string
+
(* Display info *)
+
| Info of [ `Current | `History ]
+
(* Error state *)
+
| Unknown of string list
+
[@@deriving repr]
+
+
let split_and_remove_empty s =
+
String.split_on_char ' ' s |> List.filter (fun v -> not (String.equal "" v))
+
+
let action = action_t
+
+
let shelter_action = function
+
| "mode" :: [ "r" ] -> Set_mode R
+
| "mode" :: [ "rw" ] -> Set_mode RW
+
| "session" :: [ m ] -> Set_session m
+
| "replay" :: [ onto ] -> Replay onto
+
| [ "info" ] -> Info `Current
+
| [ "undo" ] -> Undo
+
| [ "history" ] -> Info `History
+
| other -> Unknown other
+
+
let action_of_command cmd =
+
match split_and_remove_empty cmd with
+
| "@" :: rest -> shelter_action rest
+
| args -> Exec args
+
+
let () = Fmt.set_style_renderer Format.str_formatter `Ansi_tty
+
let history_key = [ "history" ]
+
let key clock = history_key @ [ string_of_float @@ Eio.Time.now clock ]
+
+
let history (H.Store ((module S), store) : entry H.t) =
+
let repo = S.repo store in
+
match S.Head.find store with
+
| None -> []
+
| Some hd ->
+
let rec linearize c =
+
match S.Commit.parents c |> List.map (S.Commit.of_hash repo) with
+
| [ Some p ] -> c :: linearize p
+
| _ -> [ c ]
+
in
+
let commits = linearize hd in
+
let get_diff_content t1 t2 =
+
match S.Tree.diff t1 t2 with
+
| [ (_, `Added (c, _)) ] -> c
+
| lst ->
+
let pp_diff =
+
Repr.pp (Irmin.Diff.t (Repr.pair History.t S.metadata_t))
+
in
+
Fmt.epr "Get diff (%i) content %a%!" (List.length lst)
+
Fmt.(list ~sep:Fmt.comma (Fmt.pair (Repr.pp S.path_t) pp_diff))
+
lst;
+
invalid_arg "Get diff should only have a single difference."
+
in
+
let hash c = S.Commit.hash c |> S.Hash.to_raw_string in
+
let rec diff_calc = function
+
| [] -> []
+
| [ x ] ->
+
let diff = get_diff_content (S.Tree.empty ()) (S.Commit.tree x) in
+
[ (hash x, diff) ]
+
| c :: p :: rest ->
+
let diff = get_diff_content (S.Commit.tree p) (S.Commit.tree c) in
+
(hash c, diff) :: diff_calc (p :: rest)
+
in
+
diff_calc commits
+
+
let with_latest ~default s f =
+
match history s with [] -> default () | (_, hd) :: _ -> f hd
+
+
let text c = Fmt.(styled (`Fg c) string)
+
+
let sessions (H.Store ((module S), store) : entry H.t) =
+
S.Branch.list (S.repo store)
+
+
let commit ~message clock (H.Store ((module S), store) : entry H.t) v =
+
let info () = S.Info.v ~message (Eio.Time.now clock |> Int64.of_float) in
+
S.set_exn ~info store (key clock) v
+
+
let which_branch ((H.Store ((module S), session) : entry H.t) as s) =
+
let branches = sessions s in
+
let repo = S.repo session in
+
let heads = List.map (fun b -> (S.Branch.find repo b, b)) branches in
+
let head = S.Head.find session in
+
let head_hash =
+
Option.map
+
(fun hash -> String.sub (Fmt.str "%a" S.Commit.pp_hash hash) 0 7)
+
head
+
in
+
(head_hash, List.assoc_opt head heads)
+
+
(* Reset the head of the current session by one commit *)
+
let reset_hard ((H.Store ((module S), session) : entry H.t) as s) =
+
match
+
List.filter_map (S.Commit.of_hash (S.repo session))
+
@@ S.Commit.parents (S.Head.get session)
+
with
+
| [] -> s
+
| p :: _ ->
+
S.Head.set session p;
+
s
+
+
(* Fork a new session from an existing one *)
+
let fork (H.Store ((module S), session) : entry H.t) new_branch =
+
let repo = S.repo session in
+
match (S.Head.find session, S.Branch.find repo new_branch) with
+
| _, Some _ ->
+
Error (new_branch ^ " already exists, try @ session " ^ new_branch)
+
| None, _ -> Error "Current branch needs at least one commit"
+
| Some commit, None ->
+
let new_store = S.of_branch (S.repo session) new_branch in
+
S.Branch.set repo new_branch commit;
+
let store = H.Store ((module S), new_store) in
+
Ok store
+
+
(* Fork a new session from an existing one *)
+
let display_history (s : entry H.t) =
+
let pp_diff fmt d =
+
if d = [] then () else Fmt.pf fmt "\n %a" (Repr.pp Diff.t) d
+
in
+
let pp_entry fmt (e : entry) =
+
Fmt.pf fmt "%-10s %s%a"
+
Fmt.(str "%a" (styled (`Fg `Yellow) uint64_ns_span) e.post.time)
+
(String.concat " " e.pre.args)
+
pp_diff e.post.diff
+
in
+
let entries = history s |> List.rev in
+
List.iter (fun (_hash, c) -> Fmt.pr "%a\n%!" pp_entry c) entries
+
+
let prompt status ((H.Store ((module S), _session) : entry H.t) as store) =
+
let head, sesh = which_branch store in
+
let sesh = Option.value ~default:"main" sesh in
+
let prompt () =
+
Fmt.(styled (`Fg `Yellow) string) Format.str_formatter "shelter> ";
+
Format.flush_str_formatter ()
+
in
+
let pp_head fmt = function
+
| None -> Fmt.nop fmt ()
+
| Some h -> Fmt.pf fmt "#%a" (text `Magenta) h
+
in
+
let pp_sesh fmt sesh = Fmt.pf fmt "[%a%a]" (text `Green) sesh pp_head head in
+
let pp_status fmt = function
+
| `Exited 0 -> Fmt.nop fmt ()
+
| `Exited n -> Fmt.pf fmt "%a " (text `Red) (string_of_int n)
+
| _ -> Fmt.nop fmt ()
+
in
+
let prompt_entry (e : entry) =
+
Fmt.pf Format.str_formatter "%a%a%a : { mode: %a }> " pp_status status
+
(text `Yellow) "shelter" pp_sesh sesh (text `Red)
+
(if e.pre.mode = R then "r" else "rw");
+
Format.flush_str_formatter ()
+
in
+
with_latest store ~default:prompt prompt_entry
+
+
type ctx = { store : Store.t; tool_dir : string }
+
+
let tools = [ ("opentrace", Tools.opentrace) ]
+
+
let init fs proc s =
+
let store = Store.init fs proc "shelter" in
+
List.iter
+
(fun (_, { History.pre = { History.args; _ }; _ }) ->
+
LNoise.history_add (String.concat " " args) |> ignore)
+
(history s);
+
let tool_cid = Store.cid (String.concat ":" (List.map snd tools)) in
+
let tools =
+
Store.Run.with_tool store tool_cid @@ fun tool_dir ->
+
Eio.Fiber.List.iter
+
(fun (toolname, content) ->
+
let new_path = Eio.Path.(fs / tool_dir / toolname) in
+
Eio.Path.save ~create:(`If_missing 0o755) new_path content)
+
tools;
+
tool_dir
+
in
+
{ store; tool_dir = tools }
+
+
(* Run a command:
+
+
- TODO: pretty confusing that we `entry` to build from and also as the
+
thing we are building (e.g. the build field and the args field... *)
+
let exec (config : config) ~stdout fs proc
+
((H.Store ((module S), _) : entry H.t), (ctx : ctx)) (entry : entry) =
+
let build, env, (uid, gid) =
+
match entry.pre.build with
+
| Store.Build.Image img ->
+
let build, env, user = Store.fetch ctx.store img in
+
(build, env, Option.value ~default:(0, 0) user)
+
| Store.Build.Build cid -> (cid, entry.pre.env, entry.pre.user)
+
in
+
let command = entry.pre.args in
+
let hash_entry =
+
{ entry with pre = { entry.pre with build = Build build } }
+
in
+
(* Store things under History.pre, this makes it possible to rediscover
+
the hash for something purely from the arguments needed to execute something
+
rather than needing, for example, the time it took to execute!
+
+
Also, combine it with previous build step. *)
+
let new_cid =
+
Store.cid (Cid.to_string build ^ Repr.to_string History.pre_t hash_entry.pre)
+
in
+
let with_rootfs fn =
+
if entry.pre.mode = R then (Store.Run.with_build ctx.store build fn, [])
+
else
+
let diff_path =
+
Eio.Path.(fs / Filename.temp_dir "shelter-diff-" "" / "diff")
+
in
+
Store.Run.with_clone ctx.store ~src:build new_cid diff_path fn
+
in
+
with_rootfs @@ function
+
| `Exists path ->
+
(* Copy the stdout log to stdout *)
+
let () =
+
Eio.Path.(with_open_in (fs / (path :> string) / "log")) @@ fun ic ->
+
Eio.Flow.copy ic stdout
+
in
+
let c = Eio.Path.(load (fs / (path :> string) / "hash")) in
+
Ok (`Reset c)
+
| `Build rootfs ->
+
let spawn sw log =
+
if config.no_runc then
+
(* Experiment Void Process *)
+
let rootfs = Filename.concat rootfs "rootfs" in
+
let void =
+
Void.empty
+
|> Void.rootfs ~mode:entry.pre.mode rootfs
+
|> Void.cwd entry.pre.cwd
+
(* TODO: Support UIDs |> Void.uid 1000 *)
+
|> Void.exec ~env
+
[
+
config.shell;
+
"-c";
+
String.concat " " command ^ " && env > /tmp/shelter-env";
+
]
+
in
+
`Void (Void.spawn ~sw void |> Void.exit_status)
+
else
+
let tool_mount : Runc.Json_config.mount =
+
{
+
ty = `Bind;
+
src = ctx.tool_dir;
+
dst = "/shelter-tools";
+
readonly = true;
+
}
+
in
+
let config =
+
Runc.Json_config.
+
{
+
cwd = entry.pre.cwd;
+
argv =
+
[
+
config.shell;
+
"-c";
+
String.concat " " command ^ " && env > /tmp/shelter-env";
+
];
+
hostname = "builder";
+
network = [ "host" ];
+
user = (uid, gid);
+
env = entry.pre.env;
+
mounts = [ tool_mount ];
+
entrypoint = None;
+
}
+
in
+
let env =
+
object
+
method fs = fs
+
method proc = proc
+
method stdout = stdout
+
end
+
in
+
`Runc (Runc.spawn ~sw log env config rootfs)
+
in
+
let start, res =
+
Switch.run @@ fun sw ->
+
let log =
+
Eio.Path.open_out ~sw ~create:(`If_missing 0o644)
+
Eio.Path.(fs / rootfs / "log")
+
in
+
let res = spawn sw log in
+
let start = Mtime_clock.now () in
+
match res with
+
| `Runc r -> (start, Eio.Process.await r)
+
| `Void v -> (start, Void.to_eio_status (Eio.Promise.await v))
+
in
+
let stop = Mtime_clock.now () in
+
let span = Mtime.span start stop in
+
let time = Mtime.Span.to_uint64_ns span in
+
(* Add command to history regardless of exit status *)
+
let _ : (unit, string) result =
+
LNoise.history_add (String.concat " " command)
+
in
+
if res = `Exited 0 then (
+
(* Extract env *)
+
let env_path =
+
Eio.Path.(fs / rootfs / "rootfs" / "tmp" / "shelter-env")
+
in
+
let env =
+
Eio.Path.(load env_path)
+
|> String.split_on_char '\n'
+
|> List.filter (fun s -> not (String.equal "" s))
+
in
+
Eio.Path.unlink env_path;
+
let cwd =
+
List.find_map
+
(fun v ->
+
match Astring.String.cut ~sep:"=" v with
+
| Some ("PWD", dir) -> Some dir
+
| _ -> None)
+
env
+
|> Option.value ~default:hash_entry.pre.cwd
+
in
+
if entry.pre.mode = RW then
+
Ok
+
(`Entry
+
( {
+
hash_entry with
+
History.pre =
+
{
+
hash_entry.pre with
+
build = Build new_cid;
+
env;
+
cwd;
+
user = (uid, gid);
+
};
+
},
+
rootfs ))
+
else
+
Ok
+
(`Entry
+
( {
+
pre = { hash_entry.pre with cwd; env; user = (uid, gid) };
+
post = { hash_entry.post with time };
+
},
+
rootfs )))
+
else Shelter.process_error (Eio.Process.Child_error res)
+
+
let complete_exec ((H.Store ((module S), store) as s : entry H.t), ctx) clock fs
+
new_entry diff =
+
match new_entry with
+
| Error e -> Error e
+
| Ok (`Reset c) -> (
+
match
+
S.Hash.unsafe_of_raw_string c |> S.Commit.of_hash (S.repo store)
+
with
+
| None ->
+
Fmt.epr "Resetting to existing entry failed...\n%!";
+
Ok (s, ctx)
+
| Some c ->
+
S.Head.set store c;
+
Ok (s, ctx))
+
| Ok (`Entry (entry, path)) ->
+
(* Set diff *)
+
let entry = History.{ entry with post = { entry.post with diff } } in
+
(* Commit if RW *)
+
if entry.pre.mode = RW then (
+
commit
+
~message:("exec " ^ String.concat " " entry.pre.args)
+
clock s entry;
+
(* Save the commit hash for easy restoring later *)
+
let hash = S.Head.get store |> S.Commit.hash |> S.Hash.to_raw_string in
+
Eio.Path.save ~create:(`If_missing 0o644)
+
Eio.Path.(fs / path / "hash")
+
hash);
+
Ok (s, ctx)
+
+
let replay config (H.Store ((module S), s) as store : entry H.t) ctx fs clock
+
proc stdout existing_branch =
+
let seshes = sessions store in
+
if not (List.exists (String.equal existing_branch) seshes) then (
+
Fmt.epr "%s does not exist!" existing_branch;
+
Ok (store, ctx))
+
else
+
let repo = S.repo s in
+
let onto = S.of_branch repo existing_branch in
+
match S.lcas ~n:1 s onto with
+
| Error lcas_error ->
+
Fmt.epr "Replay LCAS: %a" (Repr.pp S.lca_error_t) lcas_error;
+
Ok (store, ctx)
+
| Ok [ lcas ] -> (
+
let all_commits = history store in
+
let lcas_hash = S.Commit.hash lcas |> S.Hash.to_raw_string in
+
let rec collect = function
+
| [] -> []
+
| (x, _) :: _ when String.equal lcas_hash x -> []
+
| v :: vs -> v :: collect vs
+
in
+
let commits_to_apply = collect all_commits in
+
match commits_to_apply with
+
| [] -> Shelter.shell_error ""
+
| (h, first) :: rest ->
+
let _, last_other =
+
history (H.Store ((module S), onto)) |> List.hd
+
in
+
let new_first =
+
{
+
first with
+
pre = { first.pre with build = last_other.pre.build };
+
}
+
in
+
let commits_to_apply = (h, new_first) :: rest in
+
(* Now we reset our head to point to the other store's head
+
and replay our commits onto it *)
+
let other_head = S.Head.get onto in
+
S.Head.set s other_head;
+
let res =
+
List.fold_left
+
(fun last (_, (entry : entry)) ->
+
match last with
+
| Error _ as e -> e
+
| Ok (new_store, new_ctx) ->
+
let new_entry, diff =
+
exec config ~stdout fs proc (new_store, new_ctx) entry
+
in
+
complete_exec (new_store, new_ctx) clock fs new_entry diff)
+
(Ok (H.Store ((module S), s), ctx))
+
commits_to_apply
+
in
+
res)
+
| _ -> assert false (* Because n = 1 *)
+
+
let run (config : config) ~stdout fs clock proc
+
(((H.Store ((module S), store) : entry H.t) as s), (ctx : ctx)) = function
+
| Set_mode mode ->
+
with_latest ~default:(fun _ -> Ok (s, ctx)) s @@ fun entry ->
+
commit ~message:"mode change" clock s
+
{ entry with pre = { entry.pre with mode } };
+
Ok (s, ctx)
+
| Set_session m -> (
+
(* Either set the session if the branch exists or create a new branch
+
from the latest commit of the current branch *)
+
let sessions = sessions s in
+
match List.exists (String.equal m) sessions with
+
| true ->
+
let sesh = S.of_branch (S.repo store) m in
+
Ok (H.Store ((module S), sesh), ctx)
+
| false -> (
+
match fork s m with
+
| Error err ->
+
Fmt.pr "[fork]: %a\n%!" (text `Red) err;
+
Ok (s, ctx)
+
| Ok store -> Ok (store, ctx)))
+
| Unknown args ->
+
Fmt.epr "%a" (text `Red) "Unknown Shelter Action\n";
+
Shelter.shell_error (String.concat " " args)
+
| Info `Current ->
+
let sessions = sessions s in
+
let sesh = Option.value ~default:"main" (snd (which_branch s)) in
+
let history = history s in
+
let pp_commit fmt (hash, msg) =
+
Fmt.pf fmt "[%a]: %s" (text `Yellow) hash msg
+
in
+
let repo = S.repo store in
+
let commits =
+
List.fold_left
+
(fun acc (commit, _) ->
+
let commit =
+
S.Hash.unsafe_of_raw_string commit
+
|> S.Commit.of_hash repo |> Option.get
+
in
+
let info = S.Commit.info commit |> S.Info.message in
+
let hash = S.Commit.hash commit |> Repr.to_string S.Hash.t in
+
(String.sub hash 0 7, info) :: acc)
+
[] history
+
in
+
let latest =
+
with_latest
+
~default:(fun () -> None)
+
s
+
(fun e -> Some (Repr.to_string Store.Build.t e.pre.build))
+
in
+
Fmt.pr "Sessions: %a\nCurrent: %a\nHash: %a\nCommits:@. %a\n%!"
+
Fmt.(list ~sep:(Fmt.any ", ") string)
+
sessions (text `Green) sesh
+
Fmt.(option string)
+
latest
+
Fmt.(vbox ~indent:2 @@ list pp_commit)
+
commits;
+
Ok (s, ctx)
+
| Exec [] -> Ok (s, ctx)
+
| Undo -> Ok (reset_hard s, ctx)
+
| Replay branch -> replay config s ctx fs clock proc stdout branch
+
| Info `History ->
+
display_history s;
+
Ok (s, ctx)
+
| Exec command -> (
+
let entry =
+
with_latest
+
~default:(fun () ->
+
History.
+
{
+
pre =
+
{
+
mode = Void.RW;
+
build = Store.Build.Image config.image;
+
args = command;
+
(* TODO: extract with fetch *)
+
env = [];
+
cwd = "/";
+
user = (0, 0);
+
};
+
post = { diff = []; time = 0L };
+
})
+
s Fun.id
+
in
+
let entry = { entry with pre = { entry.pre with args = command } } in
+
try
+
let new_entry, diff = exec config ~stdout fs proc (s, ctx) entry in
+
complete_exec (s, ctx) clock fs new_entry diff
+
with Eio.Exn.Io (Eio.Process.E e, _) -> Shelter.process_error e)
+22
src/lib/shelter/shelter_main.mli
···
+
module Store = Store
+
+
module History : sig
+
type post = { diff : Diff.t; time : int64 } [@@deriving repr]
+
+
type pre = {
+
mode : Void.mode;
+
build : Store.Build.t;
+
args : string list;
+
env : string list;
+
cwd : string;
+
user : int * int;
+
}
+
[@@deriving repr]
+
(** Needed for execution *)
+
+
type t = { pre : pre; post : post } [@@deriving repr]
+
+
include Irmin.Contents.S with type t := t
+
end
+
+
include Shelter.Engine.S with type entry = History.t
+57 -25
src/lib/shelter/store.ml
···
val builds : string -> dataset
val build : string -> string -> dataset
val snapshot : dataset -> snapshot
+
val tools : string -> dataset
+
val tool : string -> string -> dataset
end = struct
type dataset = string
type snapshot = string
···
let builds pool : dataset = pool / "builds"
let build pool path : dataset = builds pool / path
let snapshot ds = ds ^ "@snappy"
+
let tools pool : dataset = pool / "tools"
+
let tool pool path : dataset = tools pool / path
end
let with_dataset ?(typ = Zfs.Types.filesystem) t dataset f =
···
}
in
create_and_mount t (Datasets.builds t.pool);
+
create_and_mount t (Datasets.tools t.pool);
t
let snapshot t (snap : Datasets.snapshot) =
···
Zfs.clone src (tgt :> string)
let read_all fd =
-
let buf = Buffer.create 128 in
-
let bytes = Bytes.create 4096 in
+
let buf = Buffer.create 10_000 in
+
let bytes = Bytes.create 10_000 in
let rec loop () =
match Unix.read fd bytes 0 4096 with
| 0 | (exception End_of_file) -> Buffer.contents buf
···
in
loop ()
-
let diff t (data : Datasets.snapshot) (snap : Datasets.snapshot) =
+
let diff t (data : Datasets.snapshot) (snap : Datasets.snapshot) output =
let data_fs =
String.sub (data :> string) 0 (String.index (data :> string) '@')
in
let zh = Zfs.open_ t.zfs data_fs Zfs.Types.filesystem in
-
let diff =
-
let r, w = Unix.pipe ~cloexec:false () in
+
let () =
try
-
Zfs.show_diff zh ~from_:(data :> string) ~to_:(snap :> string) w;
-
let f = read_all r in
-
Unix.close r;
-
f
-
with e ->
-
Unix.close r;
-
raise e
+
Eio.Path.with_open_out ~create:(`If_missing 0o644) output
+
@@ fun flow_fd ->
+
let eio_fd = Eio_unix.Resource.fd_opt flow_fd in
+
Eio_unix.Fd.use_exn_opt "zfs-diff" eio_fd @@ function
+
| None -> Fmt.failwith "Output needs to have an FD"
+
| Some fd ->
+
Zfs.show_diff zh ~from_:(data :> string) ~to_:(snap :> string) fd
+
with Unix.Unix_error (Unix.EBADF, _, _) -> ()
in
Zfs.close zh;
+
let diff = Eio.Path.load output in
Diff.of_zfs diff
let cid s =
···
in
Cid.v ~version:`Cidv1 ~base:`Base32 ~codec:`Raw ~hash
+
let not_empty s = not String.(equal empty s)
+
+
let get_uid_gid ~username rootfs =
+
let passwd =
+
Eio.Path.load Eio.Path.(rootfs / "etc" / "passwd")
+
|> String.split_on_char '\n'
+
|> List.map (String.split_on_char ':')
+
|> List.map (List.filter not_empty)
+
in
+
List.find_map
+
(function
+
| user :: _ :: uid :: gid :: _ when String.equal user username ->
+
Some (int_of_string uid, int_of_string gid)
+
| _ -> None)
+
passwd
+
let fetch t image =
let cid = cid image in
let cids = cid |> Cid.to_string in
let dataset = Datasets.build t.pool cids in
let dir = Eio.Path.(t.fs / ("/" ^ (Datasets.build t.pool cids :> string))) in
-
if Zfs.exists t.zfs (dataset :> string) Zfs.Types.filesystem then cid
-
else (
-
create_and_mount t dataset;
-
let _dir : string = Fetch.get_image ~dir ~proc:t.proc image in
-
snapshot t (Datasets.snapshot dataset);
-
cid)
+
create_and_mount t dataset;
+
let _dir : string = Fetch.get_image ~dir ~proc:t.proc image in
+
snapshot t (Datasets.snapshot dataset);
+
let username = Fetch.get_user t.proc image in
+
( cid,
+
Fetch.get_env t.proc image,
+
get_uid_gid ~username Eio.Path.(dir / "rootfs") )
module Run = struct
let with_build t cid fn =
let ds = Datasets.build t.pool (Cid.to_string cid) in
Fun.protect ~finally:(fun () -> unmount_dataset t ds) @@ fun () ->
mount_dataset t ds;
-
fn ("/" ^ (ds :> string) ^ "/rootfs")
+
fn (`Build ("/" ^ (ds :> string)))
+
+
let with_tool t cid fn =
+
let ds = Datasets.tool t.pool (Cid.to_string cid) in
+
Fun.protect ~finally:(fun () -> unmount_dataset t ds) @@ fun () ->
+
mount_dataset t ds;
+
fn ("/" ^ (ds :> string))
-
let with_clone t ~src new_cid fn =
+
let with_clone t ~src new_cid output fn =
let ds = Datasets.build t.pool (Cid.to_string src) in
let tgt = Datasets.build t.pool (Cid.to_string new_cid) in
let src_snap = Datasets.snapshot ds in
let tgt_snap = Datasets.snapshot tgt in
-
clone t src_snap tgt;
-
let v = with_build t new_cid fn in
-
snapshot t tgt_snap;
-
let d = diff t src_snap tgt_snap in
-
(v, d)
+
if Zfs.exists t.zfs (tgt :> string) Zfs.Types.dataset then
+
(fn (`Exists ("/" ^ (tgt :> string))), diff t src_snap tgt_snap output)
+
else (
+
clone t src_snap tgt;
+
let v = with_build t new_cid fn in
+
snapshot t tgt_snap;
+
let d = diff t src_snap tgt_snap output in
+
(v, d))
end
+1
src/lib/shelter/tools.ml
···
+
let opentrace = [%blob "./opentrace"]
+73
src/lib/shelter.ml
···
+
module History = History
+
module Engine = Engine
+
module Script = Script
+
+
let process_error e = Error (`Process e)
+
let shell_error e = Error (`Shell e)
+
+
module Make (H : History.S) (Engine : Engine.S with type entry = H.t) = struct
+
module Store = Irmin_git_unix.FS.KV (H)
+
+
let run config ~stdout fs clock proc store =
+
let store = History.Store ((module Store), store) in
+
let initial_ctx = Engine.init fs proc store in
+
let rec loop store ctx exit_code =
+
let prompt = Engine.prompt exit_code store in
+
match LNoise.linenoise prompt with
+
| None -> ()
+
| Some input -> (
+
let action = Engine.action_of_command input in
+
match Engine.run config ~stdout fs clock proc (store, ctx) action with
+
| Error (`Process (Eio.Process.Child_error exit_code)) ->
+
Fmt.epr "%a\n%!" Eio.Process.pp_status exit_code;
+
loop store ctx exit_code
+
| Error (`Process (Eio.Process.Executable_not_found m)) ->
+
Fmt.epr "shelter: excutable not found %s\n%!" m;
+
loop store ctx (`Exited 127)
+
| Error (`Shell e) ->
+
Fmt.epr "shelter: %a\n%!" Engine.pp_error e;
+
loop store ctx (`Exited 255)
+
| Ok (store, ctx) -> loop store ctx (`Exited 0))
+
in
+
loop store initial_ctx (`Exited 0)
+
+
let command_file_to_actions cf =
+
Eio.Path.load cf |> String.split_on_char '\n'
+
|> List.map Engine.action_of_command
+
+
let main config ~stdout fs clock proc directory command_file =
+
let conf = Irmin_git.config (Eio.Path.native_exn directory) in
+
let repo = Store.Repo.v conf in
+
let store = Store.main repo in
+
match command_file with
+
| Some file -> (
+
let actions = command_file_to_actions file in
+
let store = History.Store ((module Store), store) in
+
let initial_ctx = Engine.init fs proc store in
+
let folder (store, ctx, exit_code) action =
+
if exit_code <> `Exited 0 then (store, ctx, exit_code)
+
else
+
match
+
Engine.run config ~stdout fs clock proc (store, ctx) action
+
with
+
| Error (`Process (Eio.Process.Child_error exit_code)) ->
+
Fmt.epr "%a\n%!" Eio.Process.pp_status exit_code;
+
(store, ctx, exit_code)
+
| Error (`Process (Eio.Process.Executable_not_found m)) ->
+
Fmt.epr "shelter: excutable not found %s\n%!" m;
+
(store, ctx, `Exited 127)
+
| Error (`Shell e) ->
+
Fmt.epr "shelter: %a\n%!" Engine.pp_error e;
+
(store, ctx, `Exited 255)
+
| Ok (store, ctx) -> (store, ctx, `Exited 0)
+
in
+
let _store, _ctx, exit_code =
+
List.fold_left folder (store, initial_ctx, `Exited 0) actions
+
in
+
match exit_code with
+
| `Exited 0 -> ()
+
| `Exited n | `Signaled n ->
+
Fmt.epr "%a\n%!" Eio.Process.pp_status exit_code;
+
exit n)
+
| None -> run config ~stdout fs clock proc store
+
end
+22
vendor/void/.github/workflows/test.yml
···
+
name: Void Test
+
on:
+
- push
+
- pull_request
+
permissions: read-all
+
jobs:
+
build:
+
strategy:
+
fail-fast: false
+
runs-on: ubuntu-latest
+
steps:
+
- name: Checkout tree
+
uses: actions/checkout@v4
+
+
- name: Set-up OCaml
+
uses: ocaml/setup-ocaml@v3
+
with:
+
ocaml-compiler: 5
+
+
- run: opam install . --deps-only --with-test
+
- run: opam exec -- dune build
+
- run: sudo ./_build/default/examples/hey.exe
+2
vendor/void/.gitignore
···
+
_build
+
+1
vendor/void/.ocamlformat
···
+
version=0.27.0
+14
vendor/void/LICENSE.md
···
+
Copyright (C) 2024 Patrick Ferris
+
+
Permission to use, copy, modify, and distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies.
+
+
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+
Much of the codebase is borrowed directly from Eio, with the following license:
+
+
Copyright (C) 2021 Anil Madhavapeddy
+
Copyright (C) 2022 Thomas Leonard
+
+
Permission to use, copy, modify, and distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies.
+
+
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+5
vendor/void/README.md
···
+
Void
+
----
+
+
Void processes for Eio.
+
+29
vendor/void/dune-project
···
+
(lang dune 3.15)
+
+
(name void)
+
+
(generate_opam_files true)
+
+
(source
+
(github patricoferris/void))
+
+
(authors "Patrick Ferris")
+
+
(maintainers "Patrick Ferris")
+
+
(license MIT)
+
+
(documentation https://ocaml.org/p/void)
+
+
(package
+
(name void)
+
(synopsis "Void Processes in Eio_linux")
+
(description "A longer description")
+
(depends
+
ocaml
+
dune
+
eio_posix)
+
(tags
+
(linux process spawn)))
+
+
; See the complete stanza docs at https://dune.readthedocs.io/en/stable/dune-files.html#dune-project
+69
vendor/void/examples/alpine.ml
···
+
open Eio.Std
+
+
let ( / ) = Eio.Path.( / )
+
+
let test_data fs data =
+
let tempdir = Filename.temp_dir "void-" "-alpine" in
+
Eio.Path.(save ~create:(`If_missing 0o644) (fs / tempdir / "data.txt") data);
+
Eio.traceln "Test data in %s" tempdir;
+
tempdir
+
+
let get_alpine_image ~fs ~proc =
+
let tmpdir = Filename.temp_dir "void" "alpine" in
+
Eio.traceln "Extracting alpine to %s..." tmpdir;
+
let container_id =
+
Eio.Process.parse_out proc Eio.Buf_read.take_all
+
[ "docker"; "run"; "-d"; "alpine" ]
+
|> String.trim
+
in
+
Eio.traceln "Container %s" container_id;
+
let () =
+
Eio.Process.run proc
+
[
+
"docker";
+
"export";
+
container_id;
+
"-o";
+
Filename.concat tmpdir "alpine.tar.gz";
+
]
+
in
+
Eio.traceln "Untarring...";
+
Eio.Path.mkdir ~perm:0o777 (fs / tmpdir / "rootfs");
+
let () =
+
Eio.Process.run proc
+
[
+
"tar";
+
"-xf";
+
Filename.concat tmpdir "alpine.tar.gz";
+
"-C";
+
Filename.concat tmpdir "rootfs";
+
]
+
in
+
Filename.concat tmpdir "rootfs"
+
+
(* This example read-only mounts a copy of busybox
+
into the root-filesystem of the process. *)
+
let () =
+
Eio_posix.run @@ fun env ->
+
Switch.run @@ fun sw ->
+
let fs = env#fs in
+
let proc = env#process_mgr in
+
let alpine_img = get_alpine_image ~fs ~proc in
+
let mount_src = test_data fs "Hello, World!" in
+
let open Void in
+
let args =
+
let l = Array.length Sys.argv in
+
if l <= 1 then
+
[
+
"/bin/ash"; "-c"; "/bin/echo hello > /hello.txt && /bin/cat /hello.txt";
+
]
+
else Array.sub Sys.argv 1 (l - 1) |> Array.to_list
+
in
+
let void =
+
empty |> rootfs ~mode:RW alpine_img
+
|> mount ~mode:R ~src:mount_src ~tgt:"data"
+
|> exec args
+
in
+
let t = Void.spawn ~sw void in
+
let status = Promise.await (Void.exit_status t) in
+
Eio.traceln "Status: %s" (Void.exit_status_to_string status)
+55
vendor/void/examples/curl.ml
···
+
open Eio.Std
+
+
let ( / ) = Eio.Path.( / )
+
+
let get_alpine_image ~fs ~proc =
+
let tmpdir = Filename.temp_dir "void-" "-alpine-curl" in
+
Eio.traceln "Extracting alpine/curl to %s..." tmpdir;
+
let container_id =
+
Eio.Process.parse_out proc Eio.Buf_read.take_all
+
[ "docker"; "run"; "-d"; "alpine/curl" ]
+
|> String.trim
+
in
+
Eio.traceln "Container %s" container_id;
+
let () =
+
Eio.Process.run proc
+
[
+
"docker";
+
"export";
+
container_id;
+
"-o";
+
Filename.concat tmpdir "alpine-curl.tar.gz";
+
]
+
in
+
Eio.traceln "Untarring...";
+
Eio.Path.mkdir ~perm:0o777 (fs / tmpdir / "rootfs");
+
let () =
+
Eio.Process.run proc
+
[
+
"tar";
+
"-xf";
+
Filename.concat tmpdir "alpine-curl.tar.gz";
+
"-C";
+
Filename.concat tmpdir "rootfs";
+
]
+
in
+
Filename.concat tmpdir "rootfs"
+
+
(* This example read-only mounts a copy of busybox
+
into the root-filesystem of the process. *)
+
let () =
+
Eio_posix.run @@ fun env ->
+
Switch.run @@ fun sw ->
+
let fs = env#fs in
+
let proc = env#process_mgr in
+
let alpine_img = get_alpine_image ~fs ~proc in
+
let open Void in
+
let args =
+
let l = Array.length Sys.argv in
+
if l <= 1 then [ "/bin/ls"; "-l" ]
+
else Array.sub Sys.argv 1 (l - 1) |> Array.to_list
+
in
+
let void = empty |> rootfs ~mode:R alpine_img |> exec args in
+
let t = Void.spawn ~sw void in
+
let status = Promise.await (Void.exit_status t) in
+
Eio.traceln "Status: %s" (Void.exit_status_to_string status)
+3
vendor/void/examples/dune
···
+
(executables
+
(names hey curl alpine)
+
(libraries void eio_posix))
+11
vendor/void/examples/empty.ml
···
+
open Eio.Std
+
+
let () =
+
Eio_linux.run @@ fun _env ->
+
Switch.run @@ fun sw ->
+
let open Void in
+
let void = empty |> exec [] in
+
Eio.traceln "Spawning the empty void...";
+
let t = Void.spawn ~sw void in
+
let status = Promise.await (Void.exit_status t) in
+
Eio.traceln "Status: %s" (Void.exit_status_to_string status)
vendor/void/examples/hey

This is a binary file and will not be displayed.

+11
vendor/void/examples/hey.c
···
+
// Based off of Docker's hello-world
+
// This is standalone program
+
#include <sys/syscall.h>
+
#include <unistd.h>
+
+
const char message[] = "Hello from the Void!\n";
+
+
int main() {
+
syscall(SYS_write, STDOUT_FILENO, message, sizeof(message) - 1);
+
return 0;
+
}
+22
vendor/void/examples/hey.ml
···
+
open Eio.Std
+
+
let ( / ) = Eio.Path.( / )
+
+
let copy_hey fs =
+
let temp_dir = Filename.temp_dir "void-" "-world" in
+
let hey = Eio.Path.load (fs / "./examples/hey") in
+
Eio.Path.save ~create:(`If_missing 0o755) (fs / temp_dir / "hey") hey;
+
temp_dir
+
+
(* This mounts the hello-world into the void process. *)
+
let () =
+
Eio_posix.run @@ fun env ->
+
Switch.run @@ fun sw ->
+
let hey_dir = copy_hey env#fs in
+
let void =
+
let open Void in
+
empty |> mount ~mode:R ~src:hey_dir ~tgt:"say" |> exec [ "/say/hey" ]
+
in
+
let t = Void.spawn ~sw void in
+
let status = Promise.await (Void.exit_status t) in
+
Eio.traceln "Void process: %s" (Void.exit_status_to_string status)
+13
vendor/void/src/dune
···
+
(rule
+
(targets config.ml)
+
(action
+
(run ./include/discover.exe)))
+
+
(library
+
(name void)
+
(public_name void)
+
(foreign_stubs
+
(language c)
+
(flags -D_LARGEFILE_SOURCE -D_FILE_OFFSET_BITS=64)
+
(names void_action))
+
(libraries eio_posix eio.unix))
+57
vendor/void/src/include/discover.ml
···
+
module C = Configurator.V1
+
+
let () =
+
C.main ~name:"discover" (fun c ->
+
let defs, mount_flags =
+
C.C_define.import c ~c_flags:[ "-D_GNU_SOURCE" ]
+
~includes:[ "linux/sched.h"; "sys/mount.h" ]
+
C.C_define.Type.
+
[
+
(* Clone3 Flags *)
+
("CLONE_PIDFD", Int);
+
("CLONE_NEWPID", Int);
+
("CLONE_NEWCGROUP", Int);
+
("CLONE_NEWNS", Int);
+
("CLONE_NEWIPC", Int);
+
("CLONE_NEWNET", Int);
+
("CLONE_NEWTIME", Int);
+
("CLONE_NEWUSER", Int);
+
("CLONE_NEWUTS", Int);
+
(* Mount Flags *)
+
("MS_RDONLY", Int);
+
("MS_REMOUNT", Int);
+
("MS_BIND", Int);
+
("MS_SHARED", Int);
+
]
+
|> List.fold_left
+
(fun (cls, mnts) -> function
+
| name, C.C_define.Value.Int v ->
+
let t = (String.lowercase_ascii name, v) in
+
if String.starts_with ~prefix:"CLONE" name then
+
(t :: cls, mnts)
+
else (cls, t :: mnts)
+
| _ -> assert false)
+
([], [])
+
in
+
let sigs vs =
+
List.map (fun (name, _) -> Printf.sprintf " val %s : t" name) vs
+
in
+
let structs vs =
+
List.map (fun (name, v) -> Printf.sprintf " let %s = 0x%x" name v) vs
+
in
+
let flags_nspace =
+
List.filter (String.starts_with ~prefix:"clone_new") (List.map fst defs)
+
in
+
let mount =
+
[ "module Mount_flags : sig"; " type t = int" ]
+
@ sigs mount_flags
+
@ [ "end = struct"; " type t = int" ]
+
@ structs mount_flags @ [ "end" ]
+
in
+
C.Flags.write_lines "config.ml"
+
([ "module Clone_flags : sig"; " type t = int" ]
+
@ sigs defs @ [ "val all : t list" ]
+
@ [ "end = struct"; " type t = int" ]
+
@ structs defs
+
@ [ "let all = [" ^ String.concat ";" flags_nspace ^ "]" ]
+
@ [ "end" ] @ mount))
+4
vendor/void/src/include/dune
···
+
(executable
+
(name discover)
+
(modules discover)
+
(libraries dune-configurator))
+222
vendor/void/src/void.ml
···
+
open Eio.Std
+
module Process = Eio_posix.Low_level.Process
+
module Trace = Eio.Private.Trace
+
module Fd = Eio_unix.Fd
+
module Rcfd = Eio_unix.Private.Rcfd
+
module Fork_action = Eio_unix.Private.Fork_action
+
+
type mode = R | RW
+
+
type void = {
+
args : string list;
+
env : string list;
+
cwd : string;
+
uid : int;
+
(* TODO: gid *)
+
rootfs : (string * mode) option;
+
mounts : mount list;
+
}
+
+
and mount = { src : string; tgt : string; mode : int [@warning "-69"] }
+
+
(* Actions for namespacing *)
+
module Mount = struct
+
module Flags = struct
+
include Config.Mount_flags
+
+
let empty : t = 0
+
let ( + ) = ( lor )
+
end
+
+
module Types = struct
+
type t = string
+
+
let btrfs = "btrfs"
+
let ext4 = "ext4"
+
let auto = "auto"
+
end
+
end
+
+
external action_mount : unit -> Fork_action.fork_fn = "void_fork_mount"
+
+
let action_mount = action_mount ()
+
+
let _mount ~(src : string) ~(target : string) (type_ : Mount.Types.t)
+
(flags : Mount.Flags.t) =
+
Fork_action.
+
{ run = (fun k -> k (Obj.repr (action_mount, src, target, type_, flags))) }
+
+
external action_pivot_root : unit -> Fork_action.fork_fn
+
= "void_fork_pivot_root"
+
+
let action_pivot_root = action_pivot_root ()
+
+
let pivot_root (new_root : string) (new_root_flags : Mount.Flags.t)
+
(tmpfs : bool) (mounts : mount list) =
+
Fork_action.
+
{
+
run =
+
(fun k ->
+
k
+
(Obj.repr
+
(action_pivot_root, new_root, new_root_flags, tmpfs, mounts)));
+
}
+
+
external action_setuid : unit -> Fork_action.fork_fn
+
= "void_fork_setuid"
+
+
let action_setuid = action_setuid ()
+
+
let setuid (uid : int) = Fork_action.
+
{
+
run =
+
(fun k ->
+
k
+
(Obj.repr
+
(action_setuid, uid)));
+
}
+
+
external action_map_uid_gid : unit -> Fork_action.fork_fn
+
= "void_fork_map_uid_gid"
+
+
let action_map_uid_gid = action_map_uid_gid ()
+
+
let map_uid_gid ~uid ~gid =
+
Fork_action.{ run = (fun k -> k (Obj.repr (action_map_uid_gid, uid, gid))) }
+
+
module Flags = struct
+
include Config.Clone_flags
+
+
let ( + ) = ( lor )
+
end
+
+
external eio_spawn :
+
Unix.file_descr ->
+
Flags.t ->
+
Eio_unix.Private.Fork_action.c_action list ->
+
int * Unix.file_descr = "caml_void_clone3"
+
+
type t = {
+
pid : int;
+
pid_fd : Fd.t;
+
exit_status : Unix.process_status Promise.t;
+
}
+
+
let exit_status t = t.exit_status
+
let pid t = t.pid
+
+
(* Read a (typically short) error message from a child process. *)
+
let rec read_response fd =
+
let buf = Cstruct.create 256 in
+
match Eio_posix.Low_level.readv fd [| buf |] with
+
| 0 | (exception End_of_file) -> ""
+
| len -> Cstruct.to_string buf ~len ^ read_response fd
+
+
let void_flags = List.fold_left Flags.( + ) 0 Flags.all
+
+
type path = string
+
+
let empty = { args = []; env = []; rootfs = None; mounts = []; cwd = "/"; uid = 0 }
+
+
let actions v : Fork_action.t list =
+
let root, tmpfs, root_mode =
+
match v.rootfs with
+
| None -> (Filename.temp_dir "void-" "-tmpdir", true, R)
+
| Some (s, m) -> (s, false, m)
+
in
+
let args = match v.args with [] -> failwith "No exec" | args -> args in
+
let e =
+
Process.Fork_action.execve (List.hd args) ~env:(Array.of_list v.env)
+
~argv:(Array.of_list args)
+
in
+
(* Process mount point points *)
+
let mounts =
+
List.map
+
(fun mnt ->
+
let src = Filename.concat "/.old_root" mnt.src in
+
let tgt = Filename.concat "/" mnt.tgt in
+
{ mnt with tgt; src })
+
v.mounts
+
in
+
let root_flags =
+
if root_mode = R then Mount.Flags.ms_rdonly else Mount.Flags.empty
+
in
+
let mounts = pivot_root root root_flags tmpfs mounts in
+
let uid, gid = Unix.(getuid (), getgid ()) in
+
let user_namespace = map_uid_gid ~uid ~gid in
+
[ user_namespace; mounts; setuid v.uid; Process.Fork_action.chdir v.cwd; e ]
+
+
let rootfs ~mode path v = { v with rootfs = Some (path, mode) }
+
let cwd cwd v = { v with cwd }
+
let uid uid v = { v with uid }
+
let exec ?(env=[]) args v = { v with args; env }
+
+
let mount ~mode ~src ~tgt v =
+
let mode = if mode = R then Mount.Flags.ms_rdonly else Mount.Flags.empty in
+
{ v with mounts = { src; tgt; mode } :: v.mounts }
+
+
(* From eio_linux/eio_posix *)
+
let with_pipe fn =
+
Switch.run @@ fun sw ->
+
let r, w = Eio_posix.Low_level.pipe ~sw in
+
fn r w
+
+
external pidfd_send_signal : Unix.file_descr -> int -> unit
+
= "caml_void_pidfd_send_signal"
+
+
let signal t signum =
+
Fd.use t.pid_fd ~if_closed:Fun.id @@ fun pid_fd ->
+
pidfd_send_signal pid_fd signum
+
+
let rec waitpid pid =
+
match Unix.waitpid [] pid with
+
| p, status ->
+
assert (p = pid);
+
status
+
| exception Unix.Unix_error (EINTR, _, _) -> waitpid pid
+
+
let spawn ~sw v =
+
with_pipe @@ fun errors_r errors_w ->
+
Eio_unix.Private.Fork_action.with_actions (actions v) @@ fun c_actions ->
+
Switch.check sw;
+
let exit_status, set_exit_status = Promise.create () in
+
let t =
+
let pid, pid_fd =
+
Fd.use_exn "errors-w" errors_w @@ fun errors_w ->
+
Eio.Private.Trace.with_span "spawn" @@ fun () ->
+
let flags = Flags.(clone_pidfd + void_flags) in
+
eio_spawn errors_w flags c_actions
+
in
+
let pid_fd = Fd.of_unix ~sw ~seekable:false ~close_unix:true pid_fd in
+
{ pid; pid_fd; exit_status }
+
in
+
Fd.close errors_w;
+
Fiber.fork_daemon ~sw (fun () ->
+
let cleanup () =
+
Fd.close t.pid_fd;
+
Promise.resolve set_exit_status (waitpid t.pid);
+
`Stop_daemon
+
in
+
match Eio_posix.Low_level.await_readable "void_spawn" t.pid_fd with
+
| () -> Eio.Cancel.protect cleanup
+
| exception Eio.Cancel.Cancelled _ ->
+
Eio.Cancel.protect (fun () ->
+
Printf.eprintf "Cancelled?";
+
signal t Sys.sigkill;
+
Eio_posix.Low_level.await_readable "void_spawn" t.pid_fd;
+
cleanup ()));
+
(* Check for errors starting the process. *)
+
match read_response errors_r with
+
| "" -> t (* Success! Execing the child closed [errors_w] and we got EOF. *)
+
| err -> failwith err
+
+
let to_eio_status t =
+
match t with
+
| Unix.WEXITED i -> `Exited i
+
| Unix.WSIGNALED i -> `Signaled i
+
| Unix.WSTOPPED _ -> assert false
+
+
let exit_status_to_string = function
+
| Unix.WEXITED n -> Printf.sprintf "Exited with %i" n
+
| Unix.WSTOPPED n -> Printf.sprintf "Stopped with %i" n
+
| Unix.WSIGNALED n -> Printf.sprintf "Signalled with %i" n
+68
vendor/void/src/void.mli
···
+
(** {1 Void}
+
+
Void is a library to provide {e void processes}. A void process is an
+
{e empty} process, one in which most global resources have been removed. As
+
a user, you can add back in precisely those pieces you need for your
+
process.
+
+
Void uses Eio's [fork_action]s to provide this mechanism, thus it is only
+
available with Eio. *)
+
+
module Mount : sig
+
module Flags : sig
+
type t = private int
+
+
val ms_remount : t
+
val ms_bind : t
+
val ms_shared : t
+
val ( + ) : t -> t -> t
+
end
+
+
module Types : sig
+
type t = private string
+
+
val btrfs : t
+
val ext4 : t
+
val auto : t
+
end
+
end
+
+
type t
+
(** A void process *)
+
+
type path = string
+
(** File paths *)
+
+
type mode = R | RW
+
(* Mounting modes *)
+
+
type void
+
(** A configuration for a void process *)
+
+
val empty : void
+
(** The empty void *)
+
+
val rootfs : mode:mode -> path -> void -> void
+
(** Add a new root filesystem *)
+
+
val mount : mode:mode -> src:path -> tgt:path -> void -> void
+
+
+
val cwd : string -> void -> void
+
(** Set the current working directory *)
+
+
val uid : int -> void -> void
+
(** Set the UID *)
+
+
val exec : ?env:string list -> string list -> void -> void
+
(** Make a void configuration ready to be spawned *)
+
+
val spawn : sw:Eio.Switch.t -> void -> t
+
(** Spawn a void process *)
+
+
val pid : t -> int
+
(** The pid of a running void process *)
+
+
val exit_status : t -> Unix.process_status Eio.Promise.t
+
val exit_status_to_string : Unix.process_status -> string
+
val to_eio_status : Unix.process_status -> Eio.Process.exit_status
+420
vendor/void/src/void_action.c
···
+
#define _GNU_SOURCE
+
#define _FILE_OFFSET_BITS 64
+
#include <linux/sched.h>
+
+
#include <sys/stat.h>
+
#include <sys/types.h>
+
#include <sys/eventfd.h>
+
#if __GLIBC__ > 2 || __GLIBC_MINOR__ > 24
+
#include <sys/random.h>
+
#endif
+
#include <sys/syscall.h>
+
#include <sys/wait.h>
+
#include <sys/mount.h>
+
#include <limits.h>
+
#include <errno.h>
+
#include <dirent.h>
+
#include <fcntl.h>
+
#include <signal.h>
+
#include <unistd.h>
+
#include <string.h>
+
+
+
#define CAML_INTERNALS
+
#include <caml/mlvalues.h>
+
#include <caml/unixsupport.h>
+
#include <caml/memory.h>
+
#include <caml/custom.h>
+
#include <caml/signals.h>
+
#include <caml/fail.h>
+
#undef CAML_INTERNALS
+
+
// From Eio
+
#include <include/fork_action.h>
+
+
#ifndef SYS_pidfd_send_signal
+
#define SYS_pidfd_send_signal 424
+
#endif
+
+
// struct clone_args isn't defined in linux-lts headers, so define it here
+
// Note that this struct is versioned by size. See linux/sched.h for details
+
struct caml_void_clone_args
+
{
+
uint64_t flags;
+
uint64_t pidfd;
+
uint64_t child_tid;
+
uint64_t parent_tid;
+
uint64_t exit_signal;
+
uint64_t stack;
+
uint64_t stack_size;
+
uint64_t tls;
+
};
+
+
static int
+
pidfd_send_signal (int pidfd, int sig, siginfo_t *info, unsigned int flags)
+
{
+
return syscall (SYS_pidfd_send_signal, pidfd, sig, info, flags);
+
}
+
+
CAMLprim value
+
caml_void_pidfd_send_signal (value v_pidfd, value v_signal)
+
{
+
CAMLparam0 ();
+
int res;
+
+
res =
+
pidfd_send_signal (Int_val (v_pidfd),
+
caml_convert_signal_number (Int_val (v_signal)), NULL,
+
0);
+
if (res == -1)
+
uerror ("pidfd_send_signal", Nothing);
+
CAMLreturn (Val_unit);
+
}
+
+
static pid_t
+
clone3_no_fallback (struct caml_void_clone_args *cl_args)
+
{
+
int *pidfd = (int *) (uintptr_t) cl_args->pidfd;
+
pid_t child_pid =
+
syscall (SYS_clone3, cl_args, sizeof (struct caml_void_clone_args));
+
+
if (child_pid >= 0)
+
return child_pid; /* Success! */
+
+
if (errno != ENOSYS && errno != EPERM)
+
{
+
uerror ("clone3", Nothing); /* Unknown error */
+
}
+
+
uerror ("clone3", Nothing);
+
}
+
+
CAMLprim value
+
caml_void_clone3 (value v_errors, value v_flags, value v_actions)
+
{
+
CAMLparam1 (v_actions);
+
CAMLlocal1 (v_result);
+
pid_t child_pid;
+
int pidfd = -1; /* Is automatically close-on-exec */
+
+
struct caml_void_clone_args cl_args = {
+
.flags = Int_val (v_flags),
+
.pidfd = (uintptr_t) & pidfd,
+
.exit_signal = SIGCHLD, /* Needed for wait4 to work if we exit before exec */
+
.stack = (uintptr_t) NULL, /* Use copy-on-write parent stack */
+
.stack_size = 0,
+
};
+
+
child_pid = clone3_no_fallback (&cl_args);
+
if (child_pid == 0)
+
{
+
/* Run child actions (doesn't return) */
+
eio_unix_run_fork_actions (Int_val (v_errors), v_actions);
+
}
+
+
v_result = caml_alloc_tuple (2);
+
Store_field (v_result, 0, Val_long (child_pid));
+
Store_field (v_result, 1, Val_int (pidfd));
+
+
CAMLreturn (v_result);
+
}
+
+
+
// Actions
+
+
// MOUNT/UNMOUNT
+
static void
+
action_mount (int errors, value v_config)
+
{
+
value v_src = Field (v_config, 1);
+
value v_tgt = Field (v_config, 2);
+
value v_type = Field (v_config, 3);
+
value v_flags = Field (v_config, 4);
+
+
int r;
+
+
r =
+
mount (String_val (v_src), String_val (v_tgt), String_val (v_type),
+
Int_val (v_flags), NULL);
+
+
if (r != 0)
+
{
+
eio_unix_fork_error (errors, "mount", strerror (errno));
+
_exit (1);
+
}
+
}
+
+
CAMLprim value
+
void_fork_mount (value v_unit)
+
{
+
return Val_fork_fn (action_mount);
+
}
+
+
// Writes a single line to a file
+
static int
+
put_line (const char *filename, const char *line)
+
{
+
int fd;
+
int written;
+
+
fd = open (filename, O_WRONLY | O_CLOEXEC | O_CREAT | O_TRUNC, 0644);
+
+
if (fd < 0)
+
{
+
return fd;
+
}
+
+
written = write (fd, line, strlen (line));
+
+
close (fd);
+
+
if (written != strlen (line))
+
{
+
return -1;
+
}
+
+
return 0;
+
}
+
+
// MAP UID/GID to root
+
static void
+
action_map_uid_gid (int errors, value v_config)
+
{
+
value v_uid = Field (v_config, 1);
+
value v_gid = Field (v_config, 2);
+
int result;
+
char uid_line[30];
+
char gid_line[30];
+
+
// We map root onto the calling UID
+
snprintf (uid_line, sizeof (uid_line), "0 %i 1\n", Int_val (v_uid));
+
result = put_line ("/proc/self/uid_map", uid_line);
+
+
if (result < 0)
+
{
+
eio_unix_fork_error (errors, "map_uid_gid-uid", strerror (errno));
+
_exit (1);
+
}
+
+
/* From user_namespaces(7)
+
*
+
* Writing "deny" to the /proc/pid/setgroups file before writing to
+
* /proc/pid/gid_map will permanently disable setgroups(2) in a user
+
* namespace and allow writing to /proc/pid/gid_map without having
+
* the CAP_SETGID capability in the parent user namespace.
+
*
+
* See also: https://lwn.net/Articles/626665/ */
+
+
put_line ("/proc/self/setgroups", "deny\n");
+
+
if (result < 0)
+
{
+
eio_unix_fork_error (errors, "map_uid_gid-setgroups", strerror (errno));
+
_exit (1);
+
}
+
+
result =
+
snprintf (gid_line, sizeof (gid_line), "0 %i 1\n", Int_val (v_gid));
+
put_line ("/proc/self/gid_map", gid_line);
+
+
if (result < 0)
+
{
+
eio_unix_fork_error (errors, "map_uid_gid-gid", strerror (errno));
+
_exit (1);
+
}
+
}
+
+
+
CAMLprim value
+
void_fork_map_uid_gid (value v_unit)
+
{
+
return Val_fork_fn (action_map_uid_gid);
+
}
+
+
static void
+
action_setuid (int errors, value v_config)
+
{
+
value v_uid = Field(v_config, 1);
+
+
// if (setuid(1000)) {
+
// eio_unix_fork_error (errors, "setuid", strerror (errno));
+
// _exit (1);
+
// }
+
}
+
+
// SETUID
+
CAMLprim value
+
void_fork_setuid (value v_unit)
+
{
+
return Val_fork_fn (action_setuid);
+
}
+
+
// PIVOT ROOT
+
//
+
static int
+
pivot_root (const char *new_root, const char *put_old)
+
{
+
return syscall (SYS_pivot_root, new_root, put_old);
+
}
+
+
// Is there too much OCaml stuff going on here for a fork_action ?
+
static void
+
action_pivot_root (int errors, value v_config)
+
{
+
value v_new_root = Field (v_config, 1);
+
value v_root_flags = Field (v_config, 2);
+
value v_no_root = Field (v_config, 3);
+
value v_mounts = Field (v_config, 4);
+
char path[PATH_MAX];
+
char old_root_path[PATH_MAX];
+
char *new_root = String_val (v_new_root);
+
const char *put_old = ".old_root";
+
+
// From pivot_root example: We want to change the propagation type
+
// of root to be private so we can pivot it.
+
if (mount (NULL, "/", NULL, MS_REC | MS_PRIVATE, NULL) == -1)
+
{
+
eio_unix_fork_error (errors, "pivot_root-private", strerror (errno));
+
_exit (1);
+
}
+
+
// If no pivot_root was given, then we tmpfs the tmpdir we assume was passed.
+
if (Bool_val (v_no_root))
+
{
+
// Make a temporary directory... can't because it allocates ?
+
//if (mkdtemp(new_root) != NULL) {
+
// eio_unix_fork_error(errors, new_root, strerror(errno));
+
// _exit(1);
+
//}
+
+
if (mount ("tmpfs", new_root, "tmpfs", 0, NULL) <= -1)
+
{
+
eio_unix_fork_error (errors, "pivot_root-tmpfs", strerror (errno));
+
_exit (1);
+
}
+
}
+
else
+
{
+
// From pivot_root example: we check that new_root is indeed a mountpoint
+
if (mount (new_root, new_root, NULL, MS_BIND, NULL) <= -1)
+
{
+
eio_unix_fork_error (errors, "pivot_root-new_root",
+
strerror (errno));
+
_exit (1);
+
}
+
}
+
+
// Make the place to pivot the old root too, under the new root
+
snprintf (old_root_path, sizeof (path), "%s/%s", new_root, put_old);
+
+
if (mkdir (old_root_path, 0777) == -1)
+
{
+
eio_unix_fork_error (errors, "pivot_root-mkdir-put_old",
+
strerror (errno));
+
_exit (1);
+
}
+
+
// Pivot the root
+
if (pivot_root (new_root, old_root_path))
+
{
+
eio_unix_fork_error (errors, "pivot_root", strerror (errno));
+
_exit (1);
+
}
+
+
// Add mounts
+
value current_mount = v_mounts;
+
int mount_result;
+
int mode;
+
while (current_mount != Val_emptylist)
+
{
+
// TODO: Mode for mounting
+
mode = Int_val (Field (Field (current_mount, 0), 2));
+
+
// A mount is a record {src; tgt; mode}, we first create the mount point
+
// directory target
+
if (mkdir (String_val (Field (Field (current_mount, 0), 1)), 0777) ==
+
-1)
+
{
+
eio_unix_fork_error (errors, "pivot_root-mkdir-mount",
+
strerror (errno));
+
_exit (1);
+
}
+
+
mount_result = mount (String_val (Field (Field (current_mount, 0), 0)),
+
String_val (Field (Field (current_mount, 0), 1)),
+
NULL, MS_REC | MS_BIND, NULL);
+
+
// Fail early if a mount fails...
+
if (mount_result < 0)
+
{
+
char error[PATH_MAX];
+
snprintf (error, sizeof (error), "mount failed: (%s->%s)",
+
String_val (Field (Field (current_mount, 0), 0)),
+
String_val (Field (Field (current_mount, 0), 1)));
+
eio_unix_fork_error (errors, error, strerror (errno));
+
_exit (1);
+
}
+
+
// After mounting for the first time, we can come back and add any
+
// extra modes that may have been specified, for example RDONLY.
+
if (mode != 0)
+
{
+
mount_result =
+
mount (String_val (Field (Field (current_mount, 0), 0)),
+
String_val (Field (Field (current_mount, 0), 1)), NULL,
+
MS_REMOUNT | MS_BIND | mode, NULL);
+
+
if (mount_result < 0)
+
{
+
eio_unix_fork_error (errors, "remount for mode",
+
strerror (errno));
+
_exit (1);
+
}
+
}
+
+
// Next mount in the list
+
current_mount = Field (current_mount, 1);
+
}
+
+
+
// Change to the 'new' root
+
if (chdir ("/") == -1)
+
{
+
eio_unix_fork_error (errors, "pivot_root-chdir", strerror (errno));
+
_exit (1);
+
}
+
+
// Unmount the old root and remove it
+
if (umount2 (put_old, MNT_DETACH) == -1)
+
{
+
eio_unix_fork_error (errors, put_old, strerror (errno));
+
_exit (1);
+
}
+
+
// Remove the old root
+
if (rmdir (put_old) == -1)
+
{
+
eio_unix_fork_error (errors, put_old, strerror (errno));
+
_exit (1);
+
}
+
+
+
// Apply any flags to the new root, e.g. RDONLY
+
if (Int_val (v_root_flags))
+
{
+
if (mount
+
("/", "/", NULL, (MS_REMOUNT | MS_BIND | Int_val (v_root_flags)),
+
NULL) <= -1)
+
{
+
eio_unix_fork_error (errors, "pivot_root-rootflags",
+
strerror (errno));
+
_exit (1);
+
}
+
}
+
}
+
+
CAMLprim value
+
void_fork_pivot_root (value v_unit)
+
{
+
return Val_fork_fn (action_pivot_root);
+
}
+3
vendor/void/test/dune
···
+
(executable
+
(name main)
+
(libraries void))
+20
vendor/void/test/main.ml
···
+
open Eio.Std
+
+
let _root_filesystem =
+
"/obuilder-zfs/result/fe532e693c6a86db16b50547aae1345b3515c727b8ed668b3e0c33c1e9a895f9/rootfs"
+
+
let () =
+
Eio_posix.run @@ fun _ ->
+
Switch.run @@ fun sw ->
+
let open Void in
+
let void =
+
empty
+
|> mount ~mode:R ~src:"/tmp/test" ~tgt:"bin"
+
|> exec [ "/bin/busybox"; "ls" ]
+
in
+
let t = Void.spawn ~sw void in
+
match Promise.await (Void.exit_status t) with
+
| Unix.WEXITED 0 -> print_endline "done"
+
| Unix.WEXITED n -> Printf.printf "Exited with %i\n%!" n
+
| Unix.WSTOPPED n -> Printf.printf "Stopped with %i\n%!" n
+
| Unix.WSIGNALED n -> Printf.printf "Signalled with %i\n%!" n
+32
vendor/void/void.opam
···
+
# This file is generated by dune, edit dune-project instead
+
opam-version: "2.0"
+
synopsis: "Void Processes in Eio_linux"
+
description: "A longer description"
+
maintainer: ["Patrick Ferris"]
+
authors: ["Patrick Ferris"]
+
license: "MIT"
+
tags: ["linux" "process" "spawn"]
+
homepage: "https://github.com/patricoferris/void"
+
doc: "https://ocaml.org/p/void"
+
bug-reports: "https://github.com/patricoferris/void/issues"
+
depends: [
+
"ocaml"
+
"dune" {>= "3.15"}
+
"eio_posix"
+
"odoc" {with-doc}
+
]
+
build: [
+
["dune" "subst"] {dev}
+
[
+
"dune"
+
"build"
+
"-p"
+
name
+
"-j"
+
jobs
+
"@install"
+
"@runtest" {with-test}
+
"@doc" {with-doc}
+
]
+
]
+
dev-repo: "git+https://github.com/patricoferris/void.git"
+2
vendor/zfs/.gitignore
···
+
_build
+
_opam
+1
vendor/zfs/.ocamlformat
···
+
version=0.27.0
+12
vendor/zfs/README.md
···
+
ocaml-zfs
+
---------
+
+
Some very partial and very probably broken bindings to `libzfs`.
+
+
<!-- $MDX file=example/main.ml -->
+
```ocaml
+
let () =
+
let handle = Zfs.init () in
+
let props = [ ("compression", `String "lz4") ] in
+
Zfs.create ~props handle "obuilder-zfs/hello" Zfs.Types.filesystem
+
```
+1
vendor/zfs/dune
···
+
(mdx)
+31
vendor/zfs/dune-project
···
+
(lang dune 3.15)
+
(using mdx 0.4)
+
(using ctypes 0.3)
+
+
(name zfs)
+
+
(generate_opam_files true)
+
+
(source
+
(github patricoferris/ocaml-zfs))
+
+
(authors "Patrick Ferris <patrick@sirref.org>")
+
+
(maintainers "Patrick Ferris <patrick@sirref.org>")
+
+
(license ISC)
+
+
+
(package
+
(name zfs)
+
(synopsis "libzfs bindings")
+
(description "OCaml bindings to libzfs")
+
(depends
+
ocaml
+
dune
+
ctypes
+
(mdx :with-test))
+
(tags
+
("filesystem" "zfs")))
+
+
; See the complete stanza docs at https://dune.readthedocs.io/en/stable/dune-files.html#dune-project
+3
vendor/zfs/example/dune
···
+
(executable
+
(name main)
+
(libraries zfs))
+4
vendor/zfs/example/main.ml
···
+
let () =
+
let handle = Zfs.init () in
+
let props = [ ("compression", `String "lz4") ] in
+
Zfs.create ~props handle "obuilder-zfs/hello" Zfs.Types.filesystem
+38
vendor/zfs/src/dune
···
+
(rule
+
(targets config.ml c_flags.sexp c_library_flags.sexp)
+
(action
+
(run ./include/discover.exe)))
+
+
(library
+
(name zfs)
+
(public_name zfs)
+
(libraries unix)
+
(c_library_flags
+
(:include c_library_flags.sexp))
+
(flags
+
(:standard -w -9-27))
+
(ctypes
+
(external_library_name libzfs)
+
(build_flags_resolver
+
(vendored
+
(c_flags
+
:standard
+
-D_GNU_SOURCE
+
(:include c_flags.sexp))))
+
(headers
+
(include
+
"unistd.h"
+
"stdio.h"
+
"stdint.h"
+
"stdbool.h"
+
"libzfs_core.h"
+
"libzfs.h"))
+
(type_description
+
(instance Types)
+
(functor Type_description))
+
(function_description
+
(concurrency unlocked)
+
(instance Functions)
+
(functor Function_description))
+
(generated_types Types_generated)
+
(generated_entry_point C)))
+99
vendor/zfs/src/function_description.ml
···
+
open Ctypes
+
+
(* This Types_generated module is an instantiation of the Types
+
functor defined in the type_description.ml file. It's generated by
+
a C program that Dune creates and runs behind the scenes. *)
+
module Types = Types_generated
+
+
module Functions (F : Ctypes.FOREIGN) = struct
+
open F
+
+
let init = foreign "libzfs_init" (void @-> returning Types.libzfs_handle_t)
+
let errno = foreign "libzfs_errno" (Types.libzfs_handle_t @-> returning int)
+
+
let debug =
+
foreign "libzfs_print_on_error"
+
(Types.libzfs_handle_t @-> bool @-> returning void)
+
+
module Zpool = struct
+
let open_ =
+
foreign "zpool_open"
+
(Types.libzfs_handle_t @-> string @-> returning Types.zpool_handle_t)
+
+
let close = foreign "zpool_close" (Types.zpool_handle_t @-> returning void)
+
+
let get_name =
+
foreign "zpool_get_name" (Types.zpool_handle_t @-> returning string)
+
+
let get_state =
+
foreign "zpool_get_state" (Types.zpool_handle_t @-> returning int)
+
end
+
+
let create_ancestors =
+
foreign "zfs_create_ancestors" (Types.libzfs_handle_t @-> string @-> returning int)
+
+
let create =
+
foreign "zfs_create"
+
(Types.libzfs_handle_t @-> string @-> int @-> ptr Types.nvlist_t
+
@-> returning int)
+
+
let open_ =
+
foreign "zfs_open"
+
(Types.libzfs_handle_t @-> string @-> int @-> returning Types.zfs_handle_t)
+
+
let mount =
+
foreign "zfs_mount" (Types.zfs_handle_t @-> string @-> int @-> returning int)
+
+
let unmount =
+
foreign "zfs_unmount"
+
(Types.zfs_handle_t @-> string @-> int @-> returning int)
+
+
let close = foreign "zfs_close" (Types.zfs_handle_t @-> returning void)
+
let get_type = foreign "zfs_get_type" (Types.zfs_handle_t @-> returning int)
+
+
module Nvlist = struct
+
let alloc =
+
foreign "nvlist_alloc"
+
(ptr (ptr Types.nvlist_t) @-> int @-> int @-> returning int)
+
+
let free = foreign "nvlist_free" (ptr Types.nvlist_t @-> returning void)
+
+
let add_bool =
+
foreign "nvlist_add_boolean_value"
+
(ptr Types.nvlist_t @-> string @-> bool @-> returning int)
+
+
let add_string =
+
foreign "nvlist_add_string"
+
(ptr Types.nvlist_t @-> string @-> string @-> returning int)
+
+
let add_byte =
+
foreign "nvlist_add_byte"
+
(ptr Types.nvlist_t @-> string @-> uchar @-> returning int)
+
+
let add_int64 =
+
foreign "nvlist_add_int64"
+
(ptr Types.nvlist_t @-> string @-> int64_t @-> returning int)
+
end
+
+
let clone =
+
foreign "zfs_clone"
+
(Types.zfs_handle_t @-> string @-> ptr Types.nvlist_t @-> returning int)
+
+
let snapshot =
+
foreign "zfs_snapshot"
+
(Types.libzfs_handle_t @-> string @-> bool @-> ptr Types.nvlist_t
+
@-> returning int)
+
+
let exists =
+
foreign "zfs_dataset_exists"
+
(Types.libzfs_handle_t @-> string @-> int @-> returning bool)
+
+
let is_mounted =
+
foreign "is_mounted"
+
(Types.libzfs_handle_t @-> string @-> ptr string @-> returning bool)
+
+
let diff =
+
foreign "zfs_show_diffs"
+
(Types.zfs_handle_t @-> int @-> string @-> string_opt @-> int
+
@-> returning int)
+
end
+209
vendor/zfs/src/include/discover.ml
···
+
module C = Configurator.V1
+
+
let starts_with ~prefix s =
+
try
+
String.iteri
+
(fun i c -> if Char.equal (String.get s i) c then () else raise Not_found)
+
prefix;
+
let l = String.length s in
+
let o = String.length prefix in
+
Some (String.sub s o (l - o))
+
with Not_found -> None
+
+
let () =
+
C.main ~name:"discover" (fun c ->
+
let pkgconf =
+
C.Pkg_config.get c |> function
+
| Some p -> p
+
| None -> failwith "Need pkgconfig"
+
in
+
match C.Pkg_config.query pkgconf ~package:"libzfs" with
+
| None -> failwith "Pkgconfig failed to find libzfs"
+
| Some conf ->
+
C.Flags.write_sexp "c_flags.sexp" conf.cflags;
+
C.Flags.write_sexp "c_library_flags.sexp" conf.libs;
+
let errs, props, types =
+
C.C_define.import c
+
~c_flags:("-D_GNU_SOURCE" :: conf.cflags)
+
~includes:
+
[
+
"unistd.h";
+
"stdio.h";
+
"stdint.h";
+
"stdbool.h";
+
"libzfs_core.h";
+
"libzfs.h";
+
]
+
C.C_define.Type.
+
[
+
(* Errors *)
+
("EZFS_SUCCESS", Int);
+
("EZFS_NOMEM", Int);
+
("EZFS_BADPROP", Int);
+
("EZFS_PROPREADONLY", Int);
+
("EZFS_PROPTYPE", Int);
+
("EZFS_PROPNONINHERIT", Int);
+
("EZFS_PROPSPACE", Int);
+
("EZFS_BADTYPE", Int);
+
("EZFS_BUSY", Int);
+
("EZFS_EXISTS", Int);
+
("EZFS_NOENT", Int);
+
("EZFS_BADSTREAM", Int);
+
("EZFS_DSREADONLY", Int);
+
("EZFS_VOLTOOBIG", Int);
+
("EZFS_INVALIDNAME", Int);
+
("EZFS_BADRESTORE", Int);
+
("EZFS_BADBACKUP", Int);
+
("EZFS_BADTARGET", Int);
+
("EZFS_NODEVICE", Int);
+
("EZFS_BADDEV", Int);
+
("EZFS_NOREPLICAS", Int);
+
("EZFS_RESILVERING", Int);
+
("EZFS_BADVERSION", Int);
+
("EZFS_POOLUNAVAIL", Int);
+
("EZFS_DEVOVERFLOW", Int);
+
("EZFS_BADPATH", Int);
+
("EZFS_CROSSTARGET", Int);
+
("EZFS_ZONED", Int);
+
("EZFS_MOUNTFAILED", Int);
+
("EZFS_UMOUNTFAILED", Int);
+
("EZFS_UNSHARENFSFAILED", Int);
+
("EZFS_SHARENFSFAILED", Int);
+
("EZFS_PERM", Int);
+
("EZFS_NOSPC", Int);
+
("EZFS_FAULT", Int);
+
("EZFS_IO", Int);
+
("EZFS_INTR", Int);
+
("EZFS_ISSPARE", Int);
+
("EZFS_INVALCONFIG", Int);
+
("EZFS_RECURSIVE", Int);
+
("EZFS_NOHISTORY", Int);
+
("EZFS_POOLPROPS", Int);
+
("EZFS_POOL_NOTSUP", Int);
+
("EZFS_POOL_INVALARG", Int);
+
("EZFS_NAMETOOLONG", Int);
+
("EZFS_OPENFAILED", Int);
+
("EZFS_NOCAP", Int);
+
("EZFS_LABELFAILED", Int);
+
("EZFS_BADWHO", Int);
+
("EZFS_BADPERM", Int);
+
("EZFS_BADPERMSET", Int);
+
("EZFS_NODELEGATION", Int);
+
("EZFS_UNSHARESMBFAILED", Int);
+
("EZFS_SHARESMBFAILED", Int);
+
("EZFS_BADCACHE", Int);
+
("EZFS_ISL2CACHE", Int);
+
("EZFS_VDEVNOTSUP", Int);
+
("EZFS_NOTSUP", Int);
+
("EZFS_ACTIVE_SPARE", Int);
+
("EZFS_UNPLAYED_LOGS", Int);
+
("EZFS_REFTAG_RELE", Int);
+
("EZFS_REFTAG_HOLD", Int);
+
("EZFS_TAGTOOLONG", Int);
+
("EZFS_PIPEFAILED", Int);
+
("EZFS_THREADCREATEFAILED", Int);
+
("EZFS_POSTSPLIT_ONLINE", Int);
+
("EZFS_SCRUBBING", Int);
+
("EZFS_ERRORSCRUBBING", Int);
+
("EZFS_ERRORSCRUB_PAUSED", Int);
+
("EZFS_NO_SCRUB", Int);
+
("EZFS_DIFF", Int);
+
("EZFS_DIFFDATA", Int);
+
("EZFS_POOLREADONLY", Int);
+
("EZFS_SCRUB_PAUSED", Int);
+
("EZFS_SCRUB_PAUSED_TO_CANCEL", Int);
+
("EZFS_ACTIVE_POOL", Int);
+
("EZFS_CRYPTOFAILED", Int);
+
("EZFS_NO_PENDING", Int);
+
("EZFS_CHECKPOINT_EXISTS", Int);
+
("EZFS_DISCARDING_CHECKPOINT", Int);
+
("EZFS_NO_CHECKPOINT", Int);
+
("EZFS_DEVRM_IN_PROGRESS", Int);
+
("EZFS_VDEV_TOO_BIG", Int);
+
("EZFS_IOC_NOTSUPPORTED", Int);
+
("EZFS_TOOMANY", Int);
+
("EZFS_INITIALIZING", Int);
+
("EZFS_NO_INITIALIZE", Int);
+
("EZFS_WRONG_PARENT", Int);
+
("EZFS_TRIMMING", Int);
+
("EZFS_NO_TRIM", Int);
+
("EZFS_TRIM_NOTSUP", Int);
+
("EZFS_NO_RESILVER_DEFER", Int);
+
("EZFS_EXPORT_IN_PROGRESS", Int);
+
("EZFS_REBUILDING", Int);
+
("EZFS_VDEV_NOTSUP", Int);
+
("EZFS_NOT_USER_NAMESPACE", Int);
+
("EZFS_CKSUM", Int);
+
("EZFS_RESUME_EXISTS", Int);
+
("EZFS_SHAREFAILED", Int);
+
("EZFS_UNKNOWN", Int);
+
(* Properties *)
+
("ZFS_PROP_CREATION", Int);
+
("ZFS_PROP_USED", Int);
+
("ZFS_PROP_AVAILABLE", Int);
+
("ZFS_PROP_REFERENCED", Int);
+
("ZFS_PROP_COMPRESSRATIO", Int);
+
("ZFS_PROP_COMPRESSION", Int);
+
("ZFS_PROP_SNAPDIR", Int);
+
("ZFS_PROP_ENCRYPTION", Int);
+
(* Types *)
+
("ZFS_TYPE_INVALID", Int);
+
("ZFS_TYPE_FILESYSTEM", Int);
+
("ZFS_TYPE_SNAPSHOT", Int);
+
("ZFS_TYPE_VOLUME", Int);
+
("ZFS_TYPE_POOL", Int);
+
("ZFS_TYPE_BOOKMARK", Int);
+
("ZFS_TYPE_VDEV", Int);
+
("ZFS_TYPE_DATASET", Int);
+
]
+
|> List.fold_left
+
(fun (errs, props, types) -> function
+
| name, C.C_define.Value.Int v -> (
+
let type_ name =
+
Printf.sprintf "val %s : t"
+
(String.lowercase_ascii name)
+
in
+
let definition name =
+
Printf.sprintf "let %s : t = 0x%x"
+
(String.lowercase_ascii name)
+
v
+
in
+
match starts_with ~prefix:"EZFS_" name with
+
| Some r ->
+
((type_ r, definition r) :: errs, props, types)
+
| None -> (
+
match starts_with ~prefix:"ZFS_PROP_" name with
+
| Some r ->
+
(errs, (type_ r, definition r) :: props, types)
+
| None -> (
+
match starts_with ~prefix:"ZFS_TYPE_" name with
+
| Some t ->
+
( errs,
+
props,
+
(type_ t, definition t) :: types )
+
| None -> failwith "Unknown ZFS static value")))
+
| _ -> assert false)
+
([], [], [])
+
in
+
let with_module ~name defs =
+
[ Printf.sprintf "module %s = struct\n type t = int\n" name ]
+
@ defs @ [ "end" ]
+
in
+
let with_module_type ~name defs =
+
[
+
Printf.sprintf "module type %s = sig\n type t = private int\n"
+
name;
+
]
+
@ defs @ [ "end" ]
+
in
+
let defs =
+
with_module ~name:"Error" (List.map snd errs)
+
@ with_module ~name:"Props" (List.map snd props)
+
@ with_module ~name:"Types" (List.map snd types)
+
in
+
let types =
+
with_module_type ~name:"ERROR" (List.map fst errs)
+
@ with_module_type ~name:"PROPS" (List.map fst props)
+
@ with_module_type ~name:"TYPES" (List.map fst types)
+
in
+
C.Flags.write_lines "config.ml" (defs @ types))
+4
vendor/zfs/src/include/dune
···
+
(executable
+
(name discover)
+
(modules discover)
+
(libraries dune-configurator))
+31
vendor/zfs/src/type_description.ml
···
+
module Types (F : Ctypes.TYPE) = struct
+
open F
+
+
type libzfs_handle_t
+
+
let libzfs_handle_t :
+
libzfs_handle_t Ctypes_static.structure Ctypes_static.ptr typ =
+
ptr @@ structure "libzfs_handle"
+
+
type zpool_handle_t
+
+
let zpool_handle_t :
+
zpool_handle_t Ctypes_static.structure Ctypes_static.ptr typ =
+
ptr @@ structure "zpool_handle"
+
+
type zfs_handle_t
+
+
let zfs_handle_t : zfs_handle_t Ctypes_static.structure Ctypes_static.ptr typ
+
=
+
ptr @@ structure "zfs_handle"
+
+
type nvlist_t
+
+
let nvlist_t : nvlist_t Ctypes_static.structure typ = structure "nvlist"
+
let nvl_version = field nvlist_t "nvl_version" int32_t
+
let nvl_nvflag = field nvlist_t "nvl_nvflag" uint32_t
+
let nvl_priv = field nvlist_t "nvl_priv" uint64_t
+
let nvl_flag = field nvlist_t "nvl_flag" uint32_t
+
let nvl_pad = field nvlist_t "nvl_pad" int32_t
+
let () = seal nvlist_t
+
end
+141
vendor/zfs/src/zfs.ml
···
+
module Error = struct
+
include Config.Error
+
end
+
+
module Flags = struct
+
type t = int
+
+
let empty = 0
+
let of_int x = x
+
let ( + ) = ( lor )
+
let mem a b = a land b = a
+
end
+
+
module Types = struct
+
include Flags
+
+
let vdev = Config.Types.vdev
+
let pool = Config.Types.pool
+
let volume = Config.Types.volume
+
let invalid = Config.Types.invalid
+
let bookmark = Config.Types.bookmark
+
let snapshot = Config.Types.snapshot
+
let filesystem = Config.Types.filesystem
+
let dataset = Config.Types.dataset
+
end
+
+
module Handle = struct
+
type t = C.Types.libzfs_handle_t Ctypes_static.structure Ctypes_static.ptr
+
end
+
+
let init : unit -> Handle.t = C.Functions.init
+
let debug : Handle.t -> bool -> unit = C.Functions.debug
+
let errno : Handle.t -> int = C.Functions.errno
+
+
module Zpool = struct
+
type t = C.Types.zpool_handle_t Ctypes_static.structure Ctypes_static.ptr
+
+
let open_ = C.Functions.Zpool.open_
+
let close = C.Functions.Zpool.close
+
let get_name = C.Functions.Zpool.get_name
+
end
+
+
module Nvlist = struct
+
type t = C.Types.nvlist_t Ctypes_static.structure Ctypes_static.ptr
+
+
type nvlist =
+
(string
+
* [ `Bool of bool
+
| `String of string
+
| `Byte of Unsigned.uchar
+
| `Int64 of int64 ])
+
list
+
+
let check_return i =
+
if i = 22 then invalid_arg "Nvlist.v: add bool" else assert (i = 0)
+
+
let v (schema : nvlist) : t =
+
let open Ctypes in
+
let finalise v = C.Functions.Nvlist.free !@v in
+
let nv_pp =
+
allocate ~finalise (ptr C.Types.nvlist_t)
+
(from_voidp C.Types.nvlist_t null)
+
in
+
(* TODO: Unique names or not... *)
+
C.Functions.Nvlist.alloc nv_pp 0x1 0 |> check_return;
+
let rec aux = function
+
| [] -> !@nv_pp
+
| (k, `Bool b) :: rest ->
+
C.Functions.Nvlist.add_bool !@nv_pp k b |> check_return;
+
aux rest
+
| (k, `String s) :: rest ->
+
C.Functions.Nvlist.add_string !@nv_pp k s |> check_return;
+
aux rest
+
| (k, `Int64 i64) :: rest ->
+
C.Functions.Nvlist.add_int64 !@nv_pp k i64 |> check_return;
+
aux rest
+
| (k, `Byte u) :: rest ->
+
C.Functions.Nvlist.add_byte !@nv_pp k u |> check_return;
+
aux rest
+
| _ -> assert false
+
in
+
aux schema
+
+
let empty = Ctypes.(coerce (ptr void) (ptr C.Types.nvlist_t) null)
+
end
+
+
type t = C.Types.zfs_handle_t Ctypes_static.structure Ctypes_static.ptr
+
+
let create_ancestors handle path =
+
let i = C.Functions.create_ancestors handle path in
+
if i != 0 then failwith "Failed to create ancestors" else ()
+
+
let create ?(props = []) handle path (type_ : Types.t) =
+
let i = C.Functions.create handle path type_ (Nvlist.v props) in
+
if i != 0 then failwith "Failed to create" else ()
+
+
let open_ handle path (type_ : Types.t) = C.Functions.open_ handle path type_
+
let close : t -> unit = C.Functions.close
+
let get_type : t -> Types.t = C.Functions.get_type
+
+
let clone ?(options = Nvlist.empty) handle path =
+
let res = C.Functions.clone handle path options in
+
if res = 0 then () else invalid_arg "clone"
+
+
let snapshot ?(options = Nvlist.empty) handle path b =
+
let res = C.Functions.snapshot handle path b options in
+
if res = 0 then () else invalid_arg "snapshot"
+
+
let exists handle path (type_ : Types.t) = C.Functions.exists handle path type_
+
+
let is_mounted handle path =
+
let where = Ctypes.allocate Ctypes.string "" in
+
let v = C.Functions.is_mounted handle path where in
+
if not v then None else Some (Ctypes.( !@ ) where)
+
+
let null_string = Ctypes.(coerce (ptr void) (ptr char) null)
+
+
let mount ?mount_opts ?(mount_flags = 0) dataset =
+
let opts =
+
Option.value
+
~default:(Ctypes.string_from_ptr null_string ~length:0)
+
mount_opts
+
in
+
let res = C.Functions.mount dataset opts mount_flags in
+
if res <> 0 then invalid_arg "mounting dataset"
+
+
let unmount ?mount_opts ?(mount_flags = 0) dataset =
+
let opts =
+
Option.value
+
~default:(Ctypes.string_from_ptr null_string ~length:0)
+
mount_opts
+
in
+
let res = C.Functions.unmount dataset opts mount_flags in
+
if res <> 0 then invalid_arg "unmounting dataset"
+
+
let show_diff ?to_ handle ~from_ (fd : Unix.file_descr) =
+
(* TODO: Other Diff Flags https://github.com/openzfs/zfs/blob/5b0c27cd14bbc07d50304c97735cc105d0258673/include/libzfs.h#L917? *)
+
let res = C.Functions.diff handle (Obj.magic fd : int) from_ to_ 1 in
+
if res = 0 then () else begin
+
invalid_arg "show_diff"
+
end
+206
vendor/zfs/src/zfs.mli
···
+
module Types : sig
+
type t = private int
+
+
val empty : int
+
val of_int : 'a -> 'a
+
val ( + ) : int -> int -> int
+
val mem : int -> int -> bool
+
val vdev : t
+
val pool : t
+
val volume : t
+
val invalid : t
+
val bookmark : t
+
val snapshot : t
+
val filesystem : t
+
val dataset : t
+
end
+
+
module Handle : sig
+
type t
+
(** An instance handle for the ZFS library *)
+
end
+
+
val init : unit -> Handle.t
+
(** Initialise the library *)
+
+
val debug : Handle.t -> bool -> unit
+
(** Enable/disable printing on error from ZFS *)
+
+
val errno : Handle.t -> int
+
(** Check for errors on the handle *)
+
+
module Zpool : sig
+
type t
+
(** A Zpool handle *)
+
+
val open_ : Handle.t -> string -> t
+
(** Open a Zpool *)
+
+
val close : t -> unit
+
(** Close an open Zpool *)
+
+
val get_name : t -> string
+
(** The name of an open Zpool *)
+
end
+
+
module Nvlist : sig
+
type t
+
(** Generic name-value lists used by ZFS *)
+
+
type nvlist =
+
(string
+
* [ `Bool of bool
+
| `Byte of Unsigned.uchar
+
| `String of string
+
| `Int64 of int64 ])
+
list
+
(** A partial OCaml representation of an NV list *)
+
+
val v : nvlist -> t
+
(** Convert the OCaml representation to the C representation *)
+
end
+
+
type t
+
(** A ZFS Dataset *)
+
+
val create_ancestors : Handle.t -> string -> unit
+
(** Often called before {! create} *)
+
+
val create : ?props:Nvlist.nvlist -> Handle.t -> string -> Types.t -> unit
+
(** Create a new ZFS dataset *)
+
+
val open_ : Handle.t -> string -> Types.t -> t
+
(** Open an existing ZFS dataset *)
+
+
val close : t -> unit
+
(** Close a dataset *)
+
+
val exists : Handle.t -> string -> Types.t -> bool
+
(** Check if a dataset of a specific type exists *)
+
+
val is_mounted : Handle.t -> string -> string option
+
(** [is_mounted h d = None] if [d] is not mounted, otherwise
+
[is_mounted h d = Some mountpoint]. *)
+
+
val mount : ?mount_opts:string -> ?mount_flags:int -> t -> unit
+
(** Mount a dataset *)
+
+
val unmount : ?mount_opts:string -> ?mount_flags:int -> t -> unit
+
(** Unmount a dataset *)
+
+
val get_type : t -> Types.t
+
(** Get the type of the dataset *)
+
+
val clone : ?options:Nvlist.t -> t -> string -> unit
+
(** Clone an open dataset *)
+
+
val snapshot : ?options:Nvlist.t -> Handle.t -> string -> bool -> unit
+
(** Snapshot a dataset *)
+
+
val show_diff : ?to_:string -> t -> from_:string -> Unix.file_descr -> unit
+
(** Output diff to the file descriptor *)
+
+
module Error : sig
+
type t = int
+
+
val unknown : t
+
val sharefailed : t
+
val resume_exists : t
+
val cksum : t
+
val not_user_namespace : t
+
val vdev_notsup : t
+
val rebuilding : t
+
val export_in_progress : t
+
val no_resilver_defer : t
+
val trim_notsup : t
+
val no_trim : t
+
val trimming : t
+
val wrong_parent : t
+
val no_initialize : t
+
val initializing : t
+
val toomany : t
+
val ioc_notsupported : t
+
val vdev_too_big : t
+
val devrm_in_progress : t
+
val no_checkpoint : t
+
val discarding_checkpoint : t
+
val checkpoint_exists : t
+
val no_pending : t
+
val cryptofailed : t
+
val active_pool : t
+
val scrub_paused_to_cancel : t
+
val scrub_paused : t
+
val poolreadonly : t
+
val diffdata : t
+
val diff : t
+
val no_scrub : t
+
val errorscrub_paused : t
+
val errorscrubbing : t
+
val scrubbing : t
+
val postsplit_online : t
+
val threadcreatefailed : t
+
val pipefailed : t
+
val tagtoolong : t
+
val reftag_hold : t
+
val reftag_rele : t
+
val unplayed_logs : t
+
val active_spare : t
+
val notsup : t
+
val vdevnotsup : t
+
val isl2cache : t
+
val badcache : t
+
val sharesmbfailed : t
+
val unsharesmbfailed : t
+
val nodelegation : t
+
val badpermset : t
+
val badperm : t
+
val badwho : t
+
val labelfailed : t
+
val nocap : t
+
val openfailed : t
+
val nametoolong : t
+
val pool_invalarg : t
+
val pool_notsup : t
+
val poolprops : t
+
val nohistory : t
+
val recursive : t
+
val invalconfig : t
+
val isspare : t
+
val intr : t
+
val io : t
+
val fault : t
+
val nospc : t
+
val perm : t
+
val sharenfsfailed : t
+
val unsharenfsfailed : t
+
val umountfailed : t
+
val mountfailed : t
+
val zoned : t
+
val crosstarget : t
+
val badpath : t
+
val devoverflow : t
+
val poolunavail : t
+
val badversion : t
+
val resilvering : t
+
val noreplicas : t
+
val baddev : t
+
val nodevice : t
+
val badtarget : t
+
val badbackup : t
+
val badrestore : t
+
val invalidname : t
+
val voltoobig : t
+
val dsreadonly : t
+
val badstream : t
+
val noent : t
+
val exists : t
+
val busy : t
+
val badtype : t
+
val propspace : t
+
val propnoninherit : t
+
val proptype : t
+
val propreadonly : t
+
val badprop : t
+
val nomem : t
+
val success : t
+
end
+150
vendor/zfs/src/zfs_stubs.c
···
+
/*
+
* Copyright (C) 2020-2021 Anil Madhavapeddy
+
* Copyright (C) 2020-2021 Sadiq Jaffer
+
* Copyright (C) 2022 Christiano Haesbaert
+
*
+
* Permission to use, copy, modify, and distribute this software for any
+
* purpose with or without fee is hereby granted, provided that the above
+
* copyright notice and this permission notice appear in all copies.
+
*
+
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
*/
+
+
#include <unistd.h>
+
#include <stdio.h>
+
#include <stdint.h>
+
#include <stdbool.h>
+
+
#include <libzfs_core.h>
+
#include <libzfs.h>
+
#include <caml/alloc.h>
+
#include <caml/bigarray.h>
+
#include <caml/callback.h>
+
#include <caml/custom.h>
+
#include <caml/fail.h>
+
#include <caml/memory.h>
+
#include <caml/mlvalues.h>
+
#include <caml/signals.h>
+
#include <caml/unixsupport.h>
+
#include <caml/socketaddr.h>
+
+
#undef ZFS_DEBUG
+
#ifdef ZFS_DEBUG
+
#define dprintf(fmt, ...) fprintf(stderr, fmt, ##__VA_ARGS__)
+
#else
+
#define dprintf(fmt, ...) ((void)0)
+
#endif
+
+
value ocaml_zfs_prop_is_string(value v_prop){
+
int res;
+
res = zfs_prop_is_string(Int_val(v_prop));
+
if (res < 0) {
+
caml_failwith("Error occurred!");
+
}
+
return Val_bool(res);
+
}
+
+
#define Zfs_list_val(v) (*((struct nv_list **) Data_custom_val(v)))
+
#define Zfs_handle_val(v) (*((libzfs_handle_t **) Data_custom_val(v)))
+
#define Zfs_pool_val(v) (*((zpool_handle_t **) Data_custom_val(v)))
+
+
static void finalize_zfs_list(value v) {
+
caml_stat_free(Zfs_list_val(v));
+
Zfs_list_val(v) = NULL;
+
}
+
+
static struct custom_operations zfs_list_ops = {
+
"zfs.zfs_list_ops",
+
finalize_zfs_list,
+
custom_compare_default,
+
custom_hash_default,
+
custom_serialize_default,
+
custom_deserialize_default,
+
custom_compare_ext_default,
+
custom_fixed_length_default
+
};
+
+
static void finalize_zfs_handle(value v) {
+
caml_stat_free(Zfs_handle_val(v));
+
Zfs_handle_val(v) = NULL;
+
}
+
+
static struct custom_operations zfs_handle_ops = {
+
"zfs.zfs_handle",
+
finalize_zfs_handle,
+
custom_compare_default,
+
custom_hash_default,
+
custom_serialize_default,
+
custom_deserialize_default,
+
custom_compare_ext_default,
+
custom_fixed_length_default
+
};
+
+
static void finalize_zfs_pool(value v) {
+
caml_stat_free(Zfs_pool_val(v));
+
Zfs_pool_val(v) = NULL;
+
}
+
+
static struct custom_operations zfs_pool_ops = {
+
"zfs.zfs_pool",
+
finalize_zfs_pool,
+
custom_compare_default,
+
custom_hash_default,
+
custom_serialize_default,
+
custom_deserialize_default,
+
custom_compare_ext_default,
+
custom_fixed_length_default
+
};
+
+
// ZFS Initialisation
+
+
value
+
ocaml_zfs_init(value v_unit) {
+
CAMLparam0();
+
libzfs_handle_t* res;
+
CAMLlocal1(v_handle);
+
+
v_handle = caml_alloc_custom_mem(&zfs_handle_ops, sizeof(libzfs_handle_t*), 64);
+
res = libzfs_init();
+
Zfs_handle_val(v_handle) = res;
+
+
CAMLreturn(v_handle);
+
}
+
+
// ZFS Pools
+
+
value
+
ocaml_zfs_pool_open(value v_handle, value v_path) {
+
CAMLparam2(v_handle, v_path);
+
zpool_handle_t* res;
+
CAMLlocal1(v_pool);
+
+
if (!caml_string_is_c_safe(v_path))
+
caml_invalid_argument("ocaml_zfs_pool_open: path is not C-safe");
+
+
v_pool = caml_alloc_custom_mem(&zfs_pool_ops, sizeof(zpool_handle_t*), 64);
+
res = zpool_open(Zfs_handle_val(v_handle), String_val(v_path));
+
Zfs_pool_val(v_handle) = res;
+
+
CAMLreturn(v_handle);
+
}
+
+
value
+
ocaml_zfs_pool_get_name(value v_pool) {
+
CAMLparam1(v_pool);
+
CAMLlocal1(v_path);
+
const char* result;
+
+
result = zpool_get_name(Zfs_pool_val(v_pool));
+
v_path = caml_copy_string(result);
+
+
CAMLreturn(v_path);
+
}
+
+
+32
vendor/zfs/zfs.opam
···
+
# This file is generated by dune, edit dune-project instead
+
opam-version: "2.0"
+
synopsis: "libzfs bindings"
+
description: "OCaml bindings to libzfs"
+
maintainer: ["Patrick Ferris <patrick@sirref.org>"]
+
authors: ["Patrick Ferris <patrick@sirref.org>"]
+
license: "ISC"
+
tags: ["filesystem" "zfs"]
+
homepage: "https://github.com/patricoferris/ocaml-zfs"
+
bug-reports: "https://github.com/patricoferris/ocaml-zfs/issues"
+
depends: [
+
"ocaml"
+
"dune" {>= "3.15"}
+
"ctypes"
+
"mdx" {with-test}
+
"odoc" {with-doc}
+
]
+
build: [
+
["dune" "subst"] {dev}
+
[
+
"dune"
+
"build"
+
"-p"
+
name
+
"-j"
+
jobs
+
"@install"
+
"@runtest" {with-test}
+
"@doc" {with-doc}
+
]
+
]
+
dev-repo: "git+https://github.com/patricoferris/ocaml-zfs.git"