Conversation
talex5
left a comment
There was a problem hiding this comment.
Thanks - looking good so far!
| @@ -0,0 +1,31 @@ | |||
| type status = Exited of int | Signaled of int | Stopped of int | |||
There was a problem hiding this comment.
This could be a polymorphic variant (often the Stopped case isn't possible, e.g. for an exit status).
There was a problem hiding this comment.
Would something like
type exit_status = [ `Exited of int | `Signaled of int ]
type status = [ exit_status | `Stopped of int ] be useful? If the Stopped case can sometimes happen in the exit case the exit_status method would still have to handle it. If it can never happen I suppose we can assert false it?
There was a problem hiding this comment.
Yes, I think it can never happen and we should assert false for now.
lib_eio/process.mli
Outdated
|
|
||
| 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 stopped when the switch is released. |
There was a problem hiding this comment.
We should be careful about the word "stopped" here. Normally a "stopped" process is one that has not exited but it just paused.
| let buff = Buffer.create 128 in | ||
| Eio.Flow.copy flow (Eio.Flow.buffer_sink buff); | ||
| Buffer.contents buff | ||
| | _ -> failwith "Subprocess didn't exit cleanly!";; |
There was a problem hiding this comment.
We could probably do with a helper function in lib_eio for checking the exit status. Process.await_exn or something?
There was a problem hiding this comment.
Good idea -- do you think instead of the val exit_status we should provide val await : t -> exit_status and val await_exn : t -> unit ?
lib_eio_linux/eio_linux.ml
Outdated
| inherit Eio.Process.mgr | ||
|
|
||
| method spawn ~sw ?cwd ~(stdin : #Eio.Flow.source) ~(stdout : #Eio.Flow.sink) ~(stderr : #Eio.Flow.sink) prog args = | ||
| let chdir = Option.to_list cwd |> List.map (fun (_, s) -> Process.Fork_action.chdir s) in |
There was a problem hiding this comment.
You can't ignore the base directory here (s is relative to that). It needs to use fchdir in that case.
There was a problem hiding this comment.
Hmm, I've given it a quick go in 9f2a5ed which seems to be working but wasn't sure if that's right (or the cleanest way) to do it
|
That's great! I'll probably make an Eio 0.9 release some time next week, and it would be great to have this in. The eio_luv backend is only used on Windows now (which doesn't work with OCaml 5.0) so it's probably OK if it just raises an exception if you try to do something it doesn't support. |
|
The number of users of Eio/Windows is probably zero, given the lack of a stable OCaml 5.x release for it. Perhaps we should just remove it for Eio 0.9 and ease the path for this PR, and focus all efforts on getting the IOCP backend (#125) over the line? |
| (** [spawn ~sw mgr ?cwd ~stdin ~stdout ~stderr cmd args] creates a new subprocess that is connected to the | ||
| switch [sw]. A process will be stopped when the switch is released. | ||
|
|
||
| You must provide a standard input and outputs that are backed by file descriptors and |
There was a problem hiding this comment.
| You must provide a standard input and outputs that are backed by file descriptors and | |
| [stdin], [stderr] and [stdout] must be backed by file descriptors. |
There was a problem hiding this comment.
Thanks, hopefully with the pipe changes I added in d553598 this can be completely removed as we check if there's an FD and if not we pipe into the sources and sinks (that code could do with a review I think)
Using `clone3` directly on Linux is difficult as there is no glibc wrapper for it.
f7a982f to
d553598
Compare
|
5afdbd4 added |
|
The |
talex5
left a comment
There was a problem hiding this comment.
Looking good! We also need support for environment variables (at the moment, it always clears the environment). I think it's fine to make passing an environment optional and default to inheriting the current one.
| 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 |
There was a problem hiding this comment.
While the low-level API should mimic the platform's native API, this high-level API is where we can add convenience features to make things easier to use.
For example, I think it should handle looking up the path for you. It could have an optional ?executable argument to take the path explicitly, but default to doing the sensible thing with argv[0] (i.e. look up implicit paths in $PATH). Note that this example wouldn't work on e.g. NixOS, where echo is at /run/current-system/sw/bin/echo.
| 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.Switch.run @@ fun sw -> | |
| let child = Eio.Process.spawn proc_mgr ~sw ~stdout ["echo"; "hello"] in |
stdin can default to /dev/null I think (we don't want to encourage passing it if it's not needed), and thinking about it further I'm OK with stderr being passed through by default. We already let all Eio code output traceln messages to stderr.
Not sure what's best for stdout. Making it optional and passing it though seems OK, as does making it a required argument. I think defaulting to /dev/null would be bad though as you wouldn't get any error indicating why it didn't work (unlike redirecting stdin, where you get end-of-file).
| 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;; |
There was a problem hiding this comment.
At some point we should add some other convenience functions like popen for cases like this, e.g.
| 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;; | |
| Eio.Process.popen ["echo"; "hello"] |
| [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 |
There was a problem hiding this comment.
Note: this will be a broken link until the next release. Might be OK, or we could link to the source code until then.
| let stdin (t : <stdin : #Flow.source; ..>) = t#stdin | ||
| let stdout (t : <stdout : #Flow.sink; ..>) = t#stdout | ||
| let stderr (t : <stderr : #Flow.sink; ..>) = t#stderr | ||
| let stdio (t : <stdin : #Flow.source; stdout: #Flow.sink; stderr : #Flow.sink; ..>) = t#stdin, t#stdout, t#stderr |
There was a problem hiding this comment.
Not very convinced about this. I suspect that providing defaults for the standard streams would make this unnecessary.
| @@ -0,0 +1,31 @@ | |||
| type status = Exited of int | Signaled of int | Stopped of int | |||
There was a problem hiding this comment.
Yes, I think it can never happen and we should assert false for now.
| (** 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]. *) |
There was a problem hiding this comment.
It's a shame we won't have the failing command in the error message.
We could have a Process.run for the common case of spawn followed by await. Then it would be easy to add run_exn and have it include the command in the error.
| ; (mdx | ||
| ; (package eio_luv) | ||
| ; (deps (package eio_luv))) |
| 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 |
There was a problem hiding this comment.
If the cwd isn't a native directory then it should raise an exception instead of ignoring it.
| process (Process.spawn ~sw actions) | ||
| in | ||
| Option.iter (fun stdin_w -> | ||
| Eio.Flow.copy stdin stdin_w; |
There was a problem hiding this comment.
You need to fork off a another fiber to handle this (I see you had it that way earlier and then removed it for some reason).
Failing to fork will work OK for short messages, where the kernel will buffer them, but will deadlock for longer ones. It's also a problem for interactive programs (e.g. hooked up to GUI). Imagine the program wants to output a long set of terms and conditions and then ask for confirmation. Once the stdout pipe is full, it will stop and wait for us to read it, but we're still here waiting for the user to finish sending the input (which they won't do because they haven't seen any output yet).
There was a problem hiding this comment.
Very quickly, it seemed that there was a race between the process exiting and the standard input/output being read/written. This very reliably fails in the old fork version:
open Eio
open Eio.Std
let run env =
let buf = Buffer.create 128 in
Switch.run @@ fun sw ->
let stdout = Flow.buffer_sink buf in
let child =
Process.spawn ~sw env#process_mgr ~stdin:env#stdin ~stdout ~stderr:env#stderr "/bin/echo" [ "echo"; "Hello" ]
in
Process.await_exn child;
let contents = Buffer.contents buf in
if contents = "Hello\n" then () else failwith contents
let () =
Eio_main.run @@ fun env ->
for i = 0 to 1000 do
Eio.traceln "Process number %i" i;
run env
doneBut your comment is right!
There was a problem hiding this comment.
Yes, you need to wait for stdout to be copied to the buffer. Just waiting for the process to exit isn't sufficient. This works (but is easy to get wrong):
let run env =
let buf = Buffer.create 128 in
Switch.run (fun sw ->
let stdout = Flow.buffer_sink buf in
let child =
Process.spawn ~sw env#process_mgr ~stdin:env#stdin ~stdout ~stderr:env#stderr "/bin/echo" [ "echo"; "Hello" ]
in
Process.await_exn child;
);
let contents = Buffer.contents buf in
if contents = "Hello\n" then () else failwith contentsPerhaps we should just expose the pipes and let the user do the reading, as Lwt does? It's a bit of a pain with the types (which is why Lwt needs so many functions). Maybe GADTs would help (so if you pass ~stdout:Pipe then you can do Process.stdout child to get the flow).
Then we can add convenience functions on top (like popen) that collect the output for you, rather than hard-coding that behaviour in each backend.
https://docs.python.org/3/library/subprocess.html might be a good source of ideas.
| - : string = "Hello, world\n" | ||
| ``` | ||
|
|
||
| Changing directory |
There was a problem hiding this comment.
Needs a test for the confined case too.
What was the problem?
I'm planning to remove eio_luv right after the 0.9 release. Keeping it in 0.9 might make things slightly easier for people who used it directly and want to move to eio_posix, or who want to compare the performance between them. |
|
I had a go at some more changes in my branch (at https://github.com/talex5/eio/tree/proc-redux). I think there's enough API design work still to do here that it's better to release Eio 0.9 without a cross-platform process API. Then we can spend a few weeks trying different APIs and get something merged for Eio 0.10 instead. |
|
Rebased and updated as #499. |
|
Thanks! I'll review the process PR later today :)) Closing in favour of it! |
An updated version of #330 built on top of #472 -- this is more to inform #472 and any other changes we want to make to the low-level process API. I'm not convinced by f7a982f just yet, it was more of an experiment than anything else.
I've also half-disabled the
libuvbackend just to get it out of the way.Some bits to do:
Eio.Process