diff --git a/README.md b/README.md index 065ff04bc..c49ff8ce5 100644 --- a/README.md +++ b/README.md @@ -33,6 +33,7 @@ Eio replaces existing concurrency libraries such as Lwt * [Buffered Writing](#buffered-writing) * [Error Handling](#error-handling) * [Filesystem Access](#filesystem-access) +* [Subprocesses](#subprocesses) * [Time](#time) * [Multicore Support](#multicore-support) * [Synchronisation Tools](#synchronisation-tools) @@ -891,6 +892,44 @@ Note: the `eio_luv` backend doesn't have the `openat`, `mkdirat`, etc., calls that are necessary to implement these checks without races, so be careful if symlinks out of the subtree may be created while the program is running. +## Subprocesses + +Spawning subprocesses is provided by the [Eio.Process][] module. [Eio_unix][] contains a helper function +for finding the absolute path to programs. + + +```ocaml +# Eio_main.run @@ fun env -> + let proc_mgr = Eio.Stdenv.process_mgr env in + let stdin, stdout, stderr = Eio.Stdenv.stdio env in + let echo = + Option.get @@ Eio_unix.resolve_program ~paths:[ "/usr/bin"; "/bin" ] "echo" + in + Eio.Switch.run @@ fun sw -> + let child = Eio.Process.spawn proc_mgr ~sw ~stdin ~stdout ~stderr echo [ "echo"; "hello" ] in + Eio.Process.await child;; +hello +- : Eio.Process.status = Eio.Process.Exited 0 +``` + +If you want to capture the output of a process, you can provide a suitable `Eio.Flow.sink` as the `stdout` argument. + +```ocaml +# Eio_main.run @@ fun env -> + let proc_mgr = Eio.Stdenv.process_mgr env in + let buffer = Buffer.create 4 in + let stdin, _, stderr = Eio.Stdenv.stdio env in + let stdout = Eio.Flow.buffer_sink buffer in + let echo = + Option.get @@ Eio_unix.resolve_program ~paths:[ "/usr/bin"; "/bin" ] "echo" + in + Eio.Switch.run @@ fun sw -> + let child = Eio.Process.spawn proc_mgr ~sw ~stdin ~stdout ~stderr echo [ "echo"; "hello" ] in + Eio.Process.await_exn child; + Buffer.contents buffer;; +- : string = "hello\n" +``` + ## Time The standard environment provides a [clock][Eio.Time] with the usual POSIX time: @@ -1743,3 +1782,4 @@ Some background about the effects system can be found in: [Eio.Mutex]: https://ocaml-multicore.github.io/eio/eio/Eio/Mutex/index.html [Eio.Semaphore]: https://ocaml-multicore.github.io/eio/eio/Eio/Semaphore/index.html [Eio.Condition]: https://ocaml-multicore.github.io/eio/eio/Eio/Condition/index.html +[Eio.Process]: https://ocaml-multicore.github.io/eio/eio/Eio/Process/index.html diff --git a/doc/prelude.ml b/doc/prelude.ml index 602d196de..7f39e3d38 100644 --- a/doc/prelude.ml +++ b/doc/prelude.ml @@ -32,12 +32,14 @@ module Eio_main = struct let run fn = Eio_main.run @@ fun env -> fn @@ object - method net = env#net - method stdin = env#stdin - method stdout = env#stdout - method cwd = env#cwd - method domain_mgr = fake_domain_mgr - method clock = fake_clock env#clock + method net = env#net + method stdin = env#stdin + method stdout = env#stdout + method stderr = env#stderr + method cwd = env#cwd + method process_mgr = env#process_mgr + method domain_mgr = fake_domain_mgr + method clock = fake_clock env#clock end end diff --git a/lib_eio/eio.ml b/lib_eio/eio.ml index 829506c13..122c8a394 100644 --- a/lib_eio/eio.ml +++ b/lib_eio/eio.ml @@ -23,6 +23,7 @@ module Flow = Flow module Buf_read = Buf_read module Buf_write = Buf_write module Net = Net +module Process = Process module Domain_manager = Domain_manager module Time = Time module File = File @@ -35,6 +36,7 @@ module Stdenv = struct stdout : Flow.sink; stderr : Flow.sink; net : Net.t; + process_mgr : Process.mgr; domain_mgr : Domain_manager.t; clock : Time.clock; mono_clock : Time.Mono.t; @@ -47,7 +49,9 @@ module Stdenv = struct let stdin (t : ) = t#stdin let stdout (t : ) = t#stdout let stderr (t : ) = t#stderr + let stdio (t : ) = t#stdin, t#stdout, t#stderr let net (t : ) = t#net + let process_mgr (t : ) = t#process_mgr let domain_mgr (t : ) = t#domain_mgr let clock (t : ) = t#clock let mono_clock (t : ) = t#mono_clock diff --git a/lib_eio/eio.mli b/lib_eio/eio.mli index 6557e2956..7ad7b7d84 100644 --- a/lib_eio/eio.mli +++ b/lib_eio/eio.mli @@ -84,6 +84,9 @@ module Buf_write = Buf_write (** Networking. *) module Net = Net +(** Subprocesses. *) +module Process = Process + (** Parallel computation across multiple CPU cores. *) module Domain_manager : sig class virtual t : object @@ -180,6 +183,7 @@ module Stdenv : sig stdout : Flow.sink; stderr : Flow.sink; net : Net.t; + process_mgr : Process.mgr; domain_mgr : Domain_manager.t; clock : Time.clock; mono_clock : Time.Mono.t; @@ -197,6 +201,9 @@ module Stdenv : sig val stdout : -> 'a val stderr : -> 'a + val stdio : -> 'a * 'b * 'c + (** [stdio t] returns [stdin, stdout, stderr]. *) + (** {1 File-system access} To use these, see {!Path}. *) @@ -222,6 +229,14 @@ module Stdenv : sig val net : -> 'a (** [net t] gives access to the process's network namespace. *) + (** {1 Processes } + + To use this, see {!Process}. + *) + + val process_mgr : -> 'a + (** [process_mgr t] allows you to run subprocesses. *) + (** {1 Domains (using multiple CPU cores)} To use this, see {!Domain_manager}. diff --git a/lib_eio/process.ml b/lib_eio/process.ml new file mode 100644 index 000000000..e794f39c3 --- /dev/null +++ b/lib_eio/process.ml @@ -0,0 +1,53 @@ +type status = Exited of int | Signaled of int | Stopped of int + +let pp_status ppf = function + | Exited i -> Format.fprintf ppf "Exited %i" i + | Signaled i -> Format.fprintf ppf "Signalled %i" i + | Stopped i -> Format.fprintf ppf "Stopped %i" i + +type Exn.err += E of status + +let err e = Exn.create (E e) + +let () = + Exn.register_pp (fun f -> function + | E e -> + Fmt.string f "Process "; + pp_status f e; + true + | _ -> false + ) + +class virtual t = object + method virtual pid : int + method virtual await : status + method virtual signal : int -> unit +end + +let pid proc = proc#pid +let await proc = proc#await + +let await_exn proc = + match proc#await with + | Exited 0 -> () + | status -> raise (err status) + +let signal proc = proc#signal + +class virtual mgr = object + method virtual spawn : + sw:Switch.t -> + ?cwd:Fs.dir Path.t -> + stdin:Flow.source -> + stdout:Flow.sink -> + stderr:Flow.sink -> + string -> + string list -> + t +end + +let spawn ~sw (t:#mgr) ?cwd ~stdin ~stdout ~stderr cmd args = + t#spawn ~sw ?cwd cmd args + ~stdin:(stdin :> Flow.source) + ~stdout:(stdout :> Flow.sink) + ~stderr:(stderr :> Flow.sink) \ No newline at end of file diff --git a/lib_eio/process.mli b/lib_eio/process.mli new file mode 100644 index 000000000..6440ce1dd --- /dev/null +++ b/lib_eio/process.mli @@ -0,0 +1,46 @@ +type status = Exited of int | Signaled of int | Stopped of int + +val pp_status : Format.formatter -> status -> unit + +type Exn.err += E of status + +val err : status -> exn +(** [err e] is [Eio.Exn.create (E e)] *) + +class virtual t : object + method virtual pid : int + method virtual await : status + method virtual signal : int -> unit +end + +val pid : #t -> int +(** The process ID *) + +val await : #t -> status +(** This functions waits for the subprocess to exit and then reports the status. *) + +val await_exn : #t -> unit +(** Like {! await} except an exception is raised if the status is not [Exited 0]. *) + +val signal : #t -> int -> unit +(** [signal t i] sends the signal [i] to process [t]. *) + +class virtual mgr : object + method virtual spawn : + sw:Switch.t -> + ?cwd:Fs.dir Path.t -> + stdin:Flow.source -> + stdout:Flow.sink -> + stderr:Flow.sink -> + string -> + string list -> + t +end +(** A process manager capable of spawning new processes. *) + +val spawn : sw:Switch.t -> #mgr -> ?cwd:Fs.dir Path.t -> stdin:#Flow.source -> stdout:#Flow.sink -> stderr:#Flow.sink -> string -> string list -> t +(** [spawn ~sw mgr ?cwd ~stdin ~stdout ~stderr cmd args] creates a new subprocess that is connected to the + switch [sw]. A process will be sent {! Sys.sigkill} when the switch is released. + + You must provide a standard input and outputs that are backed by file descriptors and + [cwd] will optionally change the current working directory of the process.*) diff --git a/lib_eio/unix/eio_unix.ml b/lib_eio/unix/eio_unix.ml index a078505e3..fb7e45159 100644 --- a/lib_eio/unix/eio_unix.ml +++ b/lib_eio/unix/eio_unix.ml @@ -18,7 +18,7 @@ type socket = < module Private = struct type _ Eio.Generic.ty += Unix_file_descr : [`Peek | `Take] -> Unix.file_descr Eio.Generic.ty - type _ Effect.t += + type _ Effect.t += | Await_readable : Unix.file_descr -> unit Effect.t | Await_writable : Unix.file_descr -> unit Effect.t | Get_monotonic_clock : Eio.Time.Mono.t Effect.t @@ -79,3 +79,16 @@ let getnameinfo (sockaddr : Eio.Net.Sockaddr.t) = run_in_systhread (fun () -> let Unix.{ni_hostname; ni_service} = Unix.getnameinfo sockaddr options in (ni_hostname, ni_service)) + +let resolve_program ~paths prog = + if not (Filename.is_relative prog) then begin + if Sys.file_exists prog then Some prog else None + end + else + let rec loop = function + | path :: rest -> + let p = Filename.concat path prog in + if Sys.file_exists p then Some p else loop rest + | [] -> None + in + loop paths \ No newline at end of file diff --git a/lib_eio/unix/eio_unix.mli b/lib_eio/unix/eio_unix.mli index 491d33549..97c2e149b 100644 --- a/lib_eio/unix/eio_unix.mli +++ b/lib_eio/unix/eio_unix.mli @@ -90,7 +90,7 @@ module Private : sig type _ Eio.Generic.ty += Unix_file_descr : [`Peek | `Take] -> Unix.file_descr Eio.Generic.ty (** See {!FD}. *) - type _ Effect.t += + type _ Effect.t += | Await_readable : Unix.file_descr -> unit Effect.t (** See {!await_readable} *) | Await_writable : Unix.file_descr -> unit Effect.t (** See {!await_writable} *) | Get_monotonic_clock : Eio.Time.Mono.t Effect.t @@ -98,7 +98,7 @@ module Private : sig socket Effect.t (** See {!FD.as_socket} *) | Socketpair : Eio.Switch.t * Unix.socket_domain * Unix.socket_type * int -> (socket * socket) Effect.t (** See {!socketpair} *) - | Pipe : Eio.Switch.t -> + | Pipe : Eio.Switch.t -> ( * ) Effect.t (** See {!pipe} *) module Rcfd = Rcfd @@ -110,3 +110,7 @@ module Ctf = Ctf_unix val getnameinfo : Eio.Net.Sockaddr.t -> (string * string) (** [getnameinfo sockaddr] returns domain name and service for [sockaddr]. *) + +val resolve_program : paths:string list -> string -> string option +(** [resolve_program ~paths prog] tries to resolve the absolute path for [prog] + by looking in each of [paths]. *) \ No newline at end of file diff --git a/lib_eio/unix/fork_action.c b/lib_eio/unix/fork_action.c index 2c429862d..1aa96cbc8 100644 --- a/lib_eio/unix/fork_action.c +++ b/lib_eio/unix/fork_action.c @@ -4,7 +4,9 @@ #include #include +#include #include +#include #include "fork_action.h" @@ -39,6 +41,20 @@ void eio_unix_fork_error(int fd, char *fn, char *buf) { try_write_all(fd, buf); } +CAMLprim value eio_unix_spawn(value v_errors, value v_actions) { + CAMLparam1(v_actions); + pid_t child_pid; + + child_pid = fork(); + if (child_pid == 0) { + eio_unix_run_fork_actions(Int_val(v_errors), v_actions); + } else if (child_pid < 0) { + uerror("fork", Nothing); + } + + CAMLreturn(Val_long(child_pid)); +} + static char **make_string_array(int errors, value v_array) { int n = Wosize_val(v_array); char **c = calloc(sizeof(char *), (n + 1)); diff --git a/lib_eio/unix/fork_action.ml b/lib_eio/unix/fork_action.ml index 659410bad..1b87b78ba 100644 --- a/lib_eio/unix/fork_action.ml +++ b/lib_eio/unix/fork_action.ml @@ -20,6 +20,12 @@ let rec with_actions actions fn = let err_closed op () = Fmt.failwith "%s: FD is closed!" op +external eio_spawn : Unix.file_descr -> c_action list -> int = "eio_unix_spawn" + +let spawn errors actions = + Rcfd.use ~if_closed:(err_closed "spawn") errors @@ fun errors -> + eio_spawn errors actions + external action_execve : unit -> fork_fn = "eio_unix_fork_execve" let action_execve = action_execve () let execve path ~argv ~env = { run = fun k -> k (Obj.repr (action_execve, path, argv, env)) } diff --git a/lib_eio/unix/fork_action.mli b/lib_eio/unix/fork_action.mli index 7699b5b45..29022939b 100644 --- a/lib_eio/unix/fork_action.mli +++ b/lib_eio/unix/fork_action.mli @@ -28,6 +28,12 @@ type t = { run : 'a. ((c_action -> 'a) -> 'a) } [@@unboxed] val with_actions : t list -> (c_action list -> 'a) -> 'a +val spawn : Rcfd.t -> c_action list -> int +(** [spawn errors actions] forks a child process and executes [actions] in it. + + If an error occurs, a message is written to [errors]. + Returns the PID of the child process. *) + (** {2 Actions} *) val execve : string -> argv:string array -> env:string array -> t diff --git a/lib_eio_linux/eio_linux.ml b/lib_eio_linux/eio_linux.ml index 4414d1bf3..61159897b 100644 --- a/lib_eio_linux/eio_linux.ml +++ b/lib_eio_linux/eio_linux.ml @@ -286,11 +286,88 @@ let net = object method getnameinfo = Eio_unix.getnameinfo end +module Process = Low_level.Process + +let unix_status_to_stats = function + | Unix.WEXITED i -> Eio.Process.Exited i + | Unix.WSIGNALED i -> Eio.Process.Signaled i + | Unix.WSTOPPED i -> Eio.Process.Stopped i + +let process proc : Eio.Process.t = object + method pid = Process.pid proc + method await = + let status = Eio.Promise.await @@ Process.exit_status proc in + unix_status_to_stats status + method signal i = Process.signal proc i +end + +let read_of_fd ~sw t = + match get_fd_opt t with + | None -> + let r, w = Low_level.pipe ~sw in + Some (flow w), r + | Some fd -> None, fd + +let write_of_fd ~sw t = + match get_fd_opt t with + | None -> + let r, w = Low_level.pipe ~sw in + Some (flow r), w + | Some fd -> None, fd + +let process_mgr = object + inherit Eio.Process.mgr + + method spawn ~sw ?cwd ~(stdin : #Eio.Flow.source) ~(stdout : #Eio.Flow.sink) ~(stderr : #Eio.Flow.sink) prog args = + let stdin_w, stdin_fd = read_of_fd ~sw stdin in + let stdout_r, stdout_fd = write_of_fd ~sw stdout in + let stderr_r, stderr_fd = write_of_fd ~sw stderr in + let actions = Process.Fork_action.[ + Eio_unix.Private.Fork_action.inherit_fds [ + 0, Fd.to_rcfd stdin_fd, `Blocking; + 1, Fd.to_rcfd stdout_fd, `Blocking; + 2, Fd.to_rcfd stderr_fd, `Blocking; + ]; + execve prog ~argv:(Array.of_list args) ~env:[||] + ] in + let with_actions cwd fn = match cwd with + | Some (fd, s) -> ( + match get_dir_fd_opt fd with + | None -> fn actions + | Some dir_fd -> + let root = + Low_level.openat ~sw + ~seekable:false + ~access:`R + ~perm:0 + ~flags:Uring.Open_flags.(cloexec + path + directory) + dir_fd s + in + fn (Process.Fork_action.fchdir root :: actions) + ) + | None -> fn actions + in + with_actions cwd @@ fun actions -> + let proc = process (Process.spawn ~sw actions) in + Option.iter (fun stdin_w -> + Eio.Flow.copy stdin stdin_w; + Eio.Flow.close stdin_w + ) stdin_w; + Option.iter (fun stdout_r -> + Fd.close stdout_fd; + Eio.Flow.copy stdout_r stdout) stdout_r; + Option.iter (fun stderr_r -> + Fd.close stderr_fd; + Eio.Flow.copy stderr_r stdout) stderr_r; + proc +end + type stdenv = < stdin : source; stdout : sink; stderr : sink; net : Eio.Net.t; + process_mgr : Eio.Process.mgr; domain_mgr : Eio.Domain_manager.t; clock : Eio.Time.clock; mono_clock : Eio.Time.Mono.t; @@ -427,6 +504,7 @@ let stdenv ~run_event_loop = method stdout = Lazy.force stdout method stderr = Lazy.force stderr method net = net + method process_mgr = process_mgr method domain_mgr = domain_mgr ~run_event_loop method clock = clock method mono_clock = mono_clock diff --git a/lib_eio_linux/eio_linux.mli b/lib_eio_linux/eio_linux.mli index dcfbd95cd..a6829e0c9 100644 --- a/lib_eio_linux/eio_linux.mli +++ b/lib_eio_linux/eio_linux.mli @@ -63,6 +63,7 @@ type stdenv = < stdout : sink; stderr : sink; net : Eio.Net.t; + process_mgr : Eio.Process.mgr; domain_mgr : Eio.Domain_manager.t; clock : Eio.Time.clock; mono_clock : Eio.Time.Mono.t; diff --git a/lib_eio_luv/eio_luv.ml b/lib_eio_luv/eio_luv.ml index 61de3dd68..a6ab3f71d 100644 --- a/lib_eio_luv/eio_luv.ml +++ b/lib_eio_luv/eio_luv.ml @@ -959,6 +959,7 @@ type stdenv = < stdout : sink; stderr : sink; net : Eio.Net.t; + process_mgr : Eio.Process.mgr; domain_mgr : Eio.Domain_manager.t; clock : Eio.Time.clock; mono_clock : Eio.Time.Mono.t; @@ -1160,6 +1161,7 @@ let stdenv ~run_event_loop = method stdout = Lazy.force stdout method stderr = Lazy.force stderr method net = net + method process_mgr = failwith "Processes are not supported using libuv" method domain_mgr = domain_mgr ~run_event_loop method clock = clock method mono_clock = mono_clock diff --git a/lib_eio_luv/eio_luv.mli b/lib_eio_luv/eio_luv.mli index 9de90c30a..6cee71a99 100644 --- a/lib_eio_luv/eio_luv.mli +++ b/lib_eio_luv/eio_luv.mli @@ -210,6 +210,7 @@ type stdenv = < stdout : sink; stderr : sink; net : Eio.Net.t; + process_mgr : Eio.Process.mgr; domain_mgr : Eio.Domain_manager.t; clock : Eio.Time.clock; mono_clock : Eio.Time.Mono.t; diff --git a/lib_eio_luv/tests/dune b/lib_eio_luv/tests/dune index 0e1ed9770..1168d0303 100644 --- a/lib_eio_luv/tests/dune +++ b/lib_eio_luv/tests/dune @@ -1,3 +1,3 @@ -(mdx - (package eio_luv) - (deps (package eio_luv))) +; (mdx +; (package eio_luv) +; (deps (package eio_luv))) diff --git a/lib_eio_posix/eio_posix.ml b/lib_eio_posix/eio_posix.ml index 7015f76a3..c0016649b 100644 --- a/lib_eio_posix/eio_posix.ml +++ b/lib_eio_posix/eio_posix.ml @@ -21,6 +21,7 @@ type stdenv = < stdout : ; stderr : ; net : Eio.Net.t; + process_mgr : Eio.Process.mgr; domain_mgr : Eio.Domain_manager.t; clock : Eio.Time.clock; mono_clock : Eio.Time.Mono.t; @@ -45,6 +46,7 @@ let run main = method clock = Time.clock method mono_clock = Time.mono_clock method net = Net.v + method process_mgr = Process.v method domain_mgr = Domain_mgr.v method cwd = ((Fs.cwd, "") :> Eio.Fs.dir Eio.Path.t) method fs = ((Fs.fs, "") :> Eio.Fs.dir Eio.Path.t) diff --git a/lib_eio_posix/eio_posix.mli b/lib_eio_posix/eio_posix.mli index 25e912ca0..fee07d4e0 100644 --- a/lib_eio_posix/eio_posix.mli +++ b/lib_eio_posix/eio_posix.mli @@ -5,6 +5,7 @@ type stdenv = < stdout : ; stderr : ; net : Eio.Net.t; + process_mgr : Eio.Process.mgr; domain_mgr : Eio.Domain_manager.t; clock : Eio.Time.clock; mono_clock : Eio.Time.Mono.t; diff --git a/lib_eio_posix/eio_posix_stubs.c b/lib_eio_posix/eio_posix_stubs.c index 93a0f10c4..0d82cba8b 100644 --- a/lib_eio_posix/eio_posix_stubs.c +++ b/lib_eio_posix/eio_posix_stubs.c @@ -187,17 +187,3 @@ CAMLprim value caml_eio_posix_renameat(value v_old_fd, value v_old_path, value v if (ret == -1) uerror("renameat", v_old_path); CAMLreturn(Val_unit); } - -CAMLprim value caml_eio_posix_spawn(value v_errors, value v_actions) { - CAMLparam1(v_actions); - pid_t child_pid; - - child_pid = fork(); - if (child_pid == 0) { - eio_unix_run_fork_actions(Int_val(v_errors), v_actions); - } else if (child_pid < 0) { - uerror("fork", Nothing); - } - - CAMLreturn(Val_long(child_pid)); -} diff --git a/lib_eio_posix/low_level.ml b/lib_eio_posix/low_level.ml index e61a9a2a0..ad1eaf0d4 100644 --- a/lib_eio_posix/low_level.ml +++ b/lib_eio_posix/low_level.ml @@ -261,8 +261,6 @@ module Process = struct Unix.kill t.pid signal ) - external eio_spawn : Unix.file_descr -> Eio_unix.Private.Fork_action.c_action list -> int = "caml_eio_posix_spawn" - let spawn ~sw actions = with_pipe @@ fun errors_r errors_w -> Eio_unix.Private.Fork_action.with_actions actions @@ fun c_actions -> @@ -271,10 +269,7 @@ module Process = struct (* We take the lock to ensure that the signal handler won't reap the process before we've registered it. *) Children.with_lock (fun () -> - let pid = - Fd.use_exn "errors-w" errors_w @@ fun errors_w -> - eio_spawn errors_w c_actions - in + let pid = Eio_unix.Private.Fork_action.spawn (Fd.to_rcfd errors_w) c_actions in Fd.close errors_w; { pid; exit_status = Children.register pid } ) diff --git a/lib_eio_posix/process.ml b/lib_eio_posix/process.ml new file mode 100644 index 000000000..d42b97935 --- /dev/null +++ b/lib_eio_posix/process.ml @@ -0,0 +1,72 @@ +open Low_level +module Rfcd = Eio_unix.Private.Rcfd + +let unix_status_to_stats = function + | Unix.WEXITED i -> Eio.Process.Exited i + | Unix.WSIGNALED i -> Eio.Process.Signaled i + | Unix.WSTOPPED i -> Eio.Process.Stopped i + +let process proc : Eio.Process.t = object + method pid = Process.pid proc + method await = + let status = Eio.Promise.await @@ Process.exit_status proc in + unix_status_to_stats status + method signal i = Process.signal proc i +end + +let read_of_fd ~sw flow = + match Fd.get_fd_opt flow with + | None -> + let r, w = pipe ~sw in + Some (Flow.of_fd w), r + | Some fd -> None, fd + +let write_of_fd ~sw t = + match Fd.get_fd_opt t with + | None -> + let r, w = pipe ~sw in + Some (Flow.of_fd r), w + | Some fd -> None, fd + +let v = object + inherit Eio.Process.mgr + + method spawn ~sw ?cwd ~(stdin : #Eio.Flow.source) ~(stdout : #Eio.Flow.sink) ~(stderr : #Eio.Flow.sink) prog args = + let stdin_w, stdin_fd = read_of_fd ~sw stdin in + let stdout_r, stdout_fd = write_of_fd ~sw stdout in + let stderr_r, stderr_fd = write_of_fd ~sw stderr in + let actions = Process.Fork_action.[ + Eio_unix.Private.Fork_action.inherit_fds [ + 0, Fd.to_rcfd stdin_fd, `Blocking; + 1, Fd.to_rcfd stdout_fd, `Blocking; + 2, Fd.to_rcfd stderr_fd, `Blocking; + ]; + execve prog ~argv:(Array.of_list args) ~env:[||] + ] in + let with_actions cwd fn = match cwd with + | Some ((dir, path) : Eio.Fs.dir Eio.Path.t) -> ( + match Eio.Generic.probe dir Fs.Posix_dir with + | None -> fn actions + | Some posix -> + posix#with_parent_dir path @@ fun dirfd s -> + let cwd = Low_level.openat ?dirfd ~sw ~mode:0 s Low_level.Open_flags.(rdonly + directory) in + fn (Process.Fork_action.fchdir cwd :: actions) + ) + | None -> fn actions + in + let proc = + with_actions cwd @@ fun actions -> + process (Process.spawn ~sw actions) + in + Option.iter (fun stdin_w -> + Eio.Flow.copy stdin stdin_w; + Eio.Flow.close stdin_w + ) stdin_w; + Option.iter (fun stdout_r -> + Fd.close stdout_fd; + Eio.Flow.copy stdout_r stdout) stdout_r; + Option.iter (fun stderr_r -> + Fd.close stderr_fd; + Eio.Flow.copy stderr_r stdout) stderr_r; + proc +end \ No newline at end of file diff --git a/tests/process.md b/tests/process.md new file mode 100644 index 000000000..b7245aa2c --- /dev/null +++ b/tests/process.md @@ -0,0 +1,181 @@ +# Setting up the environment + +```ocaml +# #require "eio_main";; +``` + +Creating some useful helper functions + +```ocaml +open Eio +open Eio.Std + +let paths = [ "/usr/bin"; "/bin"; "/usr/local/bin" ] + +let resolve_program cmd = + match Eio_unix.resolve_program ~paths cmd with + | Some cmd -> cmd + | None -> failwith "Failed to resolve program" + +let spawn ~env ~sw ?cwd cmd args = + let cmd = resolve_program cmd in + Process.spawn ~sw ?cwd ~stdout:env#stdout ~stdin:env#stdin ~stderr:env#stderr env#process_mgr cmd args + +let run fn = + Eio_main.run @@ fun env -> + fn (spawn ~env) env +``` + +Running a program as a subprocess + +```ocaml +# run @@ fun spawn env -> + Switch.run @@ fun sw -> + let t = spawn ~sw "echo" [ "echo"; "hello world" ] in + Process.await t;; +hello world +- : Process.status = Eio.Process.Exited 0 +``` + +Stopping a subprocess works and checking the status waits and reports correctly + +```ocaml +# run @@ fun spawn _env -> + Switch.run @@ fun sw -> + let t = spawn ~sw "sleep" [ "sleep"; "10" ] in + Process.signal t Sys.sigkill; + Process.await t;; +- : Process.status = Eio.Process.Signaled (-7) +``` + +A switch will stop a process when it is released. + + +```ocaml +# run @@ fun spawn env -> + let proc = ref None in + let run () = + Switch.run @@ fun sw -> + proc := Some (spawn ~sw "sleep" [ "sleep"; "10" ]) + in + run (); + Process.await (Option.get !proc);; +- : Process.status = Eio.Process.Signaled (-7) +``` + +Passing in flows allows you to redirect the child process' stdout. + +```ocaml +# run @@ fun _spawn env -> + let process = Eio.Stdenv.process_mgr env in + let fs = Eio.Stdenv.fs env in + let filename = "process-test.txt" in + let run () = + Eio.Path.(with_open_out ~create:(`Exclusive 0o600) (fs / filename)) @@ fun stdout -> + let stdout = (stdout :> Eio.Flow.sink) in + Switch.run @@ fun sw -> + let echo = resolve_program "echo" in + let t = Eio.Process.spawn ~sw ~stdout ~stdin:env#stdin ~stderr:env#stderr process echo [ "echo"; "Hello" ] in + Process.await t + in + match run () with + | Exited 0 -> Eio.Path.(load (fs / filename)) + | _ -> failwith "Subprocess didn't exit cleanly!";; +- : string = "Hello\n" +``` + +Pipes + +```ocaml +# let with_pipe_from_child fn = + Switch.run @@ fun sw -> + let r, w = Eio_unix.pipe sw in + fn ~sw ~r ~w;; +val with_pipe_from_child : + (sw:Switch.t -> + r:< close : unit; probe : 'a. 'a Generic.ty -> 'a option; + read_into : Cstruct.t -> int; read_methods : Flow.read_method list; + unix_fd : [ `Peek | `Take ] -> Unix.file_descr > -> + w:< close : unit; copy : 'b. (#Flow.source as 'b) -> unit; + probe : 'a. 'a Generic.ty -> 'a option; + unix_fd : [ `Peek | `Take ] -> Unix.file_descr; + write : Cstruct.t list -> unit > -> + 'c) -> + 'c = +# let pread env = + with_pipe_from_child @@ fun ~sw ~r ~w -> + let echo = resolve_program "echo" in + let t = + Eio.Process.spawn ~sw ~stdout:(w :> Flow.sink) ~stdin:env#stdin ~stderr:env#stderr env#process_mgr echo [ "echo"; "Hello" ] + in + let status = Process.await t in + Eio.traceln "%a" Eio.Process.pp_status status; + Flow.close w; + let buff = Buffer.create 10 in + Flow.copy r (Flow.buffer_sink buff); + Buffer.contents buff;; +val pread : + < process_mgr : #Process.mgr; stderr : #Flow.sink; stdin : #Flow.source; + .. > -> + string = +# run @@ fun _spawn env -> + pread env;; ++Exited 0 +- : string = "Hello\n" +``` + +Spawning subprocesses in new domains works normally + +```ocaml +# run @@ fun spawn env -> + let mgr = Eio.Stdenv.domain_mgr env in + Eio.Domain_manager.run mgr @@ fun () -> + Switch.run @@ fun sw -> + let echo = resolve_program "echo" in + let t = spawn ~sw echo [ "echo"; "Hello from another domain" ] in + Process.await t;; +Hello from another domain +- : Process.status = Eio.Process.Exited 0 +``` + +Calling `await_exit` multiple times on the same spawn just returns the status. + +```ocaml +# run @@ fun spawn env -> + Switch.run @@ fun sw -> + let echo = resolve_program "echo" in + let t = spawn ~sw echo [ "echo"; "hello world" ] in + (Process.await t, Process.await t, Process.await t);; +hello world +- : Process.status * Process.status * Process.status = +(Eio.Process.Exited 0, Eio.Process.Exited 0, Eio.Process.Exited 0) +``` + +Using sources and sinks that are not backed by file descriptors. + +```ocaml +# run @@ fun _spawn env -> + let proc = env#process_mgr in + let buf = Buffer.create 16 in + let dst = Flow.buffer_sink buf in + Eio.Switch.run @@ fun sw -> + let echo = resolve_program "echo" in + let p = + Eio.Process.spawn proc ~sw ~stdin:env#stdin ~stdout:dst ~stderr:env#stderr echo [ "echo"; "Hello, world" ] + in + let _ : Process.status = Process.await p in + Buffer.contents buf +- : string = "Hello, world\n" +``` + +Changing directory + +```ocaml +# run @@ fun spawn env -> + Switch.run @@ fun sw -> + let root = Eio.Path.(env#fs / "/") in + let child = spawn ~cwd:root ~sw "env" [ "env"; "pwd" ] in + Process.await child +/ +- : Process.status = Eio.Process.Exited 0 +```