Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
94 changes: 94 additions & 0 deletions lib_eio/process.ml
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,100 @@ type 'tag mgr_ty = [ `Process_mgr | `Platform of 'tag ]
type 'a mgr = 'a r
constraint 'a = [> [> `Generic] mgr_ty]

module Env = struct
let on_windows = (Sys.os_type = "Win32")

module Name = struct
type t = string

let normalise =
if on_windows then String.uppercase_ascii
else Fun.id

let compare x y =
String.compare (normalise x) (normalise y)

let starts_with ~prefix =
let prefix = normalise prefix in
fun x -> String.starts_with ~prefix (normalise x)

let validate t =
let bad_char = function
| '\000' | '=' -> true
| _ -> false
in
if t = "" || String.exists bad_char t then
Fmt.invalid_arg "Invalid environment variable name %S" t
end

module M = Map.Make(Name)

type t = string array

let of_array = Fun.id
let to_array = Fun.id
let empty = [| |]

let validate_value value =
if String.contains value '\000' then
Fmt.invalid_arg "Invalid environment variable value %S" value

let validate_binding (name, value) =
Name.validate name;
Option.iter validate_value value

let entry name value =
Printf.sprintf "%s=%s" name value

let get_opt name t =
Name.validate name;
let prefix = name ^ "=" in
Comment thread
avsm marked this conversation as resolved.
Array.find_opt (Name.starts_with ~prefix) t
|> Option.map (fun e ->
let i = String.length prefix in
String.sub e i (String.length e - i)
)

let override bindings t =

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a deviation from execve behaviour, which passes through duplicate bindings and lets glibc/musl/etc handle that. Not necessarily a bad thing though, I haven't investigated what the libcs do yet.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll add some docs, but the idea is that the raw of_array API lets you do whatever you want (arbitrary array of strings), while the bindings APIs (that take lists of pairs) work like setenv (every entry added must be a key/value pair and duplicates are not created).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This makes sense. What about case sensitivity? I think windows end keys are insensitive

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point. I guess the easiest way would be to change the behaviour of Process.Env depending on the host OS. I wonder what encoding Windows assumes for a case-insensitive compare? Are names always ASCII there?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've pushed a commit that does case insensitive compares on Windows now. It's not very efficient, but there aren't usually many variables anyway.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The change looks good, but I really dislike the portable interface varying behaviour based on which host it's running on. Here's a radical idea: why not specify our Eio environment interface as explicitly being case insensitive? We are already constraining it to forbid duplicates, and it seems like normalising on case should also be very safe. We could also preserve the case at the Eio level (so it's passed through as-is) but is case-insensitive for comparisons.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Being case insensitive in general seems bad: it's a surprising change to the expected behaviour, and it causes trouble with non-ascii encodings.

The other option is to track whether a particular environment is Windows-style or not. But we can't use a flag at the moment because the type needs to be string array. It would be possible to track the type with a fake entry at the start (EIO_OS=Windows or something, that gets stripped out in to_array). Ugly, though.

A simpler solution is to recommend that environment variable names are upper-case (which they mostly are anyway). As long as all variables are uppercase, the Windows and POSIX behaviours are the same anyway.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A simpler solution is to recommend that environment variable names are upper-case (which they mostly are anyway). As long as all variables are uppercase, the Windows and POSIX behaviours are the same anyway.

agreed! good idea

