Processes#
libuv offers cross-platform process management, which is exposed in Luv.Process.
Spawning child processes#
To spawn a process, use Luv.Process.spawn:
1let () =
2 ignore (Luv.Process.spawn "sleep" ["sleep"; "1"]);
3 ignore (Luv.Loop.run () : bool)
The reason this mysteriously short program waits for the child sleep
process
to exit is that Luv.Process.spawn creates
a Luv.Process.t handle and registers it with the
event loop, which we then run with Luv.Loop.run. It is Luv.Loop.run
that waits. The process handle is also returned to us, but, in this example, we
call ignore
on it on line 2.
Luv.Process.spawn has a large number of optional arguments for controlling the child process’ working directory, environment variables, and so on. Three of the optional arguments deserve some extra attention:
?on_exit
, if supplied, is called when the process terminates.?detached:true
makes the child process fully independent of the parent; in particular, the parent can exit without killing the child.?redirect
is the subject of the next section.
Child process I/O#
By default, the standard file descriptors of a child process all point to
/dev/null
, or NUL
on Windows. You can use the ?redirect
argument of
Luv.Process.spawn to change this.
For example, to share the parent’s STDOUT
with the child:
1let () =
2 let redirect = Luv.Process.[
3 inherit_fd ~fd:stdout ~from_parent_fd:stdout ()
4 ]
5 in
6 ignore (Luv.Process.spawn ~redirect "echo" ["echo"; "Hello,"; "world!"]);
7 ignore (Luv.Loop.run () : bool)
A variant of Luv.Process.inherit_fd is Luv.Process.inherit_stream. It does the same thing, but the function, rather than taking a raw file descriptor, extracts a file descriptor from a libuv stream you already have in the parent (for example, a TCP socket or a pipe).
The alternative to this is to connect a file descriptor in the child to a pipe in the parent. The difference between having a child inherit a pipe, and creating a pipe between parent and child, is, of course, that…
When inheriting a pipe, the read end in the child is the same as the read end in the parent, and likewise for the write end, and the communication is typically with some third party.
When creating a pipe for parent-child communication, the read end in the child is connected to the write end in the parent, and vice versa.
For this, you need to first create a Luv.Pipe.t, which is a kind of libuv stream. So, reading from and writing to it are done the same way as for TCP sockets.
1let () =
2 let pipe = Luv.Pipe.init () |> Result.get_ok in
3
4 let redirect = Luv.Process.[
5 to_parent_pipe ~fd:stdout ~parent_pipe:pipe ()
6 ]
7 in
8 ignore (Luv.Process.spawn ~redirect "echo" ["echo"; "Hello,"; "world!"]);
9
10 Luv.Stream.read_start pipe begin function
11 | Error `EOF ->
12 Luv.Handle.close pipe ignore;
13 | Error e ->
14 Printf.eprintf "Read error: %s\n" (Luv.Error.strerror e);
15 Luv.Handle.close pipe ignore
16 | Ok buffer ->
17 print_string (Luv.Buffer.to_string buffer)
18 end;
19
20 ignore (Luv.Loop.run () : bool)
IPC#
Luv.Pipe can also be used for general IPC between any two processes. For this, a server pipe has to be assigned a name using Luv.Pipe.bind, and the client connects with Luv.Pipe.connect. After that, the flow is largely the same as for TCP sockets. Indeed, if you compare the examples below to the ones at section TCP, you will see that only the first few lines of initialization are different. Almost everything else is the same, because both pipes and TCP sockets are libuv streams.
1let () =
2 let server = Luv.Pipe.init () |> Result.get_ok in
3 ignore (Luv.Pipe.bind server "echo-pipe");
4
5 Luv.Stream.listen server begin function
6 | Error e ->
7 Printf.eprintf "Listen error: %s\n" (Luv.Error.strerror e)
8 | Ok () ->
9 let client = Luv.Pipe.init () |> Result.get_ok in
10
11 match Luv.Stream.accept ~server ~client with
12 | Error _ ->
13 Luv.Handle.close client ignore
14 | Ok () ->
15
16 Luv.Stream.read_start client begin function
17 | Error `EOF ->
18 Luv.Handle.close client ignore
19 | Error e ->
20 Printf.eprintf "Read error: %s\n" (Luv.Error.strerror e);
21 Luv.Handle.close client ignore
22 | Ok buffer ->
23 Luv.Stream.write client [buffer] (fun _ -> ignore)
24 end
25 end;
26
27 ignore (Luv.Loop.run () : bool)
1let () =
2 let client = Luv.Pipe.init () |> Result.get_ok in
3
4 Luv.Pipe.connect client "echo-pipe" begin function
5 | Error e ->
6 Printf.eprintf "Connect error: %s\n" (Luv.Error.strerror e)
7 | Ok () ->
8 let message = Luv.Buffer.from_string "Hello, world!" in
9 Luv.Stream.write client [message] (fun _result _bytes_written ->
10 Luv.Stream.shutdown client ignore);
11
12 Luv.Stream.read_start client begin function
13 | Error `EOF ->
14 Luv.Handle.close client ignore
15 | Error e ->
16 Printf.eprintf "Read error: %s\n" (Luv.Error.strerror e);
17 Luv.Handle.close client ignore
18 | Ok response ->
19 print_endline (Luv.Buffer.to_string response)
20 end
21 end;
22
23 ignore (Luv.Loop.run () : bool)
You can run the IPC server and client together with:
dune exec example/pipe_echo_server.exe &
dune exec example/pipe_hello_world.exe
killall pipe_echo_server.exe
Signals#
Despite its frightening name, Luv.Process.kill only sends a signal to a given process:
1let () =
2 let child =
3 Luv.Process.spawn "sleep" ["sleep"; "10"] |> Result.get_ok in
4
5 ignore (Luv.Process.kill child Luv.Signal.sigkill);
6
7 ignore (Luv.Loop.run () : bool)
Use Luv.Signal to receive signals:
1let () =
2 let handle = Luv.Signal.init () |> Result.get_ok in
3
4 ignore @@ Luv.Signal.start handle Luv.Signal.sigint begin fun () ->
5 Luv.Handle.close handle ignore;
6 print_endline "Exiting"
7 end;
8
9 print_endline "Type Ctrl+C to continue...";
10
11 ignore (Luv.Loop.run () : bool)