From 47b4cc269e1400612c24767f2830f59af80c56bc Mon Sep 17 00:00:00 2001 From: Anil Madhavapeddy Date: Thu, 10 Aug 2023 16:56:49 +0100 Subject: [PATCH] Experiment: use GADT for stat Co-authored-by: Thomas Leonard Co-authored-by: Patrick Ferris --- .gitignore | 1 + bench/bench_fstat.ml | 2 +- bench/bench_stat.ml | 129 ++++++++++++++++++++++ bench/main.ml | 1 + lib_eio/file.ml | 102 ++++++++---------- lib_eio/file.mli | 177 +++++++++++++++++++------------ lib_eio/fs.ml | 2 +- lib_eio/path.ml | 25 +++++ lib_eio/path.mli | 110 ++++++++++++++++++- lib_eio_linux/eio_linux.ml | 90 +++++++++++----- lib_eio_linux/eio_linux.mli | 3 - lib_eio_linux/low_level.ml | 30 ------ lib_eio_linux/tests/eurcp_lib.ml | 4 +- lib_eio_posix/flow.ml | 41 +++---- lib_eio_posix/fs.ml | 26 ++++- lib_eio_posix/low_level.ml | 2 +- lib_eio_posix/low_level.mli | 2 +- lib_eio_windows/flow.ml | 35 +++--- lib_eio_windows/fs.ml | 4 +- lib_eio_windows/test/test_fs.ml | 4 +- lib_main/tests/dune | 3 + lib_main/tests/eio_stat.ml | 46 ++++++++ tests/fs.md | 9 +- 23 files changed, 610 insertions(+), 238 deletions(-) create mode 100644 bench/bench_stat.ml create mode 100644 lib_main/tests/dune create mode 100644 lib_main/tests/eio_stat.ml diff --git a/.gitignore b/.gitignore index a4e3c50a8..b681c145f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ _build .*.swp *.install +.vscode \ No newline at end of file diff --git a/bench/bench_fstat.ml b/bench/bench_fstat.ml index ba51dea7d..986efbc29 100644 --- a/bench/bench_fstat.ml +++ b/bench/bench_fstat.ml @@ -6,7 +6,7 @@ let n_stat = 100000 let run_fiber file = for _ = 1 to n_stat do - let info = (Eio.File.stat file).kind in + let info = (Eio.File.stat file [Kind]) Fun.id in assert (info = `Regular_file) done diff --git a/bench/bench_stat.ml b/bench/bench_stat.ml new file mode 100644 index 000000000..76c42191e --- /dev/null +++ b/bench/bench_stat.ml @@ -0,0 +1,129 @@ +open Eio + +let () = Random.init 3 + +let ( / ) = Eio.Path.( / ) + +module Bench_dir = struct + type t = + | Dir of { name : string; perm : int; children : t list } + | File of { name : string; size : int64; perm : int; } + + let get_name = function Dir { name; _ } | File { name; _ } -> name + + let get_children = function + | Dir { children; _ } -> children + | File _ -> invalid_arg "Files don't have children" + + let compare a b = String.compare (get_name a) (get_name b) + + let rec sort = function + | Dir ({ children; _ } as v) -> + let c = List.map sort children in + let c = List.stable_sort compare c in + Dir { v with children = c } + | File _ as f -> f + + let rec size = function + | Dir { children; _ } -> + List.fold_left (fun acc v -> acc + size v) 0 children + | File _ -> 1 + + let rec pp ppf = function + | Dir { name; perm; children } -> + if children = [] then Fmt.pf ppf "dir %s (0o%o)" name perm else + Fmt.pf ppf "@[dir %s (0o%o)@ %a@]" name perm Fmt.(list ~sep:Fmt.cut pp) children + | File { name; size; perm } -> + Fmt.pf ppf "file %s (0o%o) %Lu" name perm size + + let rec make random fs = function + | Dir { name; perm; children } -> + let dir = fs / name in + Path.mkdir ~perm dir; + Fiber.List.iter (make random dir) children + | File { name; size; perm } -> + Path.with_open_out ~create:(`If_missing perm) (fs / name) @@ fun oc -> + let buf = Cstruct.create (Int64.to_int size) in + Flow.read_exact random buf; + Flow.copy (Flow.cstruct_source [ buf ]) oc +end + +let with_bench_dir ~random ~fs t fn = + let dir = Filename.temp_dir "eio-bench-" "-stat" in + let root = fs / dir in + Bench_dir.make random root t; + Fun.protect ~finally:(fun () -> assert (0 = Sys.command ("rm -r " ^ (Path.native_exn root)))) (fun () -> fn root) + +let rec bench_stat dir = + let kind, perm, size = + Path.stat ~follow:false dir File.[ Kind; Perm; Size ] @@ fun kind perm size -> + (kind, perm, size) + in + match kind with + | `Directory -> + let children = Path.read_dir dir |> Fiber.List.map (fun f -> bench_stat (dir / f)) in + let name = Path.native_exn dir |> Filename.basename in + Bench_dir.Dir { name; perm; children } + | `Regular_file -> + let name = Path.native_exn dir |> Filename.basename in + File { name; perm; size } + | _ -> assert false + +let file name = Bench_dir.File { name; perm = 0o644; size = 128L } +let dir name children = Bench_dir.Dir { name; perm = 0o700; children } + +let random_bench_dir ~n ~levels = + if levels < 0 then invalid_arg "Levels should be > 0"; + let rec loop root = function + | 1 -> ( + match root with + | Bench_dir.Dir d -> + let leaf_files = List.init n (fun i -> file (Fmt.str "test-file-%i-%i" 1 i)) in + Bench_dir.Dir { d with children = leaf_files } + | _ -> failwith "Root is always expected to be a directory" + ) + | level -> + match root with + | Bench_dir.Dir d -> + let files = List.init n (fun i -> file (Fmt.str "test-file-%i-%i" level i)) in + let dirs = List.init n (fun i -> dir (Fmt.str "test-dir-%i-%i" level i) []) in + let dirs = List.map (fun dir -> loop dir (level - 1)) dirs in + Bench_dir.Dir { d with children = dirs @ files } + | _ -> failwith "Root is always expected to be directory" + in + loop (dir "root" []) levels + +let run_bench ~n ~levels ~random ~fs ~clock = + let dir = random_bench_dir ~levels ~n |> Bench_dir.sort in + Eio.traceln "Going to create %i files and directories" (Bench_dir.size dir); + let bench () = + with_bench_dir ~random ~fs dir @@ fun root -> + Eio.traceln "Created %i files and directories" (Bench_dir.size dir); + Gc.full_major (); + let stat0 = Gc.stat () in + let t0 = Eio.Time.now clock in + let res = bench_stat root in + let t1 = Eio.Time.now clock in + let stat1 = Gc.stat () in + match Bench_dir.sort res with + | Dir { children = [ dir' ]; _ } -> + assert (dir = dir'); + let time_total = t1 -. t0 in + let minor_total = stat1.minor_words -. stat0.minor_words in + let major_total = stat1.major_words -. stat0.major_words in + time_total, minor_total, major_total + | _ -> failwith "Stat not the same as the spec" + in + let time, minor, major = bench () in + [ + Metric.create "stat-time" (`Float (1e3 *. time)) "ms" (Fmt.str "Time to stat %i files and directories" (Bench_dir.size dir)); + Metric.create "stat-minor" (`Float (1e-3 *. minor)) "kwords" (Fmt.str "Minor words allocated to stat %i files and directories" (Bench_dir.size dir)); + Metric.create "stat-major" (`Float (1e-3 *. major)) "kwords" (Fmt.str "Major words allocated %i files and directories" (Bench_dir.size dir)) + ] + +let run env = + let fs = Stdenv.fs env in + let random = Stdenv.secure_random env in + let clock = Stdenv.clock env in + run_bench ~n:20 ~levels:4 ~fs ~random ~clock + \ No newline at end of file diff --git a/bench/main.ml b/bench/main.ml index 3b15386a4..f54e5b96d 100644 --- a/bench/main.ml +++ b/bench/main.ml @@ -10,6 +10,7 @@ let benchmarks = [ "HTTP", Bench_http.run; "Eio_unix.Fd", Bench_fd.run; "File.stat", Bench_fstat.run; + "Path.Stat", Bench_stat.run; ] let usage_error () = diff --git a/lib_eio/file.ml b/lib_eio/file.ml index 3353a714d..274f25724 100644 --- a/lib_eio/file.ml +++ b/lib_eio/file.ml @@ -4,59 +4,44 @@ module Unix_perm = struct type t = int end -module Stat = struct - type kind = [ - | `Unknown - | `Fifo - | `Character_special - | `Directory - | `Block_device - | `Regular_file - | `Symbolic_link - | `Socket - ] - - let pp_kind ppf = function - | `Unknown -> Fmt.string ppf "unknown" - | `Fifo -> Fmt.string ppf "fifo" - | `Character_special -> Fmt.string ppf "character special file" - | `Directory -> Fmt.string ppf "directory" - | `Block_device -> Fmt.string ppf "block device" - | `Regular_file -> Fmt.string ppf "regular file" - | `Symbolic_link -> Fmt.string ppf "symbolic link" - | `Socket -> Fmt.string ppf "socket" - - type t = { - dev : Int64.t; - ino : Int64.t; - kind : kind; - perm : Unix_perm.t; - nlink : Int64.t; - uid : Int64.t; - gid : Int64.t; - rdev : Int64.t; - size : Optint.Int63.t; - atime : float; - mtime : float; - ctime : float; - } - - let pp ppf t = - Fmt.record [ - Fmt.field "dev" (fun t -> t.dev) Fmt.int64; - Fmt.field "ino" (fun t -> t.ino) Fmt.int64; - Fmt.field "kind" (fun t -> t.kind) pp_kind; - Fmt.field "perm" (fun t -> t.perm) (fun ppf i -> Fmt.pf ppf "0o%o" i); - Fmt.field "nlink" (fun t -> t.nlink) Fmt.int64; - Fmt.field "uid" (fun t -> t.uid) Fmt.int64; - Fmt.field "gid" (fun t -> t.gid) Fmt.int64; - Fmt.field "rdev" (fun t -> t.rdev) Fmt.int64; - Fmt.field "size" (fun t -> t.size) Optint.Int63.pp; - Fmt.field "atime" (fun t -> t.atime) Fmt.float; - Fmt.field "mtime" (fun t -> t.mtime) Fmt.float; - Fmt.field "ctime" (fun t -> t.ctime) Fmt.float; - ] ppf t -end +type kind = [ + | `Unknown + | `Fifo + | `Character_special + | `Directory + | `Block_device + | `Regular_file + | `Symbolic_link + | `Socket +] + +let pp_kind ppf = function + | `Unknown -> Fmt.string ppf "unknown" + | `Fifo -> Fmt.string ppf "fifo" + | `Character_special -> Fmt.string ppf "character special file" + | `Directory -> Fmt.string ppf "directory" + | `Block_device -> Fmt.string ppf "block device" + | `Regular_file -> Fmt.string ppf "regular file" + | `Symbolic_link -> Fmt.string ppf "symbolic link" + | `Socket -> Fmt.string ppf "socket" + +type 'a stat = + | Dev : int64 stat + | Ino : int64 stat + | Kind : kind stat + | Perm : int stat + | Nlink : int64 stat + | Uid : int64 stat + | Gid : int64 stat + | Rdev : int64 stat + | Size : int64 stat + | Atime : float stat + | Ctime : float stat + | Mtime : float stat + +type ('a, 'ty) stats = + | [] : ('ty, 'ty) stats + | (::) : 'a stat * ('b, 'ty) stats -> ('a -> 'b, 'ty) stats type ro_ty = [`File | Flow.source_ty | Resource.close_ty] @@ -71,7 +56,7 @@ module Pi = struct include Flow.Pi.SOURCE val pread : t -> file_offset:Optint.Int63.t -> Cstruct.t list -> int - val stat : t -> Stat.t + val stat : 'a 'b . t -> ('a, 'b) stats -> 'a -> 'b val close : t -> unit end @@ -105,7 +90,14 @@ let stat (Resource.T (t, ops)) = let module X = (val (Resource.get ops Pi.Read)) in X.stat t -let size t = (stat t).size +let kind t = stat t [Kind] Fun.id +let perm t = stat t [Perm] Fun.id +let uid t = stat t [Uid] Fun.id +let gid t = stat t [Gid] Fun.id +let size t = stat t [Size] (fun s -> Optint.Int63.of_int64 s) +let atime t = stat t [Atime] Fun.id +let ctime t = stat t [Ctime] Fun.id +let mtime t = stat t [Mtime] Fun.id let pread (Resource.T (t, ops)) ~file_offset bufs = let module X = (val (Resource.get ops Pi.Read)) in diff --git a/lib_eio/file.mli b/lib_eio/file.mli index d957bd183..1c7d21aaf 100644 --- a/lib_eio/file.mli +++ b/lib_eio/file.mli @@ -1,48 +1,44 @@ open Std -(** Tranditional Unix permissions. *) +(** Traditional Unix permissions. *) module Unix_perm : sig type t = int (** This is the same as {!Unix.file_perm}, but avoids a dependency on [Unix]. *) end (** Portable file stats. *) -module Stat : sig - - type kind = [ - | `Unknown - | `Fifo - | `Character_special - | `Directory - | `Block_device - | `Regular_file - | `Symbolic_link - | `Socket - ] - (** Kind of file from st_mode. **) - - val pp_kind : kind Fmt.t - (** Pretty printer for {! kind}. *) - - type t = { - dev : Int64.t; - ino : Int64.t; - kind : kind; - perm : Unix_perm.t; - nlink : Int64.t; - uid : Int64.t; - gid : Int64.t; - rdev : Int64.t; - size : Optint.Int63.t; - atime : float; - mtime : float; - ctime : float; - } - (** Like stat(2). *) - - val pp : t Fmt.t - (** Pretty printer for {! t}. *) -end +type kind = [ + | `Unknown + | `Fifo + | `Character_special + | `Directory + | `Block_device + | `Regular_file + | `Symbolic_link + | `Socket +] +(** Kind of file from st_mode. **) + +val pp_kind : kind Fmt.t +(** Pretty printer for {! kind}. *) + +type 'a stat = + | Dev : int64 stat + | Ino : int64 stat + | Kind : kind stat + | Perm : int stat + | Nlink : int64 stat + | Uid : int64 stat + | Gid : int64 stat + | Rdev : int64 stat + | Size : int64 stat + | Atime : float stat + | Ctime : float stat + | Mtime : float stat + +type ('a, 'ty) stats = + | [] : ('ty, 'ty) stats + | (::) : 'a stat * ('b, 'ty) stats -> ('a -> 'b, 'ty) stats type ro_ty = [`File | Flow.source_ty | Resource.close_ty] @@ -54,36 +50,62 @@ type rw_ty = [ro_ty | Flow.sink_ty] type 'a rw = ([> rw_ty] as 'a) r (** A file opened for reading and writing. *) -module Pi : sig - module type READ = sig - include Flow.Pi.SOURCE - - val pread : t -> file_offset:Optint.Int63.t -> Cstruct.t list -> int - val stat : t -> Stat.t - val close : t -> unit - end - - module type WRITE = sig - include Flow.Pi.SINK - include READ with type t := t - - val pwrite : t -> file_offset:Optint.Int63.t -> Cstruct.t list -> int - end - - type (_, _, _) Resource.pi += - | Read : ('t, (module READ with type t = 't), [> ro_ty]) Resource.pi - | Write : ('t, (module WRITE with type t = 't), [> rw_ty]) Resource.pi - - val ro : (module READ with type t = 't) -> ('t, ro_ty) Resource.handler - - val rw : (module WRITE with type t = 't) -> ('t, rw_ty) Resource.handler -end - -val stat : _ ro -> Stat.t -(** [stat t] returns the {!Stat.t} record associated with [t]. *) +val kind : _ ro -> kind +(** [kind t] returns the kind of file that [t] is. + Equivalent to [stat t [Kind] Fun.id], + so use {!val:stat} to query multiple items from a file in one call. *) val size : _ ro -> Optint.Int63.t -(** [size t] returns the size of [t]. *) +(** [size t] returns the size of [t]. Equivalent to [stat t [File.size] Fun.id], + so use {!val:stat} to query multiple items from a file in one call. *) + +val perm : _ ro -> int +(** [perm t] returns the file permissions of [t]. + Equivalent to [stat t [Perm] Fun.id], + so use {!val:stat} to query multiple items from a file in one call. *) + +val uid : _ ro -> int64 +(** [uid t] returns the user id associated with [t]. + Equivalent to [stat t [Uid] Fun.id], + so use {!val:stat} to query multiple items from a file in one call. *) + +val gid : _ ro -> int64 +(** [gid t] returns the group id associated with [t]. + Equivalent to [stat t [Gid] Fun.id], + so use {!val:stat} to query multiple items from a file in one call. *) + +val atime : _ ro -> float +(** [atime t] returns the last access time of [t] as the seconds + since the start of the epoch. + Equivalent to [stat t [Atime] Fun.id], + so use {!val:stat} to query multiple items from a file in one call. *) + +val mtime : _ ro -> float +(** [mtime t] returns the last modified time of [t] as the seconds + since the start of the epoch. + Equivalent to [stat t [Mtime] Fun.id], + so use {!val:stat} to query multiple items from a file in one call. *) + +val ctime : _ ro -> float +(** [ctime t] returns the creation time of [t] as the seconds + since the start of the epoch. + Equivalent to [stat t [Ctime] Fun.id], + so use {!val:stat} to query multiple items from a file in one call. *) + +val stat : _ ro -> ('a, 'b) stats -> 'a -> 'b +(** [stat t fields fn] will retrieve the file statistics for the specified + [fields] and apply them as arguments to [fn]. + + For example, to print the kind of a file along with its size and last + modified time: + + {[ stat t [ Kind; Size; Mtime ] + (fun kind size mtime -> + traceln "kind: %a size: %Ld mtime: %f" pp_kind size mtime) + ]} + + If you only require access to one field, consider using simpler accessor + functions in this module such as {!val:size} or {!val:kind}. *) val pread : _ ro -> file_offset:Optint.Int63.t -> Cstruct.t list -> int (** [pread t ~file_offset bufs] performs a single read of [t] at [file_offset] into [bufs]. @@ -108,3 +130,28 @@ val pwrite_single : _ rw -> file_offset:Optint.Int63.t -> Cstruct.t list -> int val pwrite_all : _ rw -> file_offset:Optint.Int63.t -> Cstruct.t list -> unit (** [pwrite_all t ~file_offset bufs] writes all the data in [bufs] to location [file_offset] in [t]. *) + +module Pi : sig + module type READ = sig + include Flow.Pi.SOURCE + + val pread : t -> file_offset:Optint.Int63.t -> Cstruct.t list -> int + val stat : 'a 'b . t -> ('a, 'b) stats -> 'a -> 'b + val close : t -> unit + end + + module type WRITE = sig + include Flow.Pi.SINK + include READ with type t := t + + val pwrite : t -> file_offset:Optint.Int63.t -> Cstruct.t list -> int + end + + type (_, _, _) Resource.pi += + | Read : ('t, (module READ with type t = 't), [> ro_ty]) Resource.pi + | Write : ('t, (module WRITE with type t = 't), [> rw_ty]) Resource.pi + + val ro : (module READ with type t = 't) -> ('t, ro_ty) Resource.handler + + val rw : (module WRITE with type t = 't) -> ('t, rw_ty) Resource.handler +end diff --git a/lib_eio/fs.ml b/lib_eio/fs.ml index ec0bbea54..c1d1d58f0 100644 --- a/lib_eio/fs.ml +++ b/lib_eio/fs.ml @@ -60,7 +60,7 @@ module Pi = struct val mkdir : t -> perm:File.Unix_perm.t -> path -> unit val open_dir : t -> sw:Switch.t -> path -> [`Close | dir_ty] r val read_dir : t -> path -> string list - val stat : t -> follow:bool -> string -> File.Stat.t + val stat : t -> follow:bool -> string -> ('a,'b) File.stats -> 'a -> 'b val unlink : t -> path -> unit val rmdir : t -> path -> unit val rename : t -> path -> _ dir -> path -> unit diff --git a/lib_eio/path.ml b/lib_eio/path.ml index 8e3afad30..f467537ca 100644 --- a/lib_eio/path.ml +++ b/lib_eio/path.ml @@ -69,6 +69,31 @@ let stat ~follow t = let bt = Printexc.get_raw_backtrace () in Exn.reraise_with_context ex bt "examining %a" pp t +let kind ~follow t = stat ~follow t [File.Kind] Fun.id +let size ~follow t = stat ~follow t [File.Size] Fun.id +let perm ~follow t = stat ~follow t [File.Perm] Fun.id +let uid ~follow t = stat ~follow t [File.Uid] Fun.id +let gid ~follow t = stat ~follow t [File.Gid] Fun.id +let atime ~follow t = stat ~follow t [File.Atime] Fun.id +let mtime ~follow t = stat ~follow t [File.Mtime] Fun.id +let ctime ~follow t = stat ~follow t [File.Ctime] Fun.id + +let exists t = + try + stat ~follow:true t [File.Kind] + (function `Directory | `Regular_file -> true | _ -> false) + with Exn.Io (Fs.E Not_found _, _) -> false + +let is_file t = + try + stat ~follow:true t [File.Kind] ((=) `Regular_file) + with Exn.Io (Fs.E Not_found _, _) -> false + +let is_directory t = + try + stat ~follow:true t [File.Kind] ((=) `Directory) + with Exn.Io (Fs.E Not_found _, _) -> false + let with_open_in path fn = Switch.run @@ fun sw -> fn (open_in ~sw path) diff --git a/lib_eio/path.mli b/lib_eio/path.mli index 8b45447cf..f15c16962 100644 --- a/lib_eio/path.mli +++ b/lib_eio/path.mli @@ -130,11 +130,115 @@ val read_dir : _ t -> string list (** {2 Metadata} *) -val stat : follow:bool -> _ t -> File.Stat.t -(** [stat ~follow t] returns metadata about the file [t]. +val exists : _ t -> bool +(** [exists t] returns [true] if [t] is a regular file or directory, and [false] + if [t] does not exist or is some other {!type:File.kind}. + Raises an exception if the path cannot be accessed due to permissions. *) + +val is_file : _ t -> bool +(** [is_file] returns [true] if [t] is a regular file, and [false] + if [t] does not exist or is some other {!type:File.kind}. + Raises an exception if the path cannot be accessed due to permissions. *) + +val is_directory : _ t -> bool +(** [is_directory] returns [true] if [t] is a directory, and false + if it does not exist or is some other {!type:File.kind}. + Raises an exception if the path cannot be accessed due to permissions. *) + +val kind : follow:bool -> _ t -> File.kind +(** [kind ~follow t] returns the kind of file that [t] is. + + If [t] is a symlink, the information returned is about the target if [follow = true], + otherwise it is about the link itself. + + Equivalent to [stat ~follow t [File.Kind] Fun.id], + so use {!stat} to query multiple items from a file in one call. *) + +val size : follow:bool -> _ t -> int64 +(** [size ~follow t] returns the size of [t]. + + If [t] is a symlink, the information returned is about the target if [follow = true], + otherwise it is about the link itself. + + Equivalent to [stat ~follow t [File.Size] Fun.id], + so use {!stat} to query multiple items from a file in one call. *) + +val perm : follow:bool -> _ t -> int +(** [perm ~follow t] returns the file permissions of [t]. + + If [t] is a symlink, the information returned is about the target if [follow = true], + otherwise it is about the link itself. + + Equivalent to [stat ~follow t [File.Perm] Fun.id], + so use {!stat} to query multiple items from a file in one call. *) + +val uid : follow:bool -> _ t -> int64 +(** [uid ~follow t] returns the user id associated with [t]. If [t] is a symlink, the information returned is about the target if [follow = true], - otherwise it is about the link itself. *) + otherwise it is about the link itself. + + Equivalent to [stat ~follow t [File.Uid] Fun.id], + so use {!stat} to query multiple items from a file in one call. *) + +val gid : follow:bool -> _ t -> int64 +(** [gid ~follow t] returns the group id associated with [t]. + + If [t] is a symlink, the information returned is about the target if [follow = true], + otherwise it is about the link itself. + + Equivalent to [stat ~follow t [File.Gid] Fun.id], + so use {!stat} to query multiple items from a file in one call. *) + +val atime : follow:bool -> _ t -> float +(** [atime ~follow t] returns the last access time of [t] as the seconds + since the start of the epoch. + + If [t] is a symlink, the information returned is about the target if [follow = true], + otherwise it is about the link itself. + + Equivalent to [stat ~follow t [File.Atime] Fun.id], + so use {!stat} to query multiple items from a file in one call. *) + +val mtime : follow:bool -> _ t -> float +(** [mtime ~follow t] returns the last modified time of [t] as the seconds + since the start of the epoch. + + If [t] is a symlink, the information returned is about the target if [follow = true], + otherwise it is about the link itself. + + Equivalent to [stat ~follow t [File.Mtime] Fun.id], + so use {!stat} to query multiple items from a file in one call. *) + +val ctime : follow:bool -> _ t -> float +(** [ctime t] returns the creation time of [t] as the seconds + since the start of the epoch. + + If [t] is a symlink, the information returned is about the target if [follow = true], + otherwise it is about the link itself. + + Equivalent to [stat ~follow t [File.Ctime] Fun.id], + so use {!stat} to query multiple items from a file in one call. *) + +val stat : follow:bool -> _ t -> ('a, 'b) File.stats -> 'a -> 'b +(** [stat ~follow t f k] returns metadata about the file [t], querying multiple + fields [f] in one call and applying them to the continuation [k]. + + If [t] is a symlink, the information returned is about the target if [follow = true], + otherwise it is about the link itself. + + For example, to follow a symlink and print the kind, size and last modified + time of its target: + + {[ stat ~follow:true t File.[ Kind; Size; Mtime ] + (fun kind size mtime -> + traceln "kind: %a size: %Ld mtime: %f" + File.pp_kind size mtime) + ]} + + If you only require access to one field, consider using simpler accessor + functions in this module such as {!val:size} or {!val:kind}. *) + (** {1 Other} *) diff --git a/lib_eio_linux/eio_linux.ml b/lib_eio_linux/eio_linux.ml index d278cbab6..af36155e0 100644 --- a/lib_eio_linux/eio_linux.ml +++ b/lib_eio_linux/eio_linux.ml @@ -136,6 +136,57 @@ let datagram_handler = Eio_unix.Pi.datagram_handler (module Datagram_socket) let datagram_socket fd = Eio.Resource.T (fd, datagram_handler) +let float_of_time s ns = + let s = Int64.to_float s in + let f = s +. (float ns /. 1e9) in + (* It's possible that we might round up to the next second. + Since some algorithms only care about the seconds part, + make sure the integer part is always [s]: *) + if floor f = s then f + else Float.pred f + +let stat_mask_of_list f = + let module M = Uring.Statx.Mask in + let module U = Eio.File in + let rec mask : type a b . (a,b) U.stats -> M.t -> M.t = fun v acc -> + match v with + | U.Dev :: tl -> mask tl acc + | U.Ino :: tl -> mask tl M.(acc + M.ino) + | U.Kind :: tl -> mask tl M.(acc + M.type') + | U.Perm :: tl -> mask tl M.(acc + M.mode) + | U.Nlink :: tl -> mask tl M.(acc + M.nlink) + | U.Uid :: tl -> mask tl M.(acc + M.uid) + | U.Gid :: tl -> mask tl M.(acc + M.gid) + | U.Rdev :: tl -> mask tl acc + | U.Size :: tl -> mask tl M.(acc + M.size) + | U.Atime :: tl -> mask tl M.(acc + M.atime) + | U.Ctime :: tl -> mask tl M.(acc + M.ctime) + | U.Mtime :: tl -> mask tl M.(acc + M.mtime) + | [] -> acc + in + mask f M.type' (* TODO this could be empty as well, but needs uring#100 *) + +let stat_results (f : (_, _) Eio.File.stats) statx k = + let module X = Uring.Statx in + let module U = Eio.File in + let rec fn : type a b. (a, b) U.stats -> a -> b = fun v acc -> + match v with + | U.Dev :: tl -> fn tl @@ acc (X.dev statx) + | U.Ino :: tl -> fn tl @@ acc (X.ino statx) + | U.Kind :: tl -> fn tl @@ acc (X.kind statx) + | U.Perm :: tl -> fn tl @@ acc (X.perm statx) + | U.Nlink :: tl -> fn tl @@ acc (X.nlink statx) + | U.Uid :: tl -> fn tl @@ acc (X.uid statx) + | U.Gid :: tl -> fn tl @@ acc (X.gid statx) + | U.Rdev :: tl -> fn tl @@ acc (X.rdev statx) + | U.Size :: tl -> fn tl @@ acc (X.size statx) + | U.Atime :: tl -> fn tl @@ acc (float_of_time (X.atime_sec statx) (X.atime_nsec statx)) + | U.Mtime :: tl -> fn tl @@ acc (float_of_time (X.mtime_sec statx) (X.mtime_nsec statx)) + | U.Ctime :: tl -> fn tl @@ acc (float_of_time (X.ctime_sec statx) (X.ctime_nsec statx)) + | [] -> acc + in + fn f k + module Flow = struct type tag = [`Generic | `Unix] @@ -147,7 +198,12 @@ module Flow = struct let is_tty t = Fd.use_exn "isatty" t Unix.isatty - let stat = Low_level.fstat + let stat t f k = + let module X = Uring.Statx in + let mask = stat_mask_of_list f in + let statx = X.create () in + Low_level.statx ~fd:t ~mask "" statx X.Flags.empty_path; + stat_results f statx k let single_read t buf = if is_tty t then ( @@ -526,33 +582,11 @@ end = struct let unlink t path = Low_level.unlink ~rmdir:false t.fd path let rmdir t path = Low_level.unlink ~rmdir:true t.fd path - let float_of_time s ns = - let s = Int64.to_float s in - let f = s +. (float ns /. 1e9) in - (* It's possible that we might round up to the next second. - Since some algorithms only care about the seconds part, - make sure the integer part is always [s]: *) - if floor f = s then f - else Float.pred f - - let stat t ~follow path = - let module X = Uring.Statx in - let x = X.create () in - Low_level.statx_confined ~follow ~mask:X.Mask.basic_stats t.fd path x; - { Eio.File.Stat. - dev = X.dev x; - ino = X.ino x; - kind = X.kind x; - perm = X.perm x; - nlink = X.nlink x; - uid = X.uid x; - gid = X.gid x; - rdev = X.rdev x; - size = X.size x |> Optint.Int63.of_int64; - atime = float_of_time (X.atime_sec x) (X.atime_nsec x); - mtime = float_of_time (X.mtime_sec x) (X.mtime_nsec x); - ctime = float_of_time (X.ctime_sec x) (X.ctime_nsec x); - } + let stat t ~follow path f k = + let mask = stat_mask_of_list f in + let statx = Uring.Statx.create () in + Low_level.statx_confined t.fd path ~follow ~mask statx; + stat_results f statx k let rename t old_path t2 new_path = match get_dir_fd_opt t2 with diff --git a/lib_eio_linux/eio_linux.mli b/lib_eio_linux/eio_linux.mli index 5c5202642..866723667 100644 --- a/lib_eio_linux/eio_linux.mli +++ b/lib_eio_linux/eio_linux.mli @@ -153,9 +153,6 @@ module Low_level : sig val await_writable : fd -> unit (** [await_writable fd] blocks until [fd] is writable (or has an error). *) - val fstat : fd -> Eio.File.Stat.t - (** Like {!Unix.LargeFile.fstat}. *) - val statx : ?fd:fd -> mask:Uring.Statx.Mask.t -> string -> Uring.Statx.t -> Uring.Statx.Flags.t -> unit (** [statx t ?fd ~mask path buf flags] stats [path], which is resolved relative to [fd] (or the current directory if [fd] is not given). diff --git a/lib_eio_linux/low_level.ml b/lib_eio_linux/low_level.ml index 613f23618..0bf56bb5a 100644 --- a/lib_eio_linux/low_level.ml +++ b/lib_eio_linux/low_level.ml @@ -303,36 +303,6 @@ let openat ~sw ?seekable ~access ~flags ~perm dir path = | Cwd -> openat2 ~sw ?seekable ~access ~flags ~perm ~resolve:Uring.Resolve.beneath path | Fs -> openat2 ~sw ?seekable ~access ~flags ~perm ~resolve:Uring.Resolve.empty path -let fstat t = - (* todo: use uring *) - try - let ust = Fd.use_exn "fstat" t Unix.LargeFile.fstat in - let st_kind : Eio.File.Stat.kind = - match ust.st_kind with - | Unix.S_REG -> `Regular_file - | Unix.S_DIR -> `Directory - | Unix.S_CHR -> `Character_special - | Unix.S_BLK -> `Block_device - | Unix.S_LNK -> `Symbolic_link - | Unix.S_FIFO -> `Fifo - | Unix.S_SOCK -> `Socket - in - Eio.File.Stat.{ - dev = ust.st_dev |> Int64.of_int; - ino = ust.st_ino |> Int64.of_int; - kind = st_kind; - perm = ust.st_perm; - nlink = ust.st_nlink |> Int64.of_int; - uid = ust.st_uid |> Int64.of_int; - gid = ust.st_gid |> Int64.of_int; - rdev = ust.st_rdev |> Int64.of_int; - size = ust.st_size |> Optint.Int63.of_int64; - atime = ust.st_atime; - mtime = ust.st_mtime; - ctime = ust.st_ctime; - } - with Unix.Unix_error (code, name, arg) -> raise @@ Err.wrap_fs code name arg - external eio_mkdirat : Unix.file_descr -> string -> Unix.file_perm -> unit = "caml_eio_mkdirat" external eio_renameat : Unix.file_descr -> string -> Unix.file_descr -> string -> unit = "caml_eio_renameat" diff --git a/lib_eio_linux/tests/eurcp_lib.ml b/lib_eio_linux/tests/eurcp_lib.ml index aba0cd739..053ad30ab 100644 --- a/lib_eio_linux/tests/eurcp_lib.ml +++ b/lib_eio_linux/tests/eurcp_lib.ml @@ -45,7 +45,9 @@ let run_cp block_size queue_depth infile outfile () = ~resolve:Uring.Resolve.empty ~perm:0o644 in - let insize = (U.fstat infd).size in + let buf = Uring.Statx.create () in + U.statx ~fd:infd "" ~mask:Uring.Statx.Mask.size buf Uring.Statx.Flags.empty_path; + let insize = Uring.Statx.size buf |> Int63.of_int64 in Logs.debug (fun l -> l "eurcp: %s -> %s size %a queue %d bs %d" infile outfile diff --git a/lib_eio_posix/flow.ml b/lib_eio_posix/flow.ml index d84c8796c..a1c210636 100644 --- a/lib_eio_posix/flow.ml +++ b/lib_eio_posix/flow.ml @@ -11,32 +11,33 @@ let float_of_time s ns = if floor f = s then f else Float.pred f -let eio_of_stat x = - { Eio.File.Stat. - dev = Low_level.dev x; - ino = Low_level.ino x; - kind = Low_level.kind x; - perm = Low_level.perm x; - nlink = Low_level.nlink x; - uid = Low_level.uid x; - gid = Low_level.gid x; - rdev = Low_level.rdev x; - size = Low_level.size x |> Optint.Int63.of_int64; - atime = float_of_time (Low_level.atime_sec x) (Low_level.atime_nsec x); - mtime = float_of_time (Low_level.mtime_sec x) (Low_level.mtime_nsec x); - ctime = float_of_time (Low_level.ctime_sec x) (Low_level.ctime_nsec x); - } - module Impl = struct type tag = [`Generic | `Unix] type t = Eio_unix.Fd.t - let stat t = + let stat t k = + let open Eio.File in + let x = Low_level.create_stat () in + Low_level.fstat ~buf:x t; try - let x = Low_level.create_stat () in - Low_level.fstat ~buf:x t; - eio_of_stat x + let rec fn : type a b. (a, b) stats -> a -> b = fun v acc -> + match v with + | Dev :: tl -> fn tl @@ acc (Low_level.dev x) + | Ino :: tl -> fn tl @@ acc (Low_level.ino x) + | Kind :: tl -> fn tl @@ acc (Low_level.kind x) + | Perm :: tl -> fn tl @@ acc (Low_level.perm x) + | Nlink :: tl -> fn tl @@ acc (Low_level.nlink x) + | Uid :: tl -> fn tl @@ acc (Low_level.uid x) + | Gid :: tl -> fn tl @@ acc (Low_level.gid x) + | Rdev :: tl -> fn tl @@ acc (Low_level.rdev x) + | Size :: tl -> fn tl @@ acc (Low_level.size x) + | Atime :: tl -> fn tl @@ acc (float_of_time (Low_level.atime_sec x) (Low_level.atime_nsec x)) + | Mtime :: tl -> fn tl @@ acc (float_of_time (Low_level.mtime_sec x) (Low_level.mtime_nsec x)) + | Ctime :: tl -> fn tl @@ acc (float_of_time (Low_level.ctime_sec x) (Low_level.ctime_nsec x)) + | [] -> acc + in + fn k with Unix.Unix_error (code, name, arg) -> raise @@ Err.wrap code name arg let single_write t bufs = diff --git a/lib_eio_posix/fs.ml b/lib_eio_posix/fs.ml index 9e5499216..50b82bb48 100644 --- a/lib_eio_posix/fs.ml +++ b/lib_eio_posix/fs.ml @@ -137,15 +137,31 @@ end = struct with_parent_dir t path @@ fun dirfd path -> Err.run (Low_level.unlink ?dirfd ~dir:true) path - let stat t ~follow path = - let buf = Low_level.create_stat () in + let stat t ~follow path k = + let r = Low_level.create_stat () in if follow then ( - Err.run (Low_level.fstatat ~buf ~follow:true) (resolve t path); + Err.run (Low_level.fstatat ~buf:r ~follow:true) (resolve t path); ) else ( with_parent_dir t path @@ fun dirfd path -> - Err.run (Low_level.fstatat ~buf ?dirfd ~follow:false) path; + Err.run (Low_level.fstatat ~buf:r ?dirfd ~follow:false) path; ); - Flow.eio_of_stat buf + let open Eio.File in + let rec fn : type a b. (a, b) stats -> a -> b = fun v acc -> + match v with + | Dev :: tl -> fn tl @@ acc (Low_level.dev r) + | Ino :: tl -> fn tl @@ acc (Low_level.ino r) + | Kind :: tl -> fn tl @@ acc (Low_level.kind r) + | Perm :: tl -> fn tl @@ acc (Low_level.perm r) + | Nlink :: tl -> fn tl @@ acc (Low_level.nlink r) + | Uid :: tl -> fn tl @@ acc (Low_level.uid r) + | Gid :: tl -> fn tl @@ acc (Low_level.gid r) + | Rdev :: tl -> fn tl @@ acc (Low_level.rdev r) + | Size :: tl -> fn tl @@ acc (Low_level.size r) + | Atime :: tl -> fn tl @@ acc (Flow.float_of_time (Low_level.atime_sec r) (Low_level.atime_nsec r)) + | Mtime :: tl -> fn tl @@ acc (Flow.float_of_time (Low_level.mtime_sec r) (Low_level.mtime_nsec r)) + | Ctime :: tl -> fn tl @@ acc (Flow.float_of_time (Low_level.ctime_sec r) (Low_level.ctime_nsec r)) + | [] -> acc + in fn k let read_dir t path = (* todo: need fdopendir here to avoid races *) diff --git a/lib_eio_posix/low_level.ml b/lib_eio_posix/low_level.ml index 4138cabb9..b8eacd526 100644 --- a/lib_eio_posix/low_level.ml +++ b/lib_eio_posix/low_level.ml @@ -246,7 +246,7 @@ external rdev : stat -> (int64 [@unboxed]) = "ocaml_eio_posix_stat_rdev_bytes external dev : stat -> (int64 [@unboxed]) = "ocaml_eio_posix_stat_dev_bytes" "ocaml_eio_posix_stat_dev_native" [@@noalloc] external perm : stat -> (int [@untagged]) = "ocaml_eio_posix_stat_perm_bytes" "ocaml_eio_posix_stat_perm_native" [@@noalloc] external mode : stat -> (int [@untagged]) = "ocaml_eio_posix_stat_mode_bytes" "ocaml_eio_posix_stat_mode_native" [@@noalloc] -external kind : stat -> Eio.File.Stat.kind = "ocaml_eio_posix_stat_kind" +external kind : stat -> Eio.File.kind = "ocaml_eio_posix_stat_kind" external atime_sec : stat -> (int64 [@unboxed]) = "ocaml_eio_posix_stat_atime_sec_bytes" "ocaml_eio_posix_stat_atime_sec_native" [@@noalloc] external ctime_sec : stat -> (int64 [@unboxed]) = "ocaml_eio_posix_stat_ctime_sec_bytes" "ocaml_eio_posix_stat_ctime_sec_native" [@@noalloc] diff --git a/lib_eio_posix/low_level.mli b/lib_eio_posix/low_level.mli index b93acf425..c868374ab 100644 --- a/lib_eio_posix/low_level.mli +++ b/lib_eio_posix/low_level.mli @@ -52,7 +52,7 @@ external rdev : stat -> (int64 [@unboxed]) = "ocaml_eio_posix_stat_rdev_bytes external dev : stat -> (int64 [@unboxed]) = "ocaml_eio_posix_stat_dev_bytes" "ocaml_eio_posix_stat_dev_native" [@@noalloc] external perm : stat -> (int [@untagged]) = "ocaml_eio_posix_stat_perm_bytes" "ocaml_eio_posix_stat_perm_native" [@@noalloc] external mode : stat -> (int [@untagged]) = "ocaml_eio_posix_stat_mode_bytes" "ocaml_eio_posix_stat_mode_native" [@@noalloc] -external kind : stat -> Eio.File.Stat.kind = "ocaml_eio_posix_stat_kind" +external kind : stat -> Eio.File.kind = "ocaml_eio_posix_stat_kind" external atime_sec : stat -> (int64 [@unboxed]) = "ocaml_eio_posix_stat_atime_sec_bytes" "ocaml_eio_posix_stat_atime_sec_native" [@@noalloc] external ctime_sec : stat -> (int64 [@unboxed]) = "ocaml_eio_posix_stat_ctime_sec_bytes" "ocaml_eio_posix_stat_ctime_sec_native" [@@noalloc] diff --git a/lib_eio_windows/flow.ml b/lib_eio_windows/flow.ml index b56580342..4ed029d55 100755 --- a/lib_eio_windows/flow.ml +++ b/lib_eio_windows/flow.ml @@ -7,10 +7,10 @@ module Impl = struct type t = Eio_unix.Fd.t - let stat t = + let stat t k = try let ust = Low_level.fstat t in - let st_kind : Eio.File.Stat.kind = + let st_kind : Eio.File.kind = match ust.st_kind with | Unix.S_REG -> `Regular_file | Unix.S_DIR -> `Directory @@ -20,20 +20,23 @@ module Impl = struct | Unix.S_FIFO -> `Fifo | Unix.S_SOCK -> `Socket in - Eio.File.Stat.{ - dev = ust.st_dev |> Int64.of_int; - ino = ust.st_ino |> Int64.of_int; - kind = st_kind; - perm = ust.st_perm; - nlink = ust.st_nlink |> Int64.of_int; - uid = ust.st_uid |> Int64.of_int; - gid = ust.st_gid |> Int64.of_int; - rdev = ust.st_rdev |> Int64.of_int; - size = ust.st_size |> Optint.Int63.of_int64; - atime = ust.st_atime; - mtime = ust.st_mtime; - ctime = ust.st_ctime; - } + let rec fn : type a b. (a, b) Eio.File.stats -> a -> b = fun v acc -> + match v with + | Dev :: tl -> fn tl @@ acc (ust.st_dev |> Int64.of_int) + | Ino :: tl -> fn tl @@ acc (ust.st_ino |> Int64.of_int) + | Kind :: tl -> fn tl @@ acc st_kind + | Perm :: tl -> fn tl @@ acc ust.st_perm + | Nlink :: tl -> fn tl @@ acc (ust.st_nlink |> Int64.of_int) + | Uid :: tl -> fn tl @@ acc (ust.st_uid |> Int64.of_int) + | Gid :: tl -> fn tl @@ acc (ust.st_gid |> Int64.of_int) + | Rdev :: tl -> fn tl @@ acc (ust.st_rdev |> Int64.of_int) + | Size :: tl -> fn tl @@ acc ust.st_size + | Atime :: tl -> fn tl @@ acc ust.st_atime + | Mtime :: tl -> fn tl @@ acc ust.st_mtime + | Ctime :: tl -> fn tl @@ acc ust.st_ctime + | [] -> acc + in + fn k with Unix.Unix_error (code, name, arg) -> raise @@ Err.wrap code name arg let write_all t bufs = diff --git a/lib_eio_windows/fs.ml b/lib_eio_windows/fs.ml index 4095f5971..3bf5ff820 100755 --- a/lib_eio_windows/fs.ml +++ b/lib_eio_windows/fs.ml @@ -145,14 +145,14 @@ end = struct with_parent_dir t path @@ fun dirfd path -> Err.run (Low_level.unlink ?dirfd ~dir:true) path - let stat t ~follow path = + let stat t ~follow path k = Switch.run @@ fun sw -> let open Low_level in let flags = Low_level.Flags.Open.(generic_read + synchronise) in let dis = Flags.Disposition.open_if in let create = Flags.Create.non_directory in let fd = Err.run (openat ~sw ~nofollow:(not follow) (resolve t path) flags dis) create in - Flow.Impl.stat fd + Flow.Impl.stat fd k let read_dir t path = (* todo: need fdopendir here to avoid races *) diff --git a/lib_eio_windows/test/test_fs.ml b/lib_eio_windows/test/test_fs.ml index 553a55b7f..b09407833 100755 --- a/lib_eio_windows/test/test_fs.ml +++ b/lib_eio_windows/test/test_fs.ml @@ -52,7 +52,7 @@ let chdir path = let assert_kind path kind = Path.with_open_in path @@ fun file -> - assert ((Eio.File.stat file).kind = kind) + assert (Eio.File.kind file = kind) let test_create_and_read env () = let cwd = Eio.Stdenv.cwd env in @@ -259,4 +259,4 @@ let tests env = [ "unlink", `Quick, test_unlink env; "failing-unlink", `Quick, try_failing_unlink env; "rmdir", `Quick, test_remove_dir env; -] \ No newline at end of file +] diff --git a/lib_main/tests/dune b/lib_main/tests/dune new file mode 100644 index 000000000..6f56b6660 --- /dev/null +++ b/lib_main/tests/dune @@ -0,0 +1,3 @@ +(executable + (name eio_stat) + (libraries eio_main)) diff --git a/lib_main/tests/eio_stat.ml b/lib_main/tests/eio_stat.ml new file mode 100644 index 000000000..d333490fc --- /dev/null +++ b/lib_main/tests/eio_stat.ml @@ -0,0 +1,46 @@ +(* stat(1) using eio *) + +let pp_time f t = + let tm = Unix.localtime t in + Format.fprintf f "%04d-%02d-%02d %02d:%02d:%02d +0000" + (tm.Unix.tm_year + 1900) (tm.Unix.tm_mon + 1) tm.Unix.tm_mday + tm.Unix.tm_hour tm.Unix.tm_min tm.Unix.tm_sec + +let pp_perm_hum ppf v = + let rwx i = ((i land 4) = 4), ((i land 2) = 2), (i land 1 = 1) in + let pp_rwx ppf (r, w, x) = + let is_set c v = if v then c else "-" in + Format.fprintf ppf "%s%s%s" (is_set "r" r) (is_set "w" w) (is_set "x" x); + in + Format.fprintf ppf "%a%a%a" pp_rwx (rwx (v lsr 6)) pp_rwx (rwx (v lsr 3)) pp_rwx (rwx v) + +let run_stat fs fname = + let open Eio in + let path = Path.(fs / fname) in + let opt_symlink = function + | `Symbolic_link -> " -> "^(Unix.readlink fname) (* TODO need an eio readlink *) + | _ -> "" in + Path.stat ~follow:false path File.[Kind; Size; Dev; Ino; Nlink; Perm; Uid; Gid; Atime; Mtime; Ctime] + (fun kind size dev ino nlink perm uid gid atime mtime ctime -> + Format.printf " File: %s%s\n Size: %Lu\t\tFileType: %a\nDevice: %Lu\tInode: %Lu\tLinks: %Lu\nMode: (%04o/-%a)\tUid: (%Lu/TODO)\tGid: (%Lu/TODO)\nAccess: %a\nModify: %a\nChange: %a\n%!" + fname (opt_symlink kind) + size + Eio.File.pp_kind kind + dev + ino + nlink + perm + pp_perm_hum + perm + uid + gid + pp_time atime + pp_time mtime + pp_time ctime + ) + +let () = + Eio_main.run @@ fun env -> + (* TODO cmdliner *) + let fname = Sys.argv.(1) in + run_stat (Eio.Stdenv.fs env) fname diff --git a/tests/fs.md b/tests/fs.md index c15148e3d..f908e6b51 100644 --- a/tests/fs.md +++ b/tests/fs.md @@ -62,7 +62,7 @@ let chdir path = let assert_kind path kind = Path.with_open_in path @@ fun file -> - assert ((Eio.File.stat file).kind = kind) + assert (Eio.File.kind file = kind) ``` # Basic test cases @@ -524,9 +524,10 @@ Unconfined: ```ocaml let try_stat path = let stat ~follow = - match Eio.Path.stat ~follow path with - | info -> Fmt.str "@[%a@]" Eio.File.Stat.pp_kind info.kind - | exception Eio.Io (e, _) -> Fmt.str "@[%a@]" Eio.Exn.pp_err e + try + Eio.Path.stat ~follow path [Kind] @@ fun kind -> + Fmt.str "@[%a@]" Eio.File.pp_kind kind + with Eio.Io (e, _) -> Fmt.str "@[%a@]" Eio.Exn.pp_err e in let a = stat ~follow:false in let b = stat ~follow:true in