List.iter validate_binding bindings;
let all_bindings = M.of_list bindings in
let bindings = ref all_bindings in
let updated =
Array.to_list t
|> List.filter_map (fun e ->
match String.index e '=' with
| exception Not_found -> Some e (* Not a normal k=v entry *)
| i ->
let name = String.sub e 0 i in
match M.find_opt name all_bindings with
| None -> Some e (* We're not changing this *)
| Some x ->
if M.mem name !bindings then (
bindings := M.remove name !bindings;
match x with
| None -> None (* Remove existing entry *)
| Some v -> Some (entry name v) (* Update existing entry *)
) else None (* Remove duplicate entry *)
)
in
let extra =
M.to_list !bindings
|> List.filter_map (function
| _, None -> None (* Remove entry that wasn't there anyway *)
| k, Some v ->
Some (entry k v) (* Add new entry *)
)
in
Array.of_list (updated @ extra)

let of_bindings xs =
override (List.map (fun (k, v) -> (k, Some v)) xs) empty

let pp f t =
Fmt.pf f "[@[<v>%a@]]"
(Fmt.array ~sep:Fmt.cut (Fmt.fmt "%S")) t
end

module Pi = struct
module type PROCESS = sig
type t
Expand Down
65 changes: 61 additions & 4 deletions lib_eio/process.mli
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,63 @@ type 'a mgr = 'a r
constraint 'a = [> [> `Generic] mgr_ty]
(** A process manager capable of spawning new processes. *)

module Env : sig
(** A list of environment variable entries.

By convention, the entries are strings of the form "name=value",
each name is unique, order doesn't matter, and "" is not a valid name.

Also, due to the representation:
- Names cannot contain the '=' character.
- Neither names nor values can contain '\000'.

On Windows, name comparison is (ASCII) case-insensitive.
The convention of always using uppercase ASCII names for environment
variables will avoid different behaviour across platforms. *)

type t = string array
(** Note: this type is currently exposed for backwards compatibility and will
likely be made abstract in the future. The array is intended to be immutable. *)

val empty : t
(** An environment with no bindings. *)

val of_bindings : (string * string) list -> t
(** [of_bindings xs] is a new environment containing only the bindings in [xs].

This just adds [xs] to {!empty} using {!override}. *)

val get_opt : string -> t -> string option
(** [get_opt name t] is the value of [name] in [t], or [None] if there is no such binding.

@raise Invalid_argument if [name] is not a valid name. *)

val override : (string * string option) list -> t -> t
(** [override bindings t] is a new environment which is like [t]
except that the updates in [bindings] have been applied.

Each entry in [bindings] is a [(name, new_value)] pair.
[new_value] can be [None] to remove [name] (ignored if [name] is not present).

If there are several bindings for the same name in [bindings] then the last one is used.
If there are several bindings for an updated name in [t] then all are removed first.

@raise Invalid_argument if any binding is invalid. *)

val of_array : string array -> t
(** Create a [t] from e.g. the results of {!Unix.environment}.

The bindings are used as-is and need not conform to the conventions (e.g.
they may contain duplicate names).

Note: the array is assumed to be immutable. *)

val to_array : t -> string array
(** [to_array t] gets [t] as an array. The array should be treated as immutable. *)

val pp : t Fmt.t
end

(** {2 Processes} *)

val pid : _ t -> int
Expand Down Expand Up @@ -82,7 +139,7 @@ val spawn :
?stdin:_ Flow.source ->
?stdout:_ Flow.sink ->
?stderr:_ Flow.sink ->
?env:string array ->
?env:Env.t ->
?executable:string ->
string list -> 'tag ty r
(** [spawn ~sw mgr args] creates a new child process that is connected to the switch [sw].
Expand All @@ -109,7 +166,7 @@ val run :
?stdout:_ Flow.sink ->
?stderr:_ Flow.sink ->
?is_success:(int -> bool) ->
?env:string array ->
?env:Env.t ->
?executable:string ->
string list -> unit
(** [run] does {!spawn} followed by {!await_exn}, with the advantage that if the process fails then
Expand All @@ -127,7 +184,7 @@ val parse_out :
?stdin:_ Flow.source ->
?stderr:_ Flow.sink ->
?is_success:(int -> bool) ->
?env:string array ->
?env:Env.t ->
?executable:string ->
string list -> 'a
(** [parse_out mgr parser args] runs [args] and parses the child's stdout with [parser].
Expand Down Expand Up @@ -183,7 +240,7 @@ module Pi : sig
?stdin:Flow.source_ty r ->
?stdout:Flow.sink_ty r ->
?stderr:Flow.sink_ty r ->
?env:string array ->
?env:Env.t ->
?executable:string ->
string list ->
tag ty r
Expand Down
1 change: 1 addition & 0 deletions lib_eio/unix/fork_action.ml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ external make_string_array : int -> c_array = "eio_unix_make_string_array"
external action_execve : unit -> fork_fn = "eio_unix_fork_execve"
let action_execve = action_execve ()
let execve path ~argv ~env =
let env = Eio.Process.Env.to_array env in
let argv_c_array = make_string_array (Array.length argv) in
let env_c_array = make_string_array (Array.length env) in
{ run = fun k -> k (Obj.repr (action_execve, path, argv_c_array, argv, env_c_array, env)) }
Expand Down
2 changes: 1 addition & 1 deletion lib_eio/unix/fork_action.mli
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ val with_actions : t list -> (c_action list -> 'a) -> 'a

(** {2 Actions} *)

val execve : string -> argv:string array -> env:string array -> t
val execve : string -> argv:string array -> env:Eio.Process.Env.t -> t
(** See [execve(2)].

This replaces the current executable,
Expand Down
6 changes: 3 additions & 3 deletions lib_eio/unix/process.ml
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ let get_executable ~args = function

let get_env = function
| Some e -> e
| None -> Unix.environment ()
| None -> Unix.environment () |> Eio.Process.Env.of_array

let translate_execve_error ~executable f =
try f () with
Expand Down Expand Up @@ -98,7 +98,7 @@ module Pi = struct
?uid:int ->
?gid:int ->
?login_tty:Fd.t ->
env:string array ->
env:Eio.Process.Env.t ->
fds:(int * Fd.t * Fork_action.blocking) list ->
executable:string ->
string list ->
Expand Down Expand Up @@ -126,7 +126,7 @@ module Make_mgr (X : sig
?uid:int ->
?gid:int ->
?login_tty:Fd.t ->
env:string array ->
env:Eio.Process.Env.t ->
fds:(int * Fd.t * Fork_action.blocking) list ->
executable:string ->
string list ->
Expand Down
6 changes: 3 additions & 3 deletions lib_eio/unix/process.mli
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ module Pi : sig
?uid:int ->
?gid:int ->
?login_tty:Fd.t ->
env:string array ->
env:Eio.Process.Env.t ->
fds:(int * Fd.t * Fork_action.blocking) list ->
executable:string ->
string list ->
Expand All @@ -50,7 +50,7 @@ module Make_mgr (X : sig
?uid:int ->
?gid:int ->
?login_tty:Fd.t ->
env:string array ->
env:Eio.Process.Env.t ->
fds:(int * Fd.t * Fork_action.blocking) list ->
executable:string ->
string list ->
Expand All @@ -66,7 +66,7 @@ val spawn_unix :
?gid:int ->
?login_tty:Fd.t ->
fds:(int * Fd.t * Fork_action.blocking) list ->
?env:string array ->
?env:Eio.Process.Env.t ->
?executable:string ->
string list ->
ty r
Expand Down
17 changes: 10 additions & 7 deletions lib_eio_linux/tests/spawn.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@
```ocaml
open Eio.Std

module Env = Eio.Process.Env
module Process = Eio_linux.Low_level.Process

let default_env = Unix.environment () |> Env.of_array
```

## Spawning processes
Expand All @@ -18,7 +21,7 @@ Setting environment variables:
let child = Process.spawn ~sw Process.Fork_action.[
execve "/usr/bin/env"
~argv:[| "env" |]
~env:[| "FOO=bar" |];
~env:(Env.of_bindings ["FOO", "bar"]);
] in
Promise.await (Process.exit_status child);;
FOO=bar
Expand All @@ -34,7 +37,7 @@ Changing directory:
chdir "/";
execve "/usr/bin/env"
~argv:[| "env"; "pwd" |]
~env:(Unix.environment ())
~env:default_env
] in
Promise.await (Process.exit_status child);;
/
Expand All @@ -58,7 +61,7 @@ Changing directory using a file descriptor:
fchdir root;
execve "/usr/bin/env"
~argv:[| "env"; "pwd" |]
~env:(Unix.environment ())
~env:default_env
] in
Promise.await (Process.exit_status child);;
/
Expand All @@ -73,7 +76,7 @@ Exit status:
let child = Process.spawn ~sw Process.Fork_action.[
execve "/usr/bin/env"
~argv:[| "env"; "false" |]
~env:(Unix.environment ())
~env:default_env
] in
Promise.await (Process.exit_status child);;
- : Unix.process_status = Unix.WEXITED 1
Expand All @@ -88,7 +91,7 @@ Failure starting child:
chdir "/idontexist";
execve "/usr/bin/env"
~argv:[| "env"; "pwd" |]
~env:(Unix.environment ())
~env:default_env
]
Exception: Unix.Unix_error(Unix.ENOENT, "chdir", "")
```
Expand All @@ -102,7 +105,7 @@ Signalling a running child:
Process.spawn ~sw Process.Fork_action.[
execve "/usr/bin/env"
~argv:[| "env"; "sleep"; "1000" |]
~env:(Unix.environment ())
~env:default_env
]
in
Process.signal child Sys.sigkill;
Expand All @@ -122,7 +125,7 @@ Signalling an exited child does nothing:
Process.spawn ~sw Process.Fork_action.[
execve "/usr/bin/env"
~argv:[| "env" |]
~env:[| "FOO=bar" |];
~env:(Env.of_bindings ["FOO", "bar"]);
]
in
ignore (Promise.await (Process.exit_status child) : Unix.process_status);
Expand Down
Loading
Loading