Processes

libuv offers cross-platform process management, which is exposed in Luv.Process.

Spawning child processes

To spawn a process, use Luv.Process.spawn:

spawn.ml

1
2
3
let () =
  ignore (Luv.Process.spawn "sleep" ["sleep"; "1"]);
  ignore (Luv.Loop.run ())

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:

stdout.ml

1
2
3
4
5
6
7
let () =
  let redirect = Luv.Process.[
    inherit_fd ~fd:stdout ~from_parent_fd:stdout ()
  ]
  in
  ignore (Luv.Process.spawn ~redirect "echo" ["echo"; "Hello,"; "world!"]);
  ignore (Luv.Loop.run ())

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.

pipe.ml

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
let () =
  let pipe = Luv.Pipe.init () |> Stdlib.Result.get_ok in

  let redirect = Luv.Process.[
    to_parent_pipe ~fd:stdout ~parent_pipe:pipe ()
  ]
  in
  ignore (Luv.Process.spawn ~redirect "echo" ["echo"; "Hello,"; "world!"]);

  Luv.Stream.read_start pipe begin function
    | Error `EOF ->
      Luv.Handle.close pipe ignore;
    | Error e ->
      Printf.eprintf "Read error: %s\n" (Luv.Error.strerror e);
      Luv.Handle.close pipe ignore
    | Ok buffer ->
      print_string (Luv.Buffer.to_string buffer)
  end;

  ignore (Luv.Loop.run ())

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.

pipe_echo_server.ml

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
let () =
  let server = Luv.Pipe.init () |> Stdlib.Result.get_ok in
  ignore (Luv.Pipe.bind server "echo-pipe");

  Luv.Stream.listen server begin function
    | Error e ->
      Printf.eprintf "Listen error: %s\n" (Luv.Error.strerror e)
    | Ok () ->
      let client = Luv.Pipe.init () |> Stdlib.Result.get_ok in

      match Luv.Stream.accept ~server ~client with
      | Error _ ->
        Luv.Handle.close client ignore
      | Ok () ->

        Luv.Stream.read_start client begin function
          | Error `EOF ->
            Luv.Handle.close client ignore
          | Error e ->
            Printf.eprintf "Read error: %s\n" (Luv.Error.strerror e);
            Luv.Handle.close client ignore
          | Ok buffer ->
            Luv.Stream.write client [buffer] (fun _ -> ignore)
        end
  end;

  ignore (Luv.Loop.run ())

pipe_hello_world.ml

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
let () =
  let client = Luv.Pipe.init () |> Stdlib.Result.get_ok in

  Luv.Pipe.connect client "echo-pipe" begin function
    | Error e ->
      Printf.eprintf "Connect error: %s\n" (Luv.Error.strerror e)
    | Ok () ->
      let message = Luv.Buffer.from_string "Hello, world!" in
      Luv.Stream.write client [message] (fun _result _bytes_written ->
        Luv.Stream.shutdown client ignore);

      Luv.Stream.read_start client begin function
        | Error `EOF ->
          Luv.Handle.close client ignore
        | Error e ->
          Printf.eprintf "Read error: %s\n" (Luv.Error.strerror e);
          Luv.Handle.close client ignore
        | Ok response ->
          print_endline (Luv.Buffer.to_string response)
      end
  end;

  ignore (Luv.Loop.run ())

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:

send_signal.ml

1
2
3
4
5
6
7
let () =
  let child =
    Luv.Process.spawn "sleep" ["sleep"; "10"] |> Stdlib.Result.get_ok in

  ignore (Luv.Process.kill child Luv.Signal.sigkill);

  ignore (Luv.Loop.run ())

Use Luv.Signal to receive signals:

sigint.ml

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
let () =
  let handle = Luv.Signal.init () |> Stdlib.Result.get_ok in

  ignore @@ Luv.Signal.start handle Luv.Signal.sigint begin fun () ->
    Luv.Handle.close handle ignore;
    print_endline "Exiting"
  end;

  print_endline "Type Ctrl+C to continue...";

  ignore (Luv.Loop.run ())