diff --git a/lib/build.ml b/lib/build.ml index 81f0c69b..3fa6178f 100644 --- a/lib/build.ml +++ b/lib/build.ml @@ -204,7 +204,7 @@ module Make (Raw_store : S.STORE) (Sandbox : S.SANDBOX) (Fetch : S.FETCHER) = st (resolved_secret :: result) ) (Ok []) secrets let rec run_steps t ~(context:Context.t) ~base = function - | [] -> Lwt_result.return base + | [] -> Sandbox.finished () >>= fun () -> Lwt_result.return base | op :: ops -> context.log `Heading Fmt.(str "%a" (pp_op ~context) op); let k = run_steps t ops in diff --git a/lib/docker_sandbox.ml b/lib/docker_sandbox.ml index 8cece18c..1d8316b4 100644 --- a/lib/docker_sandbox.ml +++ b/lib/docker_sandbox.ml @@ -451,6 +451,9 @@ let create (c : config) = let+ () = if Result.is_error volume_exists then create_tar_volume t else Lwt.return_unit in t +let finished () = + Lwt.return () + open Cmdliner let docs = "DOCKER BACKEND" diff --git a/lib/macos.ml b/lib/macos.ml index b09efa8c..538dcca6 100644 --- a/lib/macos.ml +++ b/lib/macos.ml @@ -1,5 +1,6 @@ (* Extensions to the Os module for macOS *) open Lwt.Syntax +open Lwt.Infix open Os let ( / ) = Filename.concat @@ -17,11 +18,10 @@ let create_new_user ~username ~home_dir ~uid ~gid = let pp s ppf = Fmt.pf ppf "[ Mac ] %s\n" s in let dscl = [ "dscl"; "."; "-create"; user ] in sudo_result ~pp:(pp "UniqueID") (dscl @ [ "UniqueID"; uid ]) >>!= fun _ -> - sudo_result ~pp:(pp "PrimaryGroupID") (dscl @ [ "PrimaryGroupID"; gid ]) - >>!= fun _ -> - sudo_result ~pp:(pp "UserShell") (dscl @ [ "UserShell"; "/bin/bash" ]) - >>!= fun _ -> - sudo_result ~pp:(pp "NFSHomeDirectory") (dscl @ [ "NFSHomeDirectory"; home_dir ]) + sudo_result ~pp:(pp "PrimaryGroupID") (dscl @ [ "PrimaryGroupID"; gid ]) >>!= fun _ -> + sudo_result ~pp:(pp "UserShell") (dscl @ [ "UserShell"; "/bin/bash" ]) >>!= fun _ -> + sudo_result ~pp:(pp "NFSHomeDirectory") (dscl @ [ "NFSHomeDirectory"; home_dir ]) >>!= fun _ -> + Lwt_result.return () let delete_user ~user = let* exists = user_exists ~user in @@ -33,48 +33,41 @@ let delete_user ~user = let user = "/Users" / user in let pp s ppf = Fmt.pf ppf "[ Mac ] %s\n" s in let delete = ["dscl"; "."; "-delete"; user ] in - sudo_result ~pp:(pp "Deleting") delete + sudo_result ~pp:(pp "Deleting") delete >>!= fun _ -> + Lwt_result.return () -let descendants ~pid = - Lwt.catch - (fun () -> - let+ s = pread ["sudo"; "pgrep"; "-P"; string_of_int pid ] in - let pids = Astring.String.cuts ~sep:"\n" s in - List.filter_map int_of_string_opt pids) - (* Errors if there are none, probably errors for other reasons too… *) - (fun _ -> Lwt.return_nil) +let rec kill_users_processes ~uid = + let pp _ ppf = Fmt.pf ppf "[ PKILL ]" in + let delete = ["pkill"; "-9"; "-U"; string_of_int uid ] in + let* t = sudo_result ~pp:(pp "PKILL") delete in + match t with + | Ok _ -> kill_users_processes ~uid + | Error (`Msg _) -> + Log.info (fun f -> f "pkill all killed"); + Lwt.return () -let kill ~pid = - let pp _ ppf = Fmt.pf ppf "[ KILL ]" in - let delete = ["kill"; "-9"; string_of_int pid ] in - let* t = sudo_result ~pp:(pp "KILL") delete in +let rec sudo_fallback cmds cmds2 ~uid = + let pp f = pp_cmd f ("", cmds) in + let* t = sudo_result ~pp cmds in match t with - | Ok () -> Lwt.return_unit + | Ok _ -> Lwt.return () | Error (`Msg m) -> - Log.warn (fun f -> f "Failed to kill process %i because %s" pid m); - Lwt.return_unit - -let kill_all_descendants ~pid = - let rec kill_all pid : unit Lwt.t = - let* ds = descendants ~pid in - let* () = Lwt_list.iter_s kill_all ds in - kill ~pid - in - kill_all pid - -let copy_template ~base ~local = - let pp s ppf = Fmt.pf ppf "[ %s ]" s in - sudo_result ~pp:(pp "RSYNC") ["rsync"; "-avq"; base ^ "/"; local] - -let change_home_directory_for ~user ~home_dir = - ["dscl"; "."; "-create"; "/Users/" ^ user ; "NFSHomeDirectory"; home_dir ] + Log.warn (fun f -> f "failed with %s" m); + (* wait a second then try to kill any user processes and retry *) + Lwt_unix.sleep 2.0 >>= fun () -> + kill_users_processes ~uid >>= fun () -> + sudo cmds2 >>= fun () -> + sudo_fallback cmds cmds2 ~uid -(* Used by the FUSE filesystem to indicate where a users home directory should be …*) -let update_scoreboard ~uid ~scoreboard ~home_dir = - ["ln"; "-Fhs"; home_dir; scoreboard ^ "/" ^ string_of_int uid] - -let remove_link ~uid ~scoreboard = - [ "rm"; scoreboard ^ "/" ^ string_of_int uid ] +let rm ~directory = + let pp _ ppf = Fmt.pf ppf "[ RM ]" in + let delete = ["rm"; "-r"; directory ] in + let* t = sudo_result ~pp:(pp "RM") delete in + match t with + | Ok _ -> Lwt.return () + | Error (`Msg m) -> + Log.warn (fun f -> f "Failed to remove %s because %s" directory m); + Lwt.return () let get_tmpdir ~user = ["sudo"; "-u"; user; "-i"; "getconf"; "DARWIN_USER_TEMP_DIR"] diff --git a/lib/os.ml b/lib/os.ml index 30215bc9..8fd55c7d 100644 --- a/lib/os.ml +++ b/lib/os.ml @@ -114,9 +114,9 @@ let sudo ?stdin args = let args = if running_as_root then args else "sudo" :: "--" :: args in exec ?stdin args -let sudo_result ?cwd ?stdin ?stdout ?stderr ~pp args = +let sudo_result ?cwd ?stdin ?stdout ?stderr ?is_success ~pp args = let args = if running_as_root then args else "sudo" :: "--" :: args in - exec_result ?cwd ?stdin ?stdout ?stderr ~pp args + exec_result ?cwd ?stdin ?stdout ?stderr ?is_success ~pp args let rec write_all fd buf ofs len = assert (len >= 0); diff --git a/lib/s.ml b/lib/s.ml index 0604252f..df4974d5 100644 --- a/lib/s.ml +++ b/lib/s.ml @@ -88,6 +88,8 @@ module type SANDBOX = sig @param stdin Passed to child as its standard input. @param log Used for child's stdout and stderr. *) + + val finished : unit -> unit Lwt.t end module type BUILDER = sig diff --git a/lib/sandbox.macos.ml b/lib/sandbox.macos.ml index aadc8ce3..8e568ebc 100644 --- a/lib/sandbox.macos.ml +++ b/lib/sandbox.macos.ml @@ -4,19 +4,8 @@ open Cmdliner type t = { uid: int; gid: int; - (* Where zfs dynamic libraries are -- can't be in /usr/local/lib - see notes in .mli file under "Various Gotchas"… *) - fallback_library_path : string; - (* FUSE file system mount point *) - fuse_path : string; - (* Scoreboard -- where we keep our symlinks for knowing homedirs for users *) - scoreboard : string; - (* Should the sandbox mount and unmount the FUSE filesystem *) - no_fuse : bool; - (* Whether or not the FUSE filesystem is mounted *) - mutable fuse_mounted : bool; - (* Whether we have chowned/chmoded the data *) - mutable chowned : bool; + (* mount point where Homebrew is installed. Either /opt/homebrew or /usr/local depending upon architecture *) + brew_path : string; lock : Lwt_mutex.t; } @@ -24,10 +13,7 @@ open Sexplib.Conv type config = { uid: int; - fallback_library_path : string; - fuse_path : string; - scoreboard : string; - no_fuse : bool; + brew_path : string; }[@@deriving sexp] let run_as ~env ~user ~cmd = @@ -49,66 +35,34 @@ let copy_to_log ~src ~dst = in aux () -(* HACK: Unmounting and remounting the FUSE filesystem seems to "fix" - some weird cachining bug, see https://github.com/patricoferris/obuilder/issues/9 - - For macOS we also need to create the illusion of building in a static - home directory, and to achieve this we copy in the pre-build environment - and copy back out the result. It's not super efficient, but is necessary.*) - -let unmount_fuse t = - if not t.fuse_mounted || t.no_fuse then Lwt.return_unit - else - let f = ["umount"; "-f"; t.fuse_path] in - Os.sudo f >>= fun _ -> t.fuse_mounted <- false; - Lwt.return_unit - -let post_build ~result_dir ~home_dir t = - Os.sudo ["rsync"; "-aHq"; "--delete"; home_dir ^ "/"; result_dir ] >>= fun () -> - unmount_fuse t - -let post_cancellation ~result_tmp t = - Os.rm ~directory:result_tmp >>= fun () -> - unmount_fuse t - -(* Using rsync to delete old files seems to be a good deal faster. *) -let pre_build ~result_dir ~home_dir t = - Os.sudo [ "mkdir"; "-p"; "/tmp/obuilder-empty" ] >>= fun () -> - Os.sudo [ "rsync"; "-aHq"; "--delete"; "/tmp/obuilder-empty/"; home_dir ^ "/" ] >>= fun () -> - Os.sudo [ "rsync"; "-aHq"; result_dir ^ "/"; home_dir ] >>= fun () -> - (if t.chowned then Lwt.return_unit - else begin - Os.sudo [ "chown"; "-R"; ":" ^ (string_of_int t.gid); home_dir ] >>= fun () -> - Os.sudo [ "chmod"; "-R"; "g+w"; home_dir ] >>= fun () -> - t.chowned <- true; - Lwt.return_unit - end) >>= fun () -> - if t.fuse_mounted || t.no_fuse then Lwt.return_unit - else - let f = [ "obuilderfs"; t.scoreboard ; t.fuse_path; "-o"; "allow_other" ] in - Os.sudo f >>= fun _ -> t.fuse_mounted <- true; - Lwt.return_unit - let user_name ~prefix ~uid = Fmt.str "%s%i" prefix uid -let home_directory user = Filename.concat "/Users/" user +let zfs_volume_from path = + String.split_on_char '/' path + |> List.filter (fun x -> String.length x > 0) + |> List.tl + |> String.concat "/" -(* A build step in macos: - - Should be properly sandboxed using sandbox-exec (coming soon…) - - Umask g+w to work across users if restored from a snapshot - - Set the new home directory of the user to something static and copy in the environment - - Should be executed by the underlying user (t.uid) *) let run ~cancelled ?stdin:stdin ~log (t : t) config result_tmp = Lwt_mutex.with_lock t.lock (fun () -> + Log.info (fun f -> f "result_tmp = %s" result_tmp); Os.with_pipe_from_child @@ fun ~r:out_r ~w:out_w -> - let result_dir = Filename.concat result_tmp "rootfs" in let user = user_name ~prefix:"mac" ~uid:t.uid in - let home_dir = home_directory user in + let zfs_volume = zfs_volume_from result_tmp in + let home_dir = Filename.concat "/Users/" user in + let zfs_home_dir = Filename.concat zfs_volume "home" in + let zfs_brew = Filename.concat zfs_volume "brew" in + Os.sudo [ "zfs"; "set"; "mountpoint=" ^ home_dir; zfs_home_dir ] >>= fun () -> + Os.sudo [ "zfs"; "set"; "mountpoint=" ^ t.brew_path; zfs_brew ] >>= fun () -> + Lwt_list.iter_s (fun { Config.Mount.src; dst; readonly; _ } -> + Log.info (fun f -> f "src = %s, dst = %s, type %s" src dst (if readonly then "ro" else "rw") ); + if Sys.file_exists dst then + Os.sudo [ "zfs"; "set"; "mountpoint=" ^ dst; zfs_volume_from src ] + else Lwt.return_unit) config.Config.mounts >>= fun () -> let uid = string_of_int t.uid in - Macos.create_new_user ~username:user ~home_dir ~uid ~gid:"1000" >>= fun _ -> - let set_homedir = Macos.change_home_directory_for ~user ~home_dir in - let update_scoreboard = Macos.update_scoreboard ~uid:t.uid ~home_dir ~scoreboard:t.scoreboard in + let gid = string_of_int t.gid in + Macos.create_new_user ~username:user ~home_dir ~uid ~gid >>= fun _ -> let osenv = config.Config.env in let stdout = `FD_move_safely out_w in let stderr = stdout in @@ -117,9 +71,6 @@ let run ~cancelled ?stdin:stdin ~log (t : t) config result_tmp = let proc = let stdin = Option.map (fun x -> `FD_move_safely x) stdin in let pp f = Os.pp_cmd f ("", config.Config.argv) in - Os.sudo_result ~pp set_homedir >>= fun _ -> - Os.sudo_result ~pp update_scoreboard >>= fun _ -> - pre_build ~result_dir ~home_dir t >>= fun _ -> Os.pread @@ Macos.get_tmpdir ~user >>= fun tmpdir -> let tmpdir = List.hd (String.split_on_char '\n' tmpdir) in let env = ("TMPDIR", tmpdir) :: osenv in @@ -128,38 +79,41 @@ let run ~cancelled ?stdin:stdin ~log (t : t) config result_tmp = let pid, proc = Os.open_process ?stdin ~stdout ~stderr ~pp ~cwd:config.Config.cwd cmd in proc_id := Some pid; Os.process_result ~pp proc >>= fun r -> - post_build ~result_dir ~home_dir t >>= fun () -> Lwt.return r in Lwt.on_termination cancelled (fun () -> let aux () = - (if Lwt.is_sleeping proc then - match !proc_id with - | Some pid -> Macos.kill_all_descendants ~pid >>= fun () -> Lwt_unix.sleep 5.0 - | None -> Log.warn (fun f -> f "Failed to find pid…"); Lwt.return_unit - else Lwt.return_unit) (* Process has already finished *) - >>= fun () -> post_cancellation ~result_tmp t + if Lwt.is_sleeping proc then + match !proc_id with + | Some _ -> Macos.kill_users_processes ~uid:t.uid + | None -> Log.warn (fun f -> f "Failed to find pid…"); Lwt.return () + else Lwt.return_unit (* Process has already finished *) in Lwt.async aux ); proc >>= fun r -> copy_log >>= fun () -> - if Lwt.is_sleeping cancelled then Lwt.return (r :> (unit, [`Msg of string | `Cancelled]) result) - else Lwt_result.fail `Cancelled) + Lwt_list.iter_s (fun { Config.Mount.src; dst = _; readonly = _; ty = _ } -> + Os.sudo [ "zfs"; "inherit"; "mountpoint"; zfs_volume_from src ]) config.Config.mounts >>= fun () -> + Macos.sudo_fallback [ "zfs"; "set"; "mountpoint=none"; zfs_home_dir ] [ "zfs"; "unmount"; "-f"; zfs_home_dir ] ~uid:t.uid >>= fun () -> + Macos.sudo_fallback [ "zfs"; "set"; "mountpoint=none"; zfs_brew ] [ "zfs"; "unmount"; "-f"; zfs_brew ] ~uid:t.uid >>= fun () -> + if Lwt.is_sleeping cancelled then + Lwt.return (r :> (unit, [`Msg of string | `Cancelled]) result) + else Lwt_result.fail `Cancelled) let create ~state_dir:_ c = Lwt.return { uid = c.uid; gid = 1000; - fallback_library_path = c.fallback_library_path; - fuse_path = c.fuse_path; - scoreboard = c.scoreboard; - no_fuse = c.no_fuse; - fuse_mounted = false; - chowned = false; + brew_path = c.brew_path; lock = Lwt_mutex.create (); } +let finished () = + Os.sudo [ "zfs"; "unmount"; "obuilder/result" ] >>= fun () -> + Os.sudo [ "zfs"; "mount"; "obuilder/result" ] >>= fun () -> + Lwt.return () + let uid = Arg.required @@ Arg.opt Arg.(some int) None @@ @@ -169,43 +123,16 @@ let uid = ~docv:"UID" ["uid"] -let fallback_library_path = +let brew_path = Arg.required @@ Arg.opt Arg.(some file) None @@ Arg.info - ~doc:"The fallback path of the dynamic libraries. This is used whenever the FUSE filesystem \ - is in place preventing anything is /usr/local from being accessed." - ~docv:"FALLBACK" - ["fallback"] - -let fuse_path = - Arg.required @@ - Arg.opt Arg.(some file) None @@ - Arg.info - ~doc:"Directory to mount FUSE filesystem on, typically this is either /usr/local or /opt/homebrew." - ~docv:"FUSE_PATH" - ["fuse-path"] - -let scoreboard = - Arg.required @@ - Arg.opt Arg.(some file) None @@ - Arg.info - ~doc:"The scoreboard directory which is used by the FUSE filesystem to record \ - the association between user id and home directory." - ~docv:"SCOREBOARD" - ["scoreboard"] - -let no_fuse = - Arg.value @@ - Arg.flag @@ - Arg.info - ~doc:"Whether the macOS sandbox should mount and unmount the FUSE filesystem. \ - This is useful for testing." - ~docv:"NO-FUSE" - ["no-fuse"] + ~doc:"Directory where Homebrew is installed. Typically this is either /usr/local or /opt/homebrew." + ~docv:"BREW_PATH" + ["brew-path"] let cmdliner : config Term.t = - let make uid fallback_library_path fuse_path scoreboard no_fuse = - { uid; fallback_library_path; fuse_path; scoreboard; no_fuse } + let make uid brew_path = + { uid; brew_path } in - Term.(const make $ uid $ fallback_library_path $ fuse_path $ scoreboard $ no_fuse) + Term.(const make $ uid $ brew_path) diff --git a/lib/sandbox.mli b/lib/sandbox.mli index e4906308..fc46c63b 100644 --- a/lib/sandbox.mli +++ b/lib/sandbox.mli @@ -12,3 +12,7 @@ val cmdliner : config Cmdliner.Term.t val create : state_dir:string -> config -> t Lwt.t (** [create ~state_dir config] is a sandboxing system that keeps state in [state_dir] and is configured using [config]. *) + +val finished : unit -> unit Lwt.t +(** [finished] is a call back to the sandbox which is triggered when the current job + is finished. The sandbox may choose do nothing. *) diff --git a/lib/sandbox.runc.ml b/lib/sandbox.runc.ml index f8743531..26045fc9 100644 --- a/lib/sandbox.runc.ml +++ b/lib/sandbox.runc.ml @@ -333,6 +333,9 @@ let create ~state_dir (c : config) = clean_runc state_dir >|= fun () -> { runc_state_dir = state_dir; fast_sync = c.fast_sync; arches } +let finished () = + Lwt.return () + open Cmdliner let docs = "RUNC SANDBOX" diff --git a/lib/user_temp.ml b/lib/user_temp.ml index 189af0c8..4792b725 100644 --- a/lib/user_temp.ml +++ b/lib/user_temp.ml @@ -1,8 +1,9 @@ open Lwt.Infix +let ( / ) = Filename.concat + let fetch ~log:_ ~rootfs base = - let base = Filename.concat "/Users" base in - Macos.copy_template ~base ~local:rootfs >>= fun _ -> - Os.sudo [ "chown"; "-R"; ":1000"; rootfs ] >>= fun () -> - Os.sudo [ "chmod"; "-R"; "g+w"; rootfs ] >>= fun () -> - Lwt.return_nil + let zfs_volume = String.sub rootfs 9 (String.length rootfs - 16) in (* remove /Volume/ from front and /rootfs from the end *) + Os.sudo [ "zfs"; "clone"; "-o"; "mountpoint=none"; "obuilder" / "base-image" / base / "home@snap"; zfs_volume / "home" ] >>= fun () -> + Os.sudo [ "zfs"; "clone"; "-o"; "mountpoint=none"; "obuilder" / "base-image" / base / "brew@snap"; zfs_volume / "brew" ] >>= fun () -> + Lwt.return [] diff --git a/lib/zfs_store.ml b/lib/zfs_store.ml index 09dfa5a1..4f870333 100644 --- a/lib/zfs_store.ml +++ b/lib/zfs_store.ml @@ -1,4 +1,5 @@ open Lwt.Infix +open Lwt.Syntax (* This is rather complicated, because (unlike btrfs): - zfs won't let you delete datasets that other datasets are cloned from. @@ -50,10 +51,10 @@ module Dataset : sig val cache : string -> dataset val cache_tmp : int -> string -> dataset - val full_name : ?snapshot:string -> t -> dataset -> string + val full_name : ?snapshot:string -> ?subvolume:string -> t -> dataset -> string val path : ?snapshot:string -> t -> dataset -> string - val exists : ?snapshot:string -> t -> dataset -> bool + val exists : ?snapshot:string -> t -> dataset -> bool Lwt.t val if_missing : ?snapshot:string -> t -> dataset -> (unit -> unit Lwt.t) -> unit Lwt.t end = struct type dataset = string @@ -69,10 +70,12 @@ end = struct let cache name = "cache/" ^ Escape.cache name let cache_tmp i name = strf "cache-tmp/%d-%s" i (Escape.cache name) - let full_name ?snapshot t ds = - match snapshot with - | None -> strf "%s/%s" t.pool ds - | Some snapshot -> strf "%s/%s@%s" t.pool ds snapshot + let full_name ?snapshot ?subvolume t ds = + match snapshot, subvolume with + | None, None -> strf "%s/%s" t.pool ds + | Some snapshot, None -> strf "%s/%s@%s" t.pool ds snapshot + | None, Some subvolume -> strf "%s/%s/%s" t.pool ds subvolume + | Some snapshot, Some subvolume -> strf "%s/%s/%s@%s" t.pool ds subvolume snapshot let path ?snapshot t ds = match snapshot with @@ -80,13 +83,14 @@ end = struct | Some snapshot -> strf "%s%s/%s/.zfs/snapshot/%s" t.prefix t.pool ds snapshot let exists ?snapshot t ds = - match Os.check_dir (path ?snapshot t ds) with - | `Missing -> false - | `Present -> true + Lwt_process.pread ("", [| "zfs"; "list"; "-p"; "-H"; full_name t ds ?snapshot |]) >>= function + | "" -> Lwt.return false + | _ -> Lwt.return true let if_missing ?snapshot t ds fn = - if exists ?snapshot t ds then Lwt.return_unit - else fn () + exists ?snapshot t ds >>= function + | true -> Lwt.return_unit + | false -> fn () end let user = `Unix { Obuilder_spec.uid = Unix.getuid (); gid = Unix.getgid () } @@ -102,9 +106,9 @@ module Zfs = struct let destroy t ds mode = let opts = match mode with - | `Only -> [] - | `And_snapshots -> ["-r"] - | `And_snapshots_and_clones -> ["-R"] + | `Only -> ["-f"] + | `And_snapshots -> ["-r"; "-f"] + | `And_snapshots_and_clones -> ["-R"; "-f"] in Os.sudo (["zfs"; "destroy"] @ opts @ ["--"; Dataset.full_name t ds]) @@ -120,8 +124,37 @@ module Zfs = struct let clone t ~src ~snapshot dst = Os.sudo ["zfs"; "clone"; "--"; Dataset.full_name t src ~snapshot; Dataset.full_name t dst] + let mounted ?snapshot t ~ds = + Lwt_process.pread ("", [| "zfs"; "get"; "-pH"; "mounted"; Dataset.full_name t ds ?snapshot |]) >>= fun s -> + match ( Scanf.sscanf s "%s %s %s %s" (fun _ _ yesno _ -> yesno = "yes") ) with + | state -> Lwt.return state + | exception Scanf.Scan_failure _ -> Lwt.return false + + let mount ?snapshot t ~ds = + mounted t ~ds ?snapshot >>= fun m -> + if not m then + let pp _ ppf = Fmt.pf ppf "zfs mount" in + let* t = Os.sudo_result ~pp:(pp "zfs mount") ~is_success:(fun n -> n = 0 || n = 16) ["zfs"; "mount"; "--"; Dataset.full_name t ds ?snapshot] in + match t with + | Ok () -> Lwt.return () + | Error (`Msg m) -> + Log.info (fun f -> f "%s" m); + Lwt.return () + else Lwt.return () + + let clone_with_children t ~src ~snapshot dst = + Os.sudo ["zfs"; "clone"; "-o"; "canmount=noauto"; "--"; Dataset.full_name t src ~snapshot; Dataset.full_name t dst] >>= fun () -> + Os.sudo ["zfs"; "mount"; Dataset.full_name t dst] >>= fun () -> + let vol = Dataset.full_name t src in + let len = String.length vol in + Lwt_process.pread ("", [| "zfs"; "list"; "-H"; "-r"; "-o"; "name"; vol |]) >>= fun output -> + String.split_on_char '\n' output |> List.map (fun s -> (s, String.length s)) |> + List.filter (fun (_, l) -> l > len) |> List.map (fun (s, l) -> String.sub s (len + 1) (l - len - 1)) |> + Lwt_list.iter_s (fun subvolume -> Os.sudo ["zfs"; "clone"; "-o"; "mountpoint=none"; "--"; + Dataset.full_name t src ~subvolume ~snapshot; Dataset.full_name t dst ~subvolume]) + let snapshot t ds ~snapshot = - Os.sudo ["zfs"; "snapshot"; "--"; Dataset.full_name t ds ~snapshot] + Os.sudo ["zfs"; "snapshot"; "-r"; "--"; Dataset.full_name t ds ~snapshot] let promote t ds = Os.sudo ["zfs"; "promote"; Dataset.full_name t ds] @@ -136,8 +169,9 @@ module Zfs = struct end let delete_if_exists t ds mode = - if Dataset.exists t ds then Zfs.destroy t ds mode - else Lwt.return_unit + Dataset.exists t ds >>= function + | true -> Zfs.destroy t ds mode + | false -> Lwt.return_unit let state_dir t = Dataset.path t Dataset.state @@ -193,7 +227,7 @@ let build t ?base ~id fn = Zfs.chown ~user t ds | Some base -> let src = Dataset.result base in - Zfs.clone t ~src ~snapshot:default_snapshot ds + Zfs.clone_with_children t ~src ~snapshot:default_snapshot ds end >>= fun () -> Lwt.try_bind @@ -207,19 +241,23 @@ let build t ?base ~id fn = Lwt_result.return () | Error _ as e -> Log.debug (fun f -> f "zfs: build %S failed" id); - Zfs.destroy t ds `Only >>= fun () -> + Zfs.destroy t ds `And_snapshots >>= fun () -> Lwt.return e ) (fun ex -> Log.warn (fun f -> f "Uncaught exception from %S build function: %a" id Fmt.exn ex); - Zfs.destroy t ds `Only >>= fun () -> + Zfs.destroy t ds `And_snapshots >>= fun () -> Lwt.fail ex ) let result t id = let ds = Dataset.result id in - let path = Dataset.path t ds ~snapshot:default_snapshot in - if Sys.file_exists path then Lwt.return_some path + Dataset.exists t ds ~snapshot:default_snapshot >>= fun e -> + if e then + Zfs.mount t ~ds >>= fun () -> + Zfs.mount t ~ds ~snapshot:default_snapshot >>= fun () -> + let path = Dataset.path t ds ~snapshot:default_snapshot in + Lwt.return_some path else Lwt.return_none let log_file t id = @@ -335,13 +373,12 @@ let delete_cache t name = Lwt_mutex.with_lock cache.lock @@ fun () -> Log.debug (fun f -> f "zfs: delete_cache %S" (name :> string)); if cache.n_clones > 0 then Lwt_result.fail `Busy - else ( + else let main_ds = Dataset.cache name in - if Dataset.exists t main_ds then ( - Zfs.destroy t main_ds `And_snapshots >>= fun () -> + Dataset.exists t main_ds >>= function + | true -> Zfs.destroy t main_ds `And_snapshots >>= fun () -> Lwt_result.return () - ) else Lwt_result.return () - ) + | false -> Lwt_result.return () let complete_deletes _t = (* The man-page says "Pending changes are generally accounted for within a few seconds" *) diff --git a/test/mock_sandbox.ml b/test/mock_sandbox.ml index eb22bf44..57dbf5ee 100644 --- a/test/mock_sandbox.ml +++ b/test/mock_sandbox.ml @@ -22,3 +22,5 @@ let run ~cancelled ?stdin ~log t (config:Obuilder.Config.t) dir = ) let create () = { expect = Queue.create () } + +let finished () = Lwt.return () diff --git a/test/mock_sandbox.mli b/test/mock_sandbox.mli index dd44d05c..a61ef6de 100644 --- a/test/mock_sandbox.mli +++ b/test/mock_sandbox.mli @@ -9,3 +9,4 @@ val expect : string -> (unit, [`Msg of string | `Cancelled]) Lwt_result.t) -> unit +val finished : unit -> unit Lwt.t