-
Notifications
You must be signed in to change notification settings - Fork 71
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Initial cross-platform subprocess support
This also splits Eio_unix into separate modules. Co-authored-by: Thomas Leonard <talex5@gmail.com>
- Loading branch information
1 parent
543d77b
commit cd1a9df
Showing
18 changed files
with
749 additions
and
39 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,110 @@ | ||
type exit_status = [ | ||
| `Exited of int | ||
| `Signaled of int | ||
] | ||
|
||
type status = [ exit_status | `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 error = | ||
| Executable_not_found of string | ||
| Child_error of exit_status | ||
|
||
type Exn.err += E of error | ||
|
||
let err e = Exn.create (E e) | ||
|
||
let () = | ||
Exn.register_pp (fun f -> function | ||
| E e -> | ||
Fmt.string f "Process "; | ||
begin match e with | ||
| Executable_not_found e -> Fmt.pf f "Executable %S not found" e; | ||
| Child_error e -> Fmt.pf f "Child_error %a" pp_status e; | ||
end; | ||
true | ||
| _ -> false | ||
) | ||
|
||
class virtual t = object | ||
method virtual pid : int | ||
method virtual await : exit_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 (Child_error status)) | ||
|
||
let signal proc = proc#signal | ||
|
||
class virtual mgr = object | ||
method virtual pipe : | ||
sw:Switch.t -> | ||
<Flow.source; Flow.close> * <Flow.sink; Flow.close> | ||
|
||
method virtual spawn : | ||
sw:Switch.t -> | ||
?cwd:Fs.dir Path.t -> | ||
?stdin:Flow.source -> | ||
?stdout:Flow.sink -> | ||
?stderr:Flow.sink -> | ||
?env:string array -> | ||
?executable:string -> | ||
string list -> | ||
t | ||
end | ||
|
||
let bad_char = function | ||
| ' ' | '"' | '\'' | '\\' -> true | ||
| c -> | ||
let c = Char.code c in | ||
c <= 32 || c >= 127 | ||
|
||
let pp_arg f x = | ||
if x = "" || String.exists bad_char x then Fmt.(quote string) f x | ||
else Fmt.string f x | ||
|
||
let pp_args = Fmt.hbox (Fmt.list ~sep:Fmt.sp pp_arg) | ||
|
||
let spawn ~sw (t:#mgr) ?cwd ?stdin ?stdout ?stderr ?env ?executable args = | ||
t#spawn ~sw | ||
?cwd:(cwd :> Fs.dir Path.t option) | ||
?env | ||
?executable args | ||
?stdin:(stdin :> Flow.source option) | ||
?stdout:(stdout :> Flow.sink option) | ||
?stderr:(stderr :> Flow.sink option) | ||
|
||
let run (t:#mgr) ?cwd ?stdin ?stdout ?stderr ?env ?executable args = | ||
Switch.run @@ fun sw -> | ||
let child = spawn ~sw t ?cwd ?stdin ?stdout ?stderr ?env ?executable args in | ||
match await child with | ||
| `Exited 0 -> () | ||
| status -> | ||
let ex = err (Child_error status) in | ||
raise (Exn.add_context ex "running command: %a" pp_args args) | ||
|
||
let pipe (t:#mgr) = t#pipe | ||
|
||
let parse_out (t:#mgr) parse ?cwd ?stdin ?stderr ?env ?executable args = | ||
Switch.run @@ fun sw -> | ||
let r, w = pipe t ~sw in | ||
try | ||
let child = spawn ~sw t ?cwd ?stdin ~stdout:w ?stderr ?env ?executable args in | ||
Flow.close w; | ||
let output = Buf_read.parse_exn parse r ~max_size:max_int in | ||
Flow.close r; | ||
await_exn child; | ||
output | ||
with Exn.Io _ as ex -> | ||
let bt = Printexc.get_raw_backtrace () in | ||
Exn.reraise_with_context ex bt "running command: %a" pp_args args |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,126 @@ | ||
type exit_status = [ | ||
| `Exited of int (** Process exited with the given return code. *) | ||
| `Signaled of int (** Process was killed by the given signal. *) | ||
] | ||
|
||
type status = [ | ||
| exit_status | ||
| `Stopped of int (** Process was stopped (paused) by the given signal. *) | ||
] | ||
|
||
val pp_status : status Fmt.t | ||
|
||
type error = | ||
| Executable_not_found of string (** The requested executable does not exist. *) | ||
| Child_error of exit_status (** The child exited with an error status. *) | ||
|
||
type Exn.err += E of error | ||
|
||
val err : error -> exn | ||
(** [err e] is [Eio.Exn.create (E e)] *) | ||
|
||
(** A child process. *) | ||
class virtual t : object | ||
method virtual pid : int | ||
method virtual await : exit_status | ||
method virtual signal : int -> unit | ||
end | ||
|
||
val pid : #t -> int | ||
(** [pid t] is the process ID of [t]. *) | ||
|
||
val await : #t -> exit_status | ||
(** [await t] waits for subprocess [t] 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]. | ||
If the process has already exited then this does nothing | ||
(it will not signal a different process, even if the PID has been reused). | ||
See {!Sys} for the signal numbers. *) | ||
|
||
class virtual mgr : object | ||
method virtual pipe : | ||
sw:Switch.t -> | ||
<Flow.source; Flow.close> * <Flow.sink; Flow.close> | ||
|
||
method virtual spawn : | ||
sw:Switch.t -> | ||
?cwd:Fs.dir Path.t -> | ||
?stdin:Flow.source -> | ||
?stdout:Flow.sink -> | ||
?stderr:Flow.sink -> | ||
?env:string array -> | ||
?executable: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 -> | ||
?env:string array -> | ||
?executable:string -> | ||
string list -> t | ||
(** [spawn ~sw mgr args] creates a new subprocess that is connected to the switch [sw]. | ||
The subprocess will be sent {! Sys.sigkill} when the switch is released. | ||
If the flows [stdin], [stdout] and [stderr] are not backed by file descriptors then | ||
this also creates pipes and spawns fibers to copy the data as necessary. | ||
If you need more control over file descriptors, see {!Eio_unix.Process}. | ||
@param cwd The current working directory of the process (default: same as parent process). | ||
@param stdin The flow to attach to the process's standard input (default: same as parent process). | ||
@param stdout A flow that the child process's standard output goes to (default: same as parent process). | ||
@param stderr A flow that the child process's standard error goes to (default: same as parent process). | ||
@param env The environment for the child (default: same as parent process). | ||
@param executable The path of the executable to run. | ||
If not given then the first item in [args] is used, | ||
searching $PATH for it if necessary. *) | ||
|
||
val run : | ||
#mgr -> | ||
?cwd:#Fs.dir Path.t -> | ||
?stdin:#Flow.source -> | ||
?stdout:#Flow.sink -> | ||
?stderr:#Flow.sink -> | ||
?env:string array -> | ||
?executable:string -> | ||
string list -> unit | ||
(** [run] does {!spawn} followed by {!await_exn}, with the advantage that if the process fails then | ||
the error message includes the command that failed. | ||
Note: If [spawn] needed to create extra fibers to copy [stdin], etc, then it also waits for those to finish. *) | ||
|
||
val parse_out : | ||
#mgr -> | ||
'a Buf_read.parser -> | ||
?cwd:#Fs.dir Path.t -> | ||
?stdin:#Flow.source -> | ||
?stderr:#Flow.sink -> | ||
?env:string array -> | ||
?executable:string -> | ||
string list -> 'a | ||
(** [parse_out mgr parser args] runs [args] and parses the child's stdout with [parser]. | ||
It also waits for the process to finish and checks its exit status is zero. | ||
Note that [parser] must consume the entire output of the process (like {!Buf_read.parse}). | ||
This is a convenience wrapper around {!run}, | ||
and the optional arguments have the same meanings. *) | ||
|
||
val pp_args : string list Fmt.t | ||
(** Formats a list of arguments, quoting any that might cause confusion to the reader. | ||
This is intended for use in error messages and logging.*) |
Oops, something went wrong.