-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
12 changed files
with
276 additions
and
142 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
# Common Internal Functionality | ||
|
||
[Online Docs](https://just-max.github.io/less-power/main/less-power/Common/index.html). | ||
|
||
**Note**: this library is exposed only as an implementation detail. It may change at any time. |
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 |
---|---|---|
@@ -1,11 +1,13 @@ | ||
(** Functionality common to multiple components. *) | ||
|
||
module Error_context = Error_context | ||
[@@@alert lp_internal "This module is internal and has an unstable interface!"] | ||
|
||
module Internal = struct | ||
[@@@alert lp_internal "This module is internal and has an unstable interface!"] | ||
module Util = Util | ||
module Pp_util = Pp_util | ||
module Path_util = Path_util | ||
module Ctx_util = Ctx_util | ||
module P_run = P_run | ||
|
||
module Util = Util | ||
module Path_util = Path_util | ||
module Syntax = Syntax | ||
end | ||
(* module Syntax = Syntax *) | ||
|
||
(* {!modules: Util Pp_util Path_util Ctx_util P_run} *) |
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,54 @@ | ||
(** Context managers, loosely inspired by Python *) | ||
|
||
(** A context manager of type [('a, 'b, 'c) t] takes a continuation, and | ||
should feed the continuation a value of type ['a]. Once the continuation | ||
returns with either a [Ok of 'b] or an exception, the continuation should | ||
perform cleanup, and may suppress the exception by producing a suitable | ||
result (of type ['c]) instead. Typically, ['b = 'c]. | ||
This representation has the advantage that some existing functions library | ||
already implement this type (e.g. {!In_channel.with_open_text}). *) | ||
type ('a, 'b, 'c) t = ('a -> ('b, exn) result) -> ('c, exn) result | ||
|
||
(** [with_context cm f] runs [f] in the context manager [cm] *) | ||
let with_context (cm : _ t) f = | ||
let k x = | ||
try Ok (f x) | ||
with e -> Error e | ||
in | ||
match cm k with | ||
| Ok x -> x | ||
| Error e -> raise e | ||
|
||
(** Let-syntax for {!with_context} *) | ||
let ( let< ) = with_context | ||
|
||
(* {1 Context managers} *) | ||
|
||
(** As per {!Filename.temp_file}. Removes the temporary file upon completion. *) | ||
let temp_file ?temp_dir prefix suffix : _ t = fun k -> | ||
let path = Filename.temp_file ?temp_dir prefix suffix in | ||
let result = k path in | ||
Sys.remove path; | ||
result | ||
|
||
(** As per {!Unix.openfile}. Closes the file descriptor upon completion. *) | ||
let openfile path flags perm : _ t = fun k -> | ||
let fd = Unix.openfile path flags perm in | ||
let result = k fd in | ||
Unix.close fd; | ||
result | ||
|
||
(** As per {!Sys.set_signal}. Restores the previous signal behavior on completion. *) | ||
let set_signal signal behavior : _ t = fun k -> | ||
let prev_behavior = Sys.signal signal behavior in | ||
let result = k () in | ||
Sys.set_signal signal prev_behavior; | ||
result | ||
|
||
(** As per {!Unix.sigprocmask}. Restores the previous mask on completion. *) | ||
let sigprocmask mode sigs : _ t = fun k -> | ||
let prev_mask = Unix.sigprocmask mode sigs in | ||
let result = k () in | ||
Unix.sigprocmask Unix.SIG_SETMASK prev_mask |> (ignore : int list -> _); | ||
result |
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 |
---|---|---|
@@ -1,4 +1,4 @@ | ||
(library | ||
(name common) | ||
(public_name less-power.common) | ||
(libraries unix fmt fileutils)) | ||
(libraries unix fmt fileutils mtime mtime.clock.os)) |
This file was deleted.
Oops, something went wrong.
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,135 @@ | ||
(** Even higher-level wrapper around [Unix.open_process]. *) | ||
|
||
(** Waits for the given process or times out. *) | ||
let wait_pid_timeout t pid = | ||
let open Ctx_util in | ||
let< () = sigprocmask Unix.SIG_BLOCK [Sys.sigchld] in | ||
|
||
let exception Sigchld in | ||
let< () = set_signal Sys.sigchld (Sys.Signal_handle (fun _ -> raise Sigchld)) in | ||
let t0 = Mtime_clock.counter () in | ||
let rec task () = | ||
let pidw, status = Unix.(waitpid [WNOHANG] pid) in | ||
if pidw <> 0 then Some status | ||
else | ||
try | ||
let< () = sigprocmask Unix.SIG_UNBLOCK [Sys.sigchld] in | ||
let elapsed = Mtime_clock.count t0 in | ||
if Mtime.Span.compare elapsed t < 0 then | ||
Unix.sleepf @@ Util.span_to_float_s @@ Mtime.Span.abs_diff elapsed t; | ||
None | ||
with | ||
Sigchld -> task () | ||
in | ||
task () | ||
|
||
(** Timeout after [duration], sending [signal]. If [kill_after] is [Some t], | ||
then send [SIGKILL] after waiting an additional [t]. *) | ||
type timeout_description = | ||
{ duration : Mtime.span; signal : int; kill_after : Mtime.span option } | ||
|
||
let timeout ?(signal = Sys.sigterm) ?kill_after duration = | ||
{ duration; signal; kill_after } | ||
|
||
type phase = Completed | TimedOut | Killed | ||
(** Whether the process is running normally, has received | ||
the first signal, or has received a SIGKILL. *) | ||
|
||
type result = { | ||
phase : phase (** What phase was the process in when it completed? *); | ||
status : Unix.process_status; | ||
stdout : string; | ||
stderr : string; | ||
elapsed : Mtime.span; | ||
} | ||
|
||
let pp_result ?(hide_stdout = false) ?(hide_stderr = false) ?command_line ppf r = | ||
let open Fmt in | ||
let open Pp_util in | ||
let pp_status ppf = function | ||
| TimedOut, _ -> string ppf "timed out" | ||
| Killed, _ -> string ppf "timed out and was killed" | ||
| Completed, Unix.WEXITED st -> pf ppf "exited with status %d" st | ||
| Completed, Unix.WSIGNALED si -> pf ppf "exited on signal %a" Dump.signal si | ||
| Completed, Unix.WSTOPPED si -> pf ppf "stopped on signal %a" Dump.signal si | ||
in | ||
let pp_output name ppf body = | ||
let w = 40 in | ||
let wn = String.length name + 2 in | ||
let we1 = (w - wn) / 2 in | ||
let we2 = w - we1 - wn in | ||
pf ppf "@[<v>%a %s %a@,%a%a@]" | ||
(pp_repeat we1 char) '=' name (pp_repeat we2 char) '=' | ||
pp_text_ body | ||
(pp_repeat w char) '=' | ||
in | ||
pf ppf "@[<v>@[%a@ %a@ after %a@]%a%a@]" | ||
(option | ||
~none:(const string "A command") | ||
(fun ppf -> pf ppf "@[<2>The command @[<2>%a@]@]" (list ~sep:sp @@ pp_of string (* Filename.quote *) Fun.id))) | ||
command_line | ||
pp_status (r.phase, r.status) | ||
Mtime.Span.pp r.elapsed | ||
(option (cut ++ pp_output "STDOUT")) | ||
(if hide_stdout then None else Some r.stdout) | ||
(option (cut ++ pp_output "STDERR")) | ||
(if hide_stderr then None else Some r.stderr) | ||
|
||
(** Send the [input] to [command] with the given [args]. | ||
Note: uses temporary files provided by {!Filename}. *) | ||
let p_run ?timeout:timeout_desc ?input ?(args = []) command = | ||
let open Ctx_util in | ||
|
||
let< stdout_path = temp_file command "stdout" in | ||
let< stderr_path = temp_file command "stderr" in | ||
|
||
let phase, status, elapsed = | ||
let< stdin_path = temp_file command "stdin" in | ||
input |> Option.iter (fun inp -> | ||
let< stdin_ch = Out_channel.with_open_text stdin_path in | ||
let ppf = Format.formatter_of_out_channel stdin_ch in | ||
Format.fprintf ppf "%t" inp; | ||
Format.pp_print_flush ppf ()); | ||
|
||
let< stdin_fd = openfile stdin_path Unix.[O_RDONLY] 0o000 in (* ; O_CLOEXEC? *) | ||
let< stdout_fd = openfile stdout_path Unix.[O_WRONLY] 0o000 in | ||
let< stderr_fd = openfile stderr_path Unix.[O_WRONLY] 0o000 in | ||
|
||
let t0 = Mtime_clock.counter () in | ||
|
||
let pid = | ||
Unix.create_process command | ||
(Array.of_list (command :: args)) | ||
stdin_fd stdout_fd stderr_fd | ||
in | ||
|
||
let rec timeouts phase = function | ||
| [] -> phase, snd (Unix.waitpid [] pid) | ||
| (t, signal, phase') :: ts -> | ||
match wait_pid_timeout t pid with | ||
| Some status -> phase, status | ||
| None -> | ||
Unix.kill pid signal; | ||
timeouts phase' ts | ||
in | ||
let phase, status = | ||
timeouts Completed @@ | ||
match timeout_desc with | ||
| None -> [] | ||
| Some { duration; signal; kill_after = None } -> | ||
[(duration, signal, TimedOut)] | ||
| Some { duration; signal; kill_after = Some t } -> | ||
[(duration, signal, TimedOut); (t, Sys.sigkill, Killed)] | ||
in | ||
let elapsed = Mtime_clock.count t0 in | ||
phase, status, elapsed | ||
in | ||
|
||
let input_all path = | ||
let< ch = In_channel.with_open_text path in | ||
In_channel.input_all ch | ||
in | ||
let stdout = input_all stdout_path in | ||
let stderr = input_all stderr_path in | ||
|
||
{ phase; status; stdout; stderr; elapsed } |
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,22 @@ | ||
(** More pretty-printing combinators. *) | ||
|
||
let pp_of pp f : _ Fmt.t = fun ppf x -> pp ppf @@ f x | ||
|
||
let pp_repeat ?(sep = Fmt.nop) n0 (pp : 'a Fmt.t) : 'a Fmt.t = fun ppf x -> | ||
let rec loop n = | ||
if n = 0 then () | ||
else if n = 1 then pp ppf x | ||
else (pp ppf x; sep ppf (); loop (n - 1)) | ||
in | ||
loop n0 | ||
|
||
let pp_text ?(line = Fmt.string) : string Fmt.t = | ||
let open Fmt in | ||
vbox @@ pp_of (list line) (String.split_on_char '\n') | ||
|
||
let pp_flow ?(word = Fmt.string) : string Fmt.t = | ||
let open Fmt in | ||
pp_text ~line:(box @@ pp_of (list ~sep:sp word) (String.split_on_char ' ')) | ||
|
||
let pp_text_ = pp_text ?line:None | ||
let pp_flow_ = pp_flow ?word:None |
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
Oops, something went wrong.