diff --git a/example/main.ml b/example/main.ml index e5645f3..674c3f4 100644 --- a/example/main.ml +++ b/example/main.ml @@ -1,3 +1,5 @@ +open Eio + let ( / ) = Eio.Path.( / ) module L = Eio_mem.Low_level @@ -5,12 +7,8 @@ module L = Eio_mem.Low_level let () = Eio_main.run @@ fun e -> Eio_mem.run @@ fun env -> - let () = - Eio.Path.with_open_out ~create:(`If_missing 0o644) (env#fs / "hello.txt") - @@ fun flow -> - let fd = Eio_mem.Resource.fd_opt flow |> Option.get in - let nfd = L.dup fd in - let _ : int = L.writev nfd [ Cstruct.of_string "Hello, World" ] in - () - in - Eio.traceln "Got: %s" (Eio.Path.load (env#fs / "hello.txt")) + Eio.Switch.run @@ fun sw -> + let r, w = L.pipe sw in + let _ : int = L.writev w [ Cstruct.of_string "Hello, world!" ] in + L.close w; + Eio.Flow.copy (Eio_mem.Flow.of_fd r) e#stdout diff --git a/src/eio_mem.ml b/src/eio_mem.ml index cbc83f6..c7243a3 100644 --- a/src/eio_mem.ml +++ b/src/eio_mem.ml @@ -17,6 +17,7 @@ open Eio.Std module Resource = Resource module Low_level = Low_level +module Flow = Flow module Fs = struct module Config = struct @@ -92,7 +93,7 @@ module Fs = struct if Filename.is_relative path then Filename.concat t.dir_path path else path in - let d = v ~label ~path:full_path (Low_level.Dir.Dir_fd fd) in + let d = v ~label ~path:full_path (Low_level.Dir.Dir_fd fd.fd) in Eio.Resource.T (d, Handler.v) let chown ~follow:_ ?uid:_ ?gid:_ _t _p = () diff --git a/src/eio_mem.mli b/src/eio_mem.mli index 1c512b1..648c0c2 100644 --- a/src/eio_mem.mli +++ b/src/eio_mem.mli @@ -23,6 +23,7 @@ module Fs : sig end module Low_level = Low_level +module Flow = Flow module Resource : sig type 'a t = ([> `Mem_fd ] as 'a) Eio.Resource.t diff --git a/src/low_level.ml b/src/low_level.ml index 55cc050..362be8a 100644 --- a/src/low_level.ml +++ b/src/low_level.ml @@ -67,21 +67,7 @@ module Error = struct raise exn end -module Fd : sig - type t = private int - - val pp : t Fmt.t - val compare : t -> t -> int - val of_int : int -> t - val incr : t -> t -end = struct - type t = int - - let pp ppf i = Fmt.pf ppf "fd-%i" i - let compare (f1 : t) (f2 : t) = Int.compare (f1 :> int) (f2 :> int) - let of_int i = i - let incr t = t + 1 -end +module Fd_table = Map.Make (Int) module Inode = struct type t = int64 @@ -99,7 +85,7 @@ end module Dir = struct type 'a t = { name : string; stat : Eio.File.Stat.t; entries : 'a list } - type fd = Fs | Cwd | Dir_fd of Fd.t + type fd = Fs | Cwd | Dir_fd of Int.t let make ~parent name stat = { name; stat; entries = [ (stat.ino, "."); (parent, "..") ] } @@ -116,7 +102,17 @@ module Symlink = struct let make ~link_to name stat = { name; stat; link_to } end -module Fd_table = Map.Make (Fd) +module Pipe = struct + type t = { mutable closed : bool; chunks : Cstruct.t option Eio.Stream.t } + + let make () = { closed = false; chunks = Eio.Stream.create max_int } + let write t buf = Eio.Stream.add t.chunks (Some buf) + let read t = Eio.Stream.take t.chunks + + let close t = + t.closed <- true; + Eio.Stream.add t.chunks None +end module Resource = struct type t = @@ -126,11 +122,13 @@ module Resource = struct | File of File.t | Dir of (Inode.t * string) Dir.t | Symlink of Symlink.t + | Pipe of Pipe.t let pp ppf = function | Stdin -> Fmt.string ppf "stdin" | Stdout -> Fmt.string ppf "stdout" | Stderr -> Fmt.string ppf "stderr" + | Pipe _ -> Fmt.string ppf "pipe" | File f -> Fmt.pf ppf "file:%s:[%a]" f.name Fmt.(truncated ~max:12) @@ -165,6 +163,70 @@ end type resource = Resource.t +let std_table = + let open Resource in + Fd_table.add 0 (ref Stdin) Fd_table.empty + |> Fd_table.add 1 (ref Stdout) + |> Fd_table.add 2 (ref Stderr) + +let fd_table : resource ref Fd_table.t ref = ref std_table + +let update_fd_table_with_result fn = + let v, new_fd_table = fn !fd_table in + fd_table := new_fd_table; + v + +let update_fd_table fn = + let () = update_fd_table_with_result (fun fd -> ((), fn fd)) in + () + +module Fd : sig + type t = { fd : int; mutable hook : Eio.Switch.hook } + + val pp : t Fmt.t + val compare : t -> t -> int + val of_int_no_switch : int -> t + val of_int : sw:Eio.Switch.t -> int -> t + val incr : sw:Eio.Switch.t -> t -> t + val close : t -> unit +end = struct + type t = { fd : int; mutable hook : Eio.Switch.hook } + + let pp ppf t = Fmt.pf ppf "fd-%i" t.fd + let compare (f1 : t) (f2 : t) = Int.compare f1.fd f2.fd + + let close fd = + Eio.Switch.remove_hook fd.hook; + update_fd_table @@ fun fds -> + match Fd_table.find_opt fd.fd fds with + | None -> Error.badf ~msg:"close" () + | Some r -> ( + match !r with + | Pipe p -> + Pipe.close p; + Fd_table.remove fd.fd fds + | _ -> Fd_table.remove fd.fd fds) + + let of_int_no_switch fd = { fd; hook = Eio.Switch.null_hook } + + let of_int ~sw fd = + let v = of_int_no_switch fd in + v.hook <- Eio.Switch.on_release_cancellable sw (fun () -> close v); + v + + let incr ~sw t = of_int ~sw (t.fd + 1) +end + +module Fd_tbl = struct + include Map.Make (Int) + + let find_opt fd tbl = find_opt fd.Fd.fd tbl + let find' = find + let find fd tbl = find fd.Fd.fd tbl + let add k v tbl = add k.Fd.fd v tbl + let remove k tbl = remove k.Fd.fd tbl +end + let get_dir ~label = function | Resource.Dir dir -> dir | r -> Fmt.invalid_arg "get_dir(%s): %a" label Resource.pp r @@ -182,61 +244,41 @@ let reset_file ~truncate ~append r = r := File { f with contents; stat; pos } | _ -> () -let std_table = - let open Resource in - Fd_table.add (Fd.of_int 0) (ref Stdin) Fd_table.empty - |> Fd_table.add (Fd.of_int 1) (ref Stdout) - |> Fd_table.add (Fd.of_int 2) (ref Stderr) - -let fd_table : resource ref Fd_table.t ref = ref std_table - -let update_fd_table_with_result fn = - let v, new_fd_table = fn !fd_table in - fd_table := new_fd_table; - v - -let update_fd_table fn = - let () = update_fd_table_with_result (fun fd -> ((), fn fd)) in - () - let update_entry ?(if_missing = fun () -> ()) fd fds fn = - match Fd_table.find_opt fd fds with None -> if_missing () | Some e -> fn e + match Fd_tbl.find_opt fd fds with None -> if_missing () | Some e -> fn e let read_fd_table fn = fn !fd_table -let next_fd_from_fds fds = - Fd_table.to_list fds - |> List.sort (fun (i, _) (j, _) -> Fd.compare i j) - |> List.rev |> List.hd |> fst |> Fd.incr +let next_fd_from_fds ~sw fds = + Fd_tbl.to_list fds + |> List.sort (fun (i, _) (j, _) -> Int.compare i j) + |> List.rev |> List.hd |> fst + |> fun i -> Fd.of_int ~sw (i + 1) -let next_fd () = read_fd_table next_fd_from_fds +let next_fd sw = read_fd_table (next_fd_from_fds ~sw) -let dup fd = - let r = read_fd_table (Fd_table.find_opt fd) in +let dup ~sw fd = + let r = read_fd_table (Fd_tbl.find_opt fd) in match r with | None -> Error.badf () | Some r -> - let new_fd = next_fd () in - let () = update_fd_table @@ fun tbl -> Fd_table.add new_fd r tbl in + let new_fd = next_fd sw in + let () = update_fd_table @@ fun tbl -> Fd_tbl.add new_fd r tbl in new_fd -let close fd = - update_fd_table @@ fun fds -> - match Fd_table.find_opt fd fds with - | None -> Error.badf ~msg:"close" () - | Some _ -> Fd_table.remove fd fds +let close fd = Fd.close fd let dup2 ~src ~tgt = - let src_r = read_fd_table (Fd_table.find_opt src) in - let tgt_r = read_fd_table (Fd_table.find_opt tgt) in + let src_r = read_fd_table (Fd_tbl.find_opt src) in + let tgt_r = read_fd_table (Fd_tbl.find_opt tgt) in match src_r with | None -> Error.badf () | Some r -> ( match tgt_r with - | None -> update_fd_table @@ fun tbl -> Fd_table.add tgt r tbl + | None -> update_fd_table @@ fun tbl -> Fd_tbl.add tgt r tbl | Some _ -> close tgt; - update_fd_table @@ fun tbl -> Fd_table.add tgt r tbl) + update_fd_table @@ fun tbl -> Fd_tbl.add tgt r tbl) let default_stat = Eio.File.Stat. @@ -418,7 +460,7 @@ module Filesystem = struct if is_confined ~root:cwd_path resource_path then r else Error.permission_denied resource_path | Dir.Dir_fd dir -> - let dir = read_fd_table (Fd_table.find dir) in + let dir = read_fd_table (Fd_tbl.find' dir) in let resource_path, r = find_entry_relative_to_dir ~check:true ~follow fs dir path in @@ -432,7 +474,7 @@ module Filesystem = struct end let find_by_inode inode fds = - Fd_table.to_list fds + Fd_tbl.to_list fds |> List.find_opt (fun (_, v) -> match v with | Resource.File f -> f.stat.ino = inode @@ -443,6 +485,16 @@ let entries = function | Resource.Dir { Dir.entries; _ } -> entries | r -> Fmt.invalid_arg "entries: %a" Resource.pp r +let pipe sw = + let q = Pipe.make () in + let r = ref (Resource.Pipe q) in + let rfd = next_fd sw in + let () = update_fd_table @@ Fd_tbl.add rfd r in + let wfd = next_fd sw in + update_fd_table_with_result @@ fun tbl -> + let tbl = Fd_tbl.add wfd r tbl in + ((rfd, wfd), tbl) + let openat ~mode:_ ~sw dir_fd path flags = let path = check_path path in let is_create = Open_flags.(mem o_creat flags) in @@ -454,10 +506,10 @@ let openat ~mode:_ ~sw dir_fd path flags = match is_create || is_excl with | false -> update_fd_table_with_result @@ fun fds -> - let fd = next_fd_from_fds fds in let r = Filesystem.lookup ~follow dir_fd path in reset_file ~truncate ~append r; - (fd, Fd_table.add fd r fds) + let fd = next_fd_from_fds fds ~sw in + (fd, Fd_tbl.add fd r fds) | true -> ( let () = try @@ -467,16 +519,15 @@ let openat ~mode:_ ~sw dir_fd path flags = with Eio.Exn.Io (Eio.Fs.(E (Not_found _)), _) -> () in update_fd_table_with_result @@ fun fds -> - let fd = next_fd_from_fds fds in let r = try Option.some @@ Filesystem.lookup ~follow dir_fd path with Eio.Exn.Io (Eio.Fs.(E (Not_found _)), _) -> None in match r with | Some r -> - let fds = Fd_table.add fd r fds in + let fd = next_fd_from_fds fds ~sw in + let fds = Fd_tbl.add fd r fds in reset_file ~truncate ~append r; - Eio.Switch.on_release sw (fun () -> close fd); (fd, fds) | None -> let fname = Filename.basename path in @@ -495,8 +546,8 @@ let openat ~mode:_ ~sw dir_fd path flags = rdir := Resource.Dir dir; let new_file = ref (Resource.File f) in Filesystem.add_file f.stat.ino new_file; - let fds = Fd_table.add fd new_file fds in - Eio.Switch.on_release sw (fun () -> close fd); + let fd = next_fd_from_fds fds ~sw in + let fds = Fd_tbl.add fd new_file fds in (fd, fds)) let statat ~follow dir_fd path = @@ -631,7 +682,7 @@ let rename dir_fd old_path new_dir new_path = let stat fd = read_fd_table @@ fun fds -> - match Fd_table.find_opt fd fds |> Option.map ( ! ) with + match Fd_tbl.find_opt fd fds |> Option.map ( ! ) with | None -> Error.badf () | Some (File f) -> f.stat | Some (Dir dir) -> dir.stat @@ -661,6 +712,7 @@ let writev fd bufs = let () = update_entry fd fds @@ fun r -> match !r with + | Pipe p -> List.iter (Pipe.write p) bufs | File e -> let size = e.stat.size in (* If the position is equal to the size then we just append. *) @@ -690,12 +742,30 @@ let writev fd bufs = in (Cstruct.length (Cstruct.concat bufs), fds) +let blit_into_bufs buf bufs = + let i = ref 0 in + let len = Cstruct.length buf in + List.iter + (fun obuf -> + if !i >= len then () + else + let olen = Cstruct.length obuf in + let actual_len = min (len - !i) olen in + Cstruct.blit buf !i obuf 0 actual_len; + i := !i + actual_len) + bufs; + !i + let readv fd bufs = update_fd_table_with_result @@ fun fds -> let read = ref 0 in let () = update_entry fd fds @@ fun r -> match !r with + | Pipe p -> ( + match Pipe.read p with + | Some buf -> read := blit_into_bufs buf bufs + | None -> raise End_of_file) | File e -> let pos = ref e.pos in let size = Optint.Int63.to_int e.stat.size in diff --git a/test/fs.md b/test/fs.md index cf78672..452fc50 100644 --- a/test/fs.md +++ b/test/fs.md @@ -383,12 +383,15 @@ Confined: # Low level API +Using `dup` to create a new file descriptor. + ```ocaml # run ~clear:[ "hello.txt"; "world.txt" ] @@ fun env -> let () = + Eio.Switch.run @@ fun sw -> Eio.Path.with_open_out ~create:(`If_missing 0o644) (env#fs / "hello.txt") @@ fun flow -> let fd = Eio_mem.Resource.fd_opt flow |> Option.get in - let nfd = L.dup fd in + let nfd = L.dup ~sw fd in let _ : int = L.writev nfd [ Cstruct.of_string "Hello, World" ] in () in @@ -396,3 +399,17 @@ Confined: +Got: Hello, World - : unit = () ``` + +Using `pipe` to create a read/write pipe. + +```ocaml +# run ~clear:[ "hello.txt"; "world.txt" ] @@ fun _ -> + Eio.Switch.run @@ fun sw -> + let r, w = L.pipe sw in + let _ : int = L.writev w [ Cstruct.of_string "Hello, world!" ] in + L.close w; + Eio.traceln "Got: %s" (Eio.Flow.read_all (Eio_mem.Flow.of_fd r));; ++Got: Hello, world! +- : unit = () +``` +