|
1 | | -(* |
2 | | - * Extc : C common OCaml bindings |
3 | | - * Copyright (c)2004-2015 Nicolas Cannasse |
4 | | - * |
5 | | - * This program is free software; you can redistribute it and/or modify |
6 | | - * it under the terms of the GNU General Public License as published by |
7 | | - * the Free Software Foundation; either version 2 of the License, or |
8 | | - * (at your option) any later version. |
9 | | - * |
10 | | - * This program is distributed in the hope that it will be useful, |
11 | | - * but WITHOUT ANY WARRANTY; without even the implied warranty of |
12 | | - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
13 | | - * GNU General Public License for more details. |
14 | | - * |
15 | | - * You should have received a copy of the GNU General Public License |
16 | | - * along with this program; if not, write to the Free Software |
17 | | - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA |
18 | | - *) |
19 | | - |
20 | | -type process |
21 | | - |
22 | | -external run : string -> string array option -> process = "process_run" |
23 | | -external read_stdout : process -> string -> int -> int -> int = "process_stdout_read" |
24 | | -external read_stderr : process -> string -> int -> int -> int = "process_stderr_read" |
25 | | -external write_stdin : process -> string -> int -> int -> int = "process_stdin_write" |
26 | | -external close_stdin : process -> unit = "process_stdin_close" |
27 | | -external exit : process -> int = "process_exit" |
28 | | -external pid : process -> int = "process_pid" |
29 | | -external close : process -> unit = "process_close" |
30 | | -external kill : process -> unit = "process_kill" |
| 1 | +(** Pure OCaml implementation of subprocess management. |
31 | 2 |
|
| 3 | + Replaces the former C stubs (process_stubs.c) which were unsafe under |
| 4 | + OCaml 5 multi-domain execution (missing [caml_enter_blocking_section], |
| 5 | + direct [fork()] in multi-threaded processes). |
| 6 | +
|
| 7 | + Uses [Unix.create_process] which is domain-safe and handles |
| 8 | + [posix_spawn] on modern systems. |
| 9 | +
|
| 10 | + When the command is not found, [run] returns a process whose pipes |
| 11 | + immediately return EOF and whose [exit] returns code 127 (matching |
| 12 | + the POSIX shell convention and the old fork+exec behavior where |
| 13 | + fork always succeeded). *) |
| 14 | + |
| 15 | +type process = { |
| 16 | + pid : int; |
| 17 | + stdin_fd : Unix.file_descr; |
| 18 | + stdout_fd : Unix.file_descr; |
| 19 | + stderr_fd : Unix.file_descr; |
| 20 | + mutable exit_code : int option; |
| 21 | +} |
| 22 | + |
| 23 | +(** Returns a readable file_descr that immediately yields EOF. *) |
| 24 | +let make_eof_fd () = |
| 25 | + let (r, w) = Unix.pipe ~cloexec:true () in |
| 26 | + Unix.close w; |
| 27 | + r |
| 28 | + |
| 29 | +(** Returns a writable file_descr where writes fail with EPIPE. *) |
| 30 | +let make_null_fd () = |
| 31 | + let (r, w) = Unix.pipe ~cloexec:true () in |
| 32 | + Unix.close r; |
| 33 | + w |
| 34 | + |
| 35 | +let unix_error_msg err fn arg = |
| 36 | + Printf.sprintf "%s(%s): %s" fn arg (Unix.error_message err) |
| 37 | + |
| 38 | +let run cmd args = |
| 39 | + let (child_stdin_r, child_stdin_w) = Unix.pipe ~cloexec:true () in |
| 40 | + let (child_stdout_r, child_stdout_w) = Unix.pipe ~cloexec:true () in |
| 41 | + let (child_stderr_r, child_stderr_w) = Unix.pipe ~cloexec:true () in |
| 42 | + let shell, argv = match args with |
| 43 | + | None -> |
| 44 | + if Sys.win32 then |
| 45 | + let comspec = try Sys.getenv "COMSPEC" with Not_found -> "cmd.exe" in |
| 46 | + comspec, [|comspec; "/C"; cmd|] |
| 47 | + else |
| 48 | + "/bin/sh", [|"/bin/sh"; "-c"; cmd|] |
| 49 | + | Some a -> |
| 50 | + cmd, Array.append [|cmd|] a |
| 51 | + in |
| 52 | + match |
| 53 | + try Ok (Unix.create_process shell argv child_stdin_r child_stdout_w child_stderr_w) |
| 54 | + with Unix.Unix_error _ as e -> Error e |
| 55 | + with |
| 56 | + | Ok pid -> |
| 57 | + Unix.close child_stdin_r; |
| 58 | + Unix.close child_stdout_w; |
| 59 | + Unix.close child_stderr_w; |
| 60 | + { pid; stdin_fd = child_stdin_w; stdout_fd = child_stdout_r; stderr_fd = child_stderr_r; exit_code = None } |
| 61 | + | Error err -> |
| 62 | + Unix.close child_stdin_r; |
| 63 | + Unix.close child_stdin_w; |
| 64 | + Unix.close child_stdout_r; |
| 65 | + Unix.close child_stdout_w; |
| 66 | + Unix.close child_stderr_r; |
| 67 | + let stderr_r = match err with |
| 68 | + | (Unix.Unix_error (err, fn, arg)) -> |
| 69 | + (* Process creation failed (e.g. command not found). |
| 70 | + Match the old fork+exec behavior: return a process whose pipes |
| 71 | + immediately return EOF and whose exit code is 127. |
| 72 | + Write the error message to the stderr pipe so callers can read it. *) |
| 73 | + let errmsg = unix_error_msg err fn arg ^ "\n" in |
| 74 | + let (stderr_r, stderr_w) = Unix.pipe ~cloexec:true () in |
| 75 | + begin try |
| 76 | + ignore (Unix.write_substring stderr_w errmsg 0 (String.length errmsg)) |
| 77 | + with Unix.Unix_error _ -> |
| 78 | + () |
| 79 | + end; |
| 80 | + Unix.close stderr_w; |
| 81 | + stderr_r |
| 82 | + | _ -> |
| 83 | + Unix.close child_stderr_w; |
| 84 | + make_eof_fd () |
| 85 | + in |
| 86 | + { pid = 0; stdin_fd = make_null_fd (); stdout_fd = make_eof_fd (); stderr_fd = stderr_r; exit_code = Some 127 } |
| 87 | + |
| 88 | +let read_stdout p buf pos len = |
| 89 | + let n = try |
| 90 | + Unix.read p.stdout_fd (Bytes.unsafe_of_string buf) pos len |
| 91 | + with Unix.Unix_error (err, fn, arg) -> |
| 92 | + failwith (unix_error_msg err fn arg) |
| 93 | + in |
| 94 | + if n = 0 then failwith "process_stdout_read"; |
| 95 | + n |
| 96 | + |
| 97 | +let read_stderr p buf pos len = |
| 98 | + let n = try |
| 99 | + Unix.read p.stderr_fd (Bytes.unsafe_of_string buf) pos len |
| 100 | + with Unix.Unix_error (err, fn, arg) -> |
| 101 | + failwith (unix_error_msg err fn arg) |
| 102 | + in |
| 103 | + if n = 0 then failwith "process_stderr_read"; |
| 104 | + n |
| 105 | + |
| 106 | +let write_stdin p buf pos len = |
| 107 | + try Unix.write_substring p.stdin_fd buf pos len |
| 108 | + with Unix.Unix_error (err, fn, arg) -> failwith (unix_error_msg err fn arg) |
| 109 | + |
| 110 | +let close_stdin p = |
| 111 | + try Unix.close p.stdin_fd |
| 112 | + with Unix.Unix_error (err, fn, arg) -> failwith (unix_error_msg err fn arg) |
| 113 | + |
| 114 | +let exit p = |
| 115 | + match p.exit_code with |
| 116 | + | Some c -> c |
| 117 | + | None -> |
| 118 | + let _, status = Unix.waitpid [] p.pid in |
| 119 | + let c = match status with |
| 120 | + | Unix.WEXITED c -> c |
| 121 | + | Unix.WSIGNALED c -> c |
| 122 | + | Unix.WSTOPPED c -> c |
| 123 | + in |
| 124 | + p.exit_code <- Some c; |
| 125 | + c |
| 126 | + |
| 127 | +let pid p = p.pid |
| 128 | + |
| 129 | +let close p = |
| 130 | + (try Unix.close p.stdout_fd with Unix.Unix_error _ -> ()); |
| 131 | + (try Unix.close p.stderr_fd with Unix.Unix_error _ -> ()); |
| 132 | + (try Unix.close p.stdin_fd with Unix.Unix_error _ -> ()) |
| 133 | + |
| 134 | +let kill p = |
| 135 | + if p.exit_code = None && p.pid > 0 then |
| 136 | + (try Unix.kill p.pid Sys.sigkill with Unix.Unix_error _ -> ()) |
0 commit comments