Skip to content

Instantly share code, notes, and snippets.

@jtpaasch
Created July 5, 2018 14:28
Show Gist options
  • Save jtpaasch/025a76fd6a62ce654a6f8bcb1a51a54a to your computer and use it in GitHub Desktop.
Save jtpaasch/025a76fd6a62ce654a6f8bcb1a51a54a to your computer and use it in GitHub Desktop.
Notes/tutorial on systems programming in OCaml.

OCaml Systems Programming

Including the unix module

To compile

Compile your modules first, e.g.:

ocamlc -c mod1.ml
ocamlc -c mod2.ml

Then, when you link, list unix.cma first:

ocamlc -o prog unix.cma mod1.cmo mod2.cmo

If you use the native code compiler, compile the modules first:

ocamlopt -c mod1.ml
ocamlopt -c mod2.ml

Then, when you link, list unix.cmxa first:

ocamlopt -o prog unix.cmxa mod1.cmx mod2.cmx

If you use ocamlbuild, add this to your _tags file:

<prog.{native,byte}> : use_unix

In the toplevel

In a REPL:

#load "unix.cma";;

Make a REPL with unix.cma preloaded:

ocamlmktop -o ocamlunix unix.cma

Start it:

./ocamlunix

Reading arguments

Command line arguments are stored in Sys.argv.

In a file main.ml, get the number of arguments:

let print_num_args () =
  let len = Array.length Sys.argv in
  print_endline (string_of_int len)

let () = print_num_args ()

Try it:

ocaml main.ml
ocaml main.ml a b c

Or, print all the arguments:

let print_args () =
  let len = Array.length Sys.argv in
  for i = 0 to len - 1 do
     print_endline Sys.argv.(i);
  done

let () = print_args ()

Try it:

ocaml main.ml
ocaml main.ml a b c

Sys.argv.(0) is the program itself. Print it, and then the arguments passed to it:

let print_invocation () =
  let len = Array.length Sys.argv in
  let exe = Sys.argv.(0) in
  Printf.printf "Program: '%s'.\n" exe;
  if len > 1 then
    begin
      print_endline "Invoked with arguments:";
      for i = 1 to len - 1 do
        Printf.printf "- '%s'\n" Sys.argv.(i);
      done
    end

let () = print_invocation ()

Terminating

Use the exit int function to exit with an exit code.

For instance, to exit with code 2:

let exit_badly () =
  exit 2

let () = exit_badly ()

Try it:

ocaml main.ml
echo $?

The exit function always flushes all open buffers.

If you want a function to be called when exit calls, register it with the at_exit.

let report_shutdown () =
  print_endline "Shutting down."

let exit_badly () =
  exit 2

let () =
  at_exit report_shutdown;
  exit_badly ()

You can register multiple functions. They are executed last-registered first.

let report_status () =
  print_endline "Things are not good, Jim."

let report_shutdown () =
  print_endline "Shutting down."

let exit_badly () =
  exit 2

let () =
  at_exit report_shutdown;
  at_exit report_status;
  exit_badly ()

Handle exceptions

Use handle_unix_error to run a program, and if unix errors are caught, display the error and exit. For example:

let list_root_files () =
  raise (Unix.Unix_error (Unix.EACCES, "ls -la", "/root"))

let () =
  Unix.handle_unix_error list_root_files ()

The handle_unix_error function executes list_root_files. If a unix exception is caught, it will display the error and exit the program nicely. In the function list_root_files, it does indeed raise a "permission denied" error.

Try it to see the error it displays:

ocaml -c main.ml
ocaml -o main.byte unix.cma main.cmo
./main.byte

If you like, you can remove the Unix prefixes, and instead open the Unix module at the top of the file:

open Unix

let list_root_files () =
  raise (Unix_error (EACCES, "ls -la", "/root"))

let () =
  handle_unix_error list_root_files ()

Starting process with open_process*

Here are a variety of programs that start processes and read their output, using the Unix.open_process* functions.

This reads stdout:

let print_stat s =
  match s with
  | Unix.WEXITED r -> Printf.printf "Exited with %d\n" r
  | Unix.WSIGNALED r -> Printf.printf "Signaled with %d\n" r
  | Unix.WSTOPPED r -> Printf.printf "Stopped with %d\n" r

let syscall cmd =
  let ic = Unix.open_process_in cmd in
  let buf = Buffer.create 32 in
  ignore (try
    while true do
      Buffer.add_channel buf ic 1
    done
  with End_of_file -> ());
  let s = Unix.close_process_in ic in
  print_stat s;
  Buffer.contents buf

let () =
  let cmd = Sys.argv.(1) in
  let out = syscall cmd in
  print_endline out

This does the same, but it returns both the exit code and stdout:

let stat_code s =
  match s with
  | Unix.WEXITED r -> r
  | Unix.WSIGNALED r -> r
  | Unix.WSTOPPED r -> r

let syscall cmd =
  let ic = Unix.open_process_in cmd in
  let buf = Buffer.create 32 in
  ignore (try
    while true do
      Buffer.add_channel buf ic 1
    done
  with End_of_file -> ());
  let s = Unix.close_process_in ic in
  let exit_code = stat_code s in
  (exit_code, Buffer.contents buf)

let () =
  let cmd = Sys.argv.(1) in
  let exit_code, out = syscall cmd in
  print_endline (string_of_int exit_code);
  print_endline out

This is the same, but it adds stderr in too:

let status_code s =
  match s with
  | Unix.WEXITED r -> r
  | Unix.WSIGNALED r -> r
  | Unix.WSTOPPED r -> r

let syscall cmd env =
  let ic, oc, ec = Unix.open_process_full cmd env in
  let out_buf = Buffer.create 32 and
      err_buf = Buffer.create 32 in
  ignore (try
    while true do
      Buffer.add_channel out_buf ic 1
    done
  with End_of_file -> ());
  ignore (try
    while true do
      Buffer.add_channel err_buf ec 1
    done
  with End_of_file -> ());
  let s = Unix.close_process_full (ic, oc, ec) in
  let exit_code = status_code s in
  (exit_code, Buffer.contents out_buf, Buffer.contents err_buf)

let () =
  let cmd = Sys.argv.(1) in
  let env = [|"foo=bar"|] in
  let exit_code, out, err = syscall cmd env in
  print_endline "------ stdout";
  print_endline out;
  print_endline "------ stderr";
  print_endline err;
  print_endline "------ exit code";
  print_endline (string_of_int exit_code)

This is the same as above, but it moves some of the work into smaller functions:

let status_code s =
  match s with
  | Unix.WEXITED r -> r
  | Unix.WSIGNALED r -> r
  | Unix.WSTOPPED r -> r

let append_to_buffer b c =
  Buffer.add_channel b c 1

let read_to_EOF b c =
  try
    while true do
      append_to_buffer b c
    done
  with End_of_file -> ()

let dump_buffer b =
  Buffer.contents b

let syscall cmd env =
  let ic, oc, ec = Unix.open_process_full cmd env in

  let out_buf = Buffer.create 32 and
      err_buf = Buffer.create 32 in

  ignore (read_to_EOF out_buf ic);
  ignore (read_to_EOF err_buf ec);

  let s = Unix.close_process_full (ic, oc, ec) in
  let exit_code = status_code s in
  (exit_code, dump_buffer out_buf, dump_buffer err_buf)

let () =
  let cmd = Sys.argv.(1) in
  let env = [|"foo=bar"|] in
  let exit_code, out, err = syscall cmd env in
  print_endline "------ stdout";
  print_endline out;
  print_endline "------ stderr";
  print_endline err;
  print_endline "------ exit code";
  print_endline (string_of_int exit_code)

A final version of a general process runner using the open_process_full function:

(** Compile with:
    [ocamlc -c simpleproc.ml]
    [ocamlc -o simpleproc.byte unix.cmo simpleproc.cmo]
    Run it:
    [./simpleproc.byte "ls -la"]
    *)

module Buff = struct

  let create = Buffer.create 32
  let dump b = Buffer.contents b
  let read_all b c =
    try
      while true do
        Buffer.add_channel b c 1
      done
    with End_of_file -> ()

end

module Proc = struct

  let popen = Unix.open_process_full
  let pclose = Unix.close_process_full
  let int_of_status s =
    match s with
    | Unix.WEXITED r -> r
    | Unix.WSIGNALED r -> r
    | Unix.WSTOPPED r -> r

end

module Cmd = struct

  let proc cmd env out_buf err_buf =
    let out_ch, in_ch, err_ch = Proc.popen cmd env in
    ignore (Buff.read_all out_buf out_ch);
    ignore (Buff.read_all err_buf err_ch);
    let s = Proc.pclose (out_ch, in_ch, err_ch) in
    let c = Proc.int_of_status s in
    (c, Buff.dump out_buf, Buff.dump err_buf)

end

let main () =
  let cmd = Sys.argv.(1) in
  let env = [|"foo=bar"|] in
  let out_buf = Buffer.create 32 and
      err_buf = Buffer.create 32 in

  let exit_code, out, err = Cmd.proc cmd env out_buf err_buf in

  print_endline "------ stdout";
  print_endline out;
  print_endline "------ stderr";
  print_endline err;
  print_endline "------ exit code";
  print_endline (string_of_int exit_code)

let () = Unix.handle_unix_error main ()

Forking

Create a file called forking.ml:

let pid = Unix.fork () in

if pid = 0 then (* child process *)
  begin
    Printf.printf "Child pid: %d\n" (Unix.getpid ())
  end

else (* original (parent) process *)
  begin
    Printf.printf "Parent pid: %d\n" (Unix.getpid ());
    exit 0
  end

Fork copies the whole file into a child process. The child's pid is 0. Try it:

ocamlc -c forking.ml
ocamlc -o forking.byte unix.cma forking.cmo
./forking.byte

We can do the same with matching:

let pid = Unix.fork () in
  match pid with
  | 0 -> Printf.printf "Child pid: %d\n" (Unix.getpid ())
  | -1 -> Printf.printf "Error forking.\n"
  | _ -> Printf.printf "Parent pid: %d\n" (Unix.getpid ())

Try it:

ocamlc -c forking.ml
ocamlc -o forking.byte unix.cma forking.cmo
./forking.byte

Fork and wait

Create a file forkandwait.ml:

let int_of_status s =
  match s with
  | Unix.WEXITED n -> n
  | Unix.WSIGNALED n -> n
  | Unix.WSTOPPED n -> n
in

let pid = Unix.fork () in
  match pid with
  | 0 ->
    begin
      Printf.printf "Child pid: %d\n" (Unix.getpid ());
      Unix.sleep 2;
      exit 5
    end
  | -1 ->
    begin
      Printf.printf "Error forking.\n"
    end
  | _ ->
    begin
      Printf.printf "Parent pid: %d\n" (Unix.getpid ());
      let child_pid, status = Unix.wait () in
        Printf.printf "Child %d exited with code %d\n"
          child_pid (int_of_status status)
    end

Try it:

ocamlc -c forkandwait.ml
ocamlc -o forkandwait.byte unix.cma forkandwait.cmo
./forkandwaait.byte

Here's another version:

let () =
  match Unix.fork () with
    | -1 ->
        Printf.printf "Error forking\n"
    | 0 ->
        Printf.printf "Going to sleep\n";
        flush stdout;
        Unix.execv "/bin/sleep" [| "/bin/sleep"; "3" |]
    | pid ->
        let running = ref true in
        while !running do
          try
            begin
              ignore (Unix.waitpid [] pid);
              running := false;
              Printf.printf "Not running anymore.\n";
              flush stdout
            end
          with Unix.Unix_error _ -> ()
        done;

Use Unix.system:

Run a command with a shell (/bin/sh or cmd.exe):

let int_of_status s =
  match s with
  | Unix.WEXITED n -> n
  | Unix.WSIGNALED n -> n
  | Unix.WSTOPPED n -> n

let () =
  let cmd = "echo running cmd && sleep 2 && echo done" in
  let s = Unix.system cmd in
  Printf.printf "Exit code: %d\n" (int_of_status s)

You can run the system call in a separate process:

let int_of_status s =
  match s with
  | Unix.WEXITED n -> n
  | Unix.WSIGNALED n -> n
  | Unix.WSTOPPED n -> n

let () =
  Printf.printf "Starting 'sleep 5'...\n"; flush stdout;
  let pid = Unix.fork () in
  match pid with
  | -1 -> Printf.printf "Error forking.\n"
  | 0 ->
    Printf.printf "Child pid: %d\n" (Unix.getpid ()); flush stdout;
    let s = Unix.system "echo runnin && sleep 20 && echo boo" in
    Printf.printf "Done with code: %d\n" (int_of_status s)
  | cpid ->
    Printf.printf "Parent pid: %d - Child id: %d\n" (Unix.getpid ()) cpid;
    flush stdout;
    let cpid', s = Unix.waitpid [] (-1) in
    Printf.printf "Child %d exited with code %d\n" cpid' (int_of_status s)

You could also use one of the execv* functions:

let int_of_status s =
  match s with
  | Unix.WEXITED n -> n
  | Unix.WSIGNALED n -> n
  | Unix.WSTOPPED n -> n

let () =
  Printf.printf "Starting 'sleep 5'...\n"; flush stdout;
  let pid = Unix.fork () in
  match pid with
  | -1 -> Printf.printf "Error forking.\n"
  | 0 ->
    Printf.printf "Child pid: %d\n" (Unix.getpid ()); flush stdout;
    let cmd = "echo running && sleep 10 && echo done" in
    Unix.execvp "bash" [| "bash"; "-c"; cmd |]  
  | cpid ->
    Printf.printf "Parent pid: %d - Child id: %d\n" (Unix.getpid ()) cpid;
    flush stdout;
    let cpid', s = Unix.waitpid [] (-1) in
    Printf.printf "Child %d exited with code %d\n" cpid' (int_of_status s)

Note: if you run ps ax while either of the last two programs are running, you'll notice that in

Use create_process to get a PID.

Create a process with create_process, then use waitpid [] pid to wait for it to finish:

let int_of_status s =
  match s with
  | Unix.WEXITED n -> n
  | Unix.WSIGNALED n -> n
  | Unix.WSTOPPED n -> n

let check_exit pid =
  let cpid, s = Unix.waitpid [] pid in
  s

let () =
  Printf.printf "Starting 'sleep 3'...\n"; flush stdout;
  let pid = Unix.create_process
    "sleep" [| "sleep"; "3" |]
    Unix.stdin Unix.stdout Unix.stderr
  in
  Printf.printf "Pid: %d\n" pid; flush stdout;
  let s = check_exit pid in
  Printf.printf "Exit code: %d\n" (int_of_status s)

If you don't want to block, we can use Unix.WNOHANG, and recursively check the PID:

let int_of_status s =
  match s with
  | Unix.WEXITED n -> n
  | Unix.WSIGNALED n -> n
  | Unix.WSTOPPED n -> n

let rec check_exit pid =
  let cpid, s = Unix.waitpid [Unix.WNOHANG] pid in
  match cpid with
  | 0 -> (
    Printf.printf "...\n"; flush stdout;
    Unix.sleepf 0.5; check_exit pid)
  | n -> s

let () =
  Printf.printf "Starting 'sleep 3'...\n"; flush stdout;
  let pid = Unix.create_process
    "sleep" [| "sleep"; "3" |]
    Unix.stdin Unix.stdout Unix.stderr
  in
  Printf.printf "Pid: %d\n" pid; flush stdout;
  let s = check_exit pid in
  Printf.printf "Exit code: %d\n" (int_of_status s)

The classic fork-and-run pattern

A simple example. Make a file called ps.ml:

let ps cmd =
  match Unix.fork() with
  | 0 -> Unix.execv "/bin/sh" [| "/bin/sh"; "-c"; cmd |]
  | child_pid -> child_pid

let () =
  let pid = ps "echo running && sleep 5 && echo done now" in
  Unix.waitpid [] pid

Here we define a function called ps that takes a command (as a string). For instance, you would invoke this like so:

ps "ls -la"

In the body of the function, it forks. In the child process, it uses execv to execute /bin/sh -c cmd. In the parent process, it simply returns the child's pid.

In the main function at the bottom, we use the ps function to call echo running && sleep 5 && echo done now. This returns the child's pid. Then we use Unix.waitpid [] pid to wait until that pid is finished.

Compile and run it (you'll get a warning when you compile it, but that's okay for now):

ocamlc -c ps.ml
ocamlc -o ps.byte unix.cma ps.cmo
./ps.byte

Let's add some printing so we can see what's going on.

let ps cmd =
  Printf.printf "Running '%s'\n" cmd; flush stdout;
  match Unix.fork() with
  | 0 ->
    begin
      let parent_pid = Unix.getppid () in
      let child_pid = Unix.getpid () in
      Printf.printf "Child pid: %d - Parent pid: %d\n" child_pid parent_pid;    
      flush stdout;                                                             
      Unix.execv "/bin/sh" [| "/bin/sh"; "-c"; cmd |]
    end
  | child_pid ->
    begin
      let parent_pid = Unix.getpid () in
      Printf.printf "Parent pid: %d - Child pid: %d\n" parent_pid child_pid;
      flush stdout;
    end;
    child_pid

let () =
  let main_pid = Unix.getpid () in
  Printf.printf "Starting main pid: %d\n" main_pid;
  flush stdout;
  let pid = ps "echo running && sleep 5 && echo done now" in
  Printf.printf "Spawned process id: %d\n" pid;
  Printf.printf "Waiting for it to stop...\n";
  flush stdout;
  let pid', s = Unix.waitpid [] pid in
  Printf.printf "Exit code: %d\n" (int_of_status s);
  Printf.printf "Done\n"

The messages we print out show that there are two processes: the parent and child. The parent is the OCaml program itself, which spawns a child. The child then runs a command with execv, and that command actually takes over the whole child process. The child process simply becomes the bin/sh -c ... command.

You can confirm this by executing ps.byte, and then running ps ax. You can look at the pids printed out by your OCaml program, and then in the ps ax output, you'll see that the parent is ps.byte, and the child is /bin/sh -c ....

Handling exceptions

What happens if the attempt to execute the child fails? Let's try it. Here we try to execute /bin/shz rather than /bin/sh, which should throw an error since /bin/shz does not exist.

let int_of_status s =
  match s with
  | Unix.WEXITED n -> n
  | Unix.WSIGNALED n -> n
  | Unix.WSTOPPED n -> n

let ps cmd =
  match Unix.fork() with
  | 0 -> Unix.execv "/bin/shz" [| "/bin/shz"; "-c"; cmd |]
  | child_pid -> child_pid

let main () =
  let pid = ps "echo running && sleep 5 && echo done now" in
  let pid', s = Unix.waitpid [] pid in
  Printf.printf "Child %d exit code: %d\n" pid' (int_of_status s)

let () = main ()

If you run that, you'll see it throw an error which says that execv failed on /bin/shz because there is no such /bin/shz.

This can be a useful message. We can use the Unix.handle_unix_error function to catch and pretty-print it:

let int_of_status s =
  match s with
  | Unix.WEXITED n -> n
  | Unix.WSIGNALED n -> n
  | Unix.WSTOPPED n -> n

let ps cmd =
  match Unix.fork() with
  | 0 -> Unix.execv "/bin/shz" [| "/bin/shz"; "-c"; cmd |]
  | child_pid -> child_pid

let main () =
  let pid = ps "echo running && sleep 5 && echo done now" in
  let pid', s = Unix.waitpid [] pid in
  Printf.printf "Child %d exit code: %d\n" pid' (int_of_status s)

let () = Unix.handle_unix_error main ()

We might also wish to catch the error in the child, and simply exit with a 127. This is actually what OCaml itself does with most of the process functions (system, create_process, etc). If you use any of those to run a command, and it fails to run that command, OCaml will catch it and have the child process exit with code 127.

Here is how we can do that:

let int_of_status s =
  match s with
  | Unix.WEXITED n -> n
  | Unix.WSIGNALED n -> n
  | Unix.WSTOPPED n -> n

let ps cmd =
  match Unix.fork() with
  | 0 ->
    begin try
      Unix.execv "/bin/shz" [| "/bin/shz"; "-c"; cmd |]
    with _ -> exit 127
    end
  | child_pid -> child_pid

let main () =
  let pid = ps "echo running && sleep 5 && echo done now" in
  let pid', s = Unix.waitpid [] pid in
  Printf.printf "Child %d exit code: %d\n" pid' (int_of_status s)

let () = Unix.handle_unix_error main ()

If you run that, you'll that no unix error is thrown. Instead, the child simply exits with code 127.

Exiting with 127 is fairly standard behavior. Sometimes it may be more useful to display the actual error message though. It depends on the context.

Let's rename ps to pssh, and let's add an optional argument for the shell executable:

let int_of_status s =
  match s with
  | Unix.WEXITED n -> n
  | Unix.WSIGNALED n -> n
  | Unix.WSTOPPED n -> n

let psshell ?exe:(exe="/bin/sh") cmd =
  match Unix.fork() with
  | 0 -> Unix.execvp exe [| exe; "-c"; cmd |]
  | child_pid -> child_pid

let main () =
  let pid = psshell ~exe:("/bin/bash") "ls -la" in
  let pid', s = Unix.waitpid [] pid in
  Printf.printf "Child %d exit code: %d\n" pid' (int_of_status s)

let () = Unix.handle_unix_error main ()

This way, you can specify which shell you want to execute the command with.

Redirecting output

Create a file called redirect.ml:

let () =
  let cmd = "echo boo" in
  Unix.execvp "/bin/sh" [| "/bin/sh"; "-c"; cmd |]

This simple runs the shell command echo boo.

Compile it, link it, and run it:

ocamlc -c redirect.ml
ocamlc -o redirect.byte unix.cma redirect.cmo
./redirect.byte

The program prints boo to stdout.

Now let's redirect that output to a file:

let () =
  let cmd = "echo boo" in
  let fd = Unix.openfile "out.log" [O_WRONLY; O_TRUNC; O_CREAT] 0o666 in
  Unix.dup2 fd Unix.stdout;                                                   
  Unix.close fd;                                                              
  Unix.execvp "/bin/sh" [| "/bin/sh"; "-c"; cmd |]

Run this, and the output goes into the out.log file rather than stdout.

Now let's make a pipe. Connect the write end up to stdout (so that anything that gets written to stdout is written to the pipe), and connect a read channel (called an in_channel) to the read end of the pipe:

let () =
  let cmd = "echo boo" in

  let (stdout_read, stdout_write) = Unix.pipe () in
  let stdout_ch = Unix.in_channel_of_descr stdout_read in

  Unix.dup2 stdout_write Unix.stdout;                                           
  Unix.close stdout_write;                                                      

  Unix.execvp "/bin/sh" [| "/bin/sh"; "-c"; cmd |]

Run this, and you'll see no output. The reason is that the shell command writes its stdout to the pipe, and that's where it ends. We don't actually read anything out of that channel.

We can't read anything, in fact, because execvp takes over the program, and never returns. So we have no opportunity to read from the channel.

But we can launch execvp in its own child process, and then read from the parent process.

let int_of_status s =
  match s with
  | Unix.WEXITED n -> n
  | Unix.WSIGNALED n -> n
  | Unix.WSTOPPED n -> n

let shell cmd out =
  match Unix.fork () with
  | 0 ->
    Unix.dup2 out Unix.stdout;                                                  
    Unix.close out;                                                             
    Unix.execvp "/bin/sh" [| "/bin/sh"; "-c"; cmd |]
  | pid -> pid

let () =
  let cmd = "echo boo" in

  let (stdout_read, stdout_write) = Unix.pipe () in
  let stdout_ch = Unix.in_channel_of_descr stdout_read in

  let pid = shell cmd stdout_write in
  let pid', s = Unix.waitpid [] pid in
  Printf.printf "Child %d exit  %d\n" pid' (int_of_status s);

  let first_line = input_line stdout_ch in
  Printf.printf "stdout: %s\n" first_line;

  close_in stdout_ch;

Let's do the same thing, but connect the shell command's stdout to a file:

let int_of_status s =
  match s with
  | Unix.WEXITED n -> n
  | Unix.WSIGNALED n -> n
  | Unix.WSTOPPED n -> n

let shell cmd out =
  match Unix.fork () with
  | 0 ->
    Unix.dup2 out Unix.stdout;                                                  
    Unix.close out;                                                             
    Unix.execvp "/bin/sh" [| "/bin/sh"; "-c"; cmd |]
  | pid -> pid

let () =
  let cmd = "echo boo" in

  let write_file =
    Unix.openfile "out.log" [O_WRONLY; O_TRUNC; O_CREAT] 0o666 in

  let pid = shell cmd write_file in
  let pid', s = Unix.waitpid [] pid in
  Printf.printf "Child %d exit %d\n" pid' (int_of_status s);

Collecting stdout and stderr

let int_of_status s =
  match s with
  | Unix.WEXITED n -> n
  | Unix.WSIGNALED n -> n
  | Unix.WSTOPPED n -> n

let shell cmd out err =
  match Unix.fork () with
  | 0 ->
    Unix.dup2 out Unix.stdout;                                                  
    Unix.close out;                                                             
    Unix.dup2 err Unix.stderr;                                                  
    Unix.close err;                                                             
    Unix.execvp "/bin/sh" [| "/bin/sh"; "-c"; cmd |]
  | pid -> pid

let start_ps cmd =
  let (stdout_read, stdout_write) = Unix.pipe () in
  let (stderr_read, stderr_write) = Unix.pipe () in
  let pid = shell cmd stdout_write stderr_write in
  (pid, stdout_read, stderr_read)

let poll_ps pid =
  let pid', s = Unix.waitpid [Unix.WNOHANG] pid in
  match pid' with
  | 0 -> None
  | _ -> Some (int_of_status s)

let () =
  let cmd = "sleep 1 && echo boo && echo foo > /dev/stderr && exit 3" in

  let pid, stdout_fd, stderr_fd = start_ps cmd in
  Printf.printf "Child %d started.\n%!" pid;

  (let exit_code = poll_ps pid in
  match exit_code with
  | None -> Printf.printf "Child not finished yet.\n%!"
  | Some n -> Printf.printf "Exit code: %d\n%!" n);

  Printf.printf "Sleeping\n%!";
  Unix.sleep 1;

  let stdout_ch = Unix.in_channel_of_descr stdout_fd in
  Printf.printf "Stdout: %s\n%!" (input_line stdout_ch);

  let stderr_ch = Unix.in_channel_of_descr stderr_fd in
  Printf.printf "Stderr: %s\n%!" (input_line stderr_ch);

  (let exit_code = poll_ps pid in
  match exit_code with
  | None -> Printf.printf "Child not finished yet.\n%!"
  | Some n -> Printf.printf "Exit code: %d\n%!" n);

  Printf.printf "Sleeping again\n%!";
  Unix.sleep 1;

  (let exit_code = poll_ps pid in
  match exit_code with
  | None -> Printf.printf "Child not finished yet.\n%!"
  | Some n -> Printf.printf "Exit code: %d\n%!" n);

  Printf.printf "Done\n%!";
  close_in stdout_ch;
  close_in stderr_ch;

Add a wait_and_do polling ability:

let int_of_status s =
  match s with
  | Unix.WEXITED n -> n
  | Unix.WSIGNALED n -> n
  | Unix.WSTOPPED n -> n

let shell cmd out err =
  match Unix.fork () with
  | 0 ->
    Unix.dup2 out Unix.stdout;                                                  
    Unix.close out;                                                             
    Unix.dup2 err Unix.stderr;                                                  
    Unix.close err;                                                             
    Unix.execvp "/bin/sh" [| "/bin/sh"; "-c"; cmd |]
  | pid -> pid

let start_ps cmd =
  let (stdout_read, stdout_write) = Unix.pipe () in
  let (stderr_read, stderr_write) = Unix.pipe () in
  let pid = shell cmd stdout_write stderr_write in
  (pid, stdout_read, stderr_read)

let poll_ps pid =
  let pid', s = Unix.waitpid [Unix.WNOHANG] pid in
  match pid' with
  | 0 -> None
  | _ -> Some (int_of_status s)

let rec wait_and_do pid delay f x =
  let exit_code = poll_ps pid in
  match exit_code with
  | None ->
    f x;                                                                        
    Unix.sleepf delay;                                                          
    wait_and_do pid delay f x
  | Some n -> n

let report () =
  Printf.printf "Polling...\n%!"

let () =
  let cmd = "sleep 2 && echo boo && echo foo > /dev/stderr && exit 3" in

  let pid, stdout_fd, stderr_fd = start_ps cmd in
  Printf.printf "Child %d started.\n%!" pid;

  let exit_code = wait_and_do pid 0.25 report () in
  Printf.printf "Exit code: %d\n%!" exit_code;

  let stdout_ch = Unix.in_channel_of_descr stdout_fd in
  Printf.printf "Stdout: %s\n%!" (input_line stdout_ch);

  let stderr_ch = Unix.in_channel_of_descr stderr_fd in
  Printf.printf "Stderr: %s\n%!" (input_line stderr_ch);

  Printf.printf "Done\n%!";
  close_in stdout_ch;
  close_in stderr_ch;

Let's move the channels into the start_ps command, and add some error handling around the attempts to open file descriptors -- if these fail, we need to be sure to close them:

let int_of_status s =
  match s with
  | Unix.WEXITED n -> n
  | Unix.WSIGNALED n -> n
  | Unix.WSTOPPED n -> n

let shell cmd out err =
  match Unix.fork () with
  | 0 ->
    Unix.dup2 out Unix.stdout;                                                  
    Unix.close out;                                                             
    Unix.dup2 err Unix.stderr;                                                  
    Unix.close err;                                                             
    Unix.execvp "/bin/sh" [| "/bin/sh"; "-c"; cmd |]
  | pid -> pid

let start_ps cmd =
  let (stdout_read, stdout_write) = Unix.pipe ~cloexec:true () in
  let (stderr_read, stderr_write) =
    try Unix.pipe ~cloexec:true ()
    with e ->
      Unix.close stdout_read;
      Unix.close stdout_write;
      raise e in
  let out_ch = Unix.in_channel_of_descr stdout_read in
  let err_ch = Unix.in_channel_of_descr stderr_read in
  begin
    try
      let pid = shell cmd stdout_write stderr_write in
      Unix.close stdout_write;
      Unix.close stderr_write;
      (pid, out_ch, err_ch)
    with e ->
      Unix.close stdout_read; Unix.close stdout_write;
      Unix.close stderr_read; Unix.close stderr_write;
      raise e;
  end

let poll_ps pid =
  let pid', s = Unix.waitpid [Unix.WNOHANG] pid in
  match pid' with
  | 0 -> None
  | _ -> Some (int_of_status s)

let rec poll_ps_every pid delay f x =
  let exit_code = poll_ps pid in
  match exit_code with
  | None ->
    f x;                                                                        
    Unix.sleepf delay;                                                          
    poll_ps_every pid delay f x
  | Some n -> n

let report stdout_ch stderr_ch =
  Printf.printf "Polling...\n%!";
  Printf.printf "stdout %s\n%!" (input_line stdout_ch);
  Printf.printf "stderr %s\n%!" (input_line stderr_ch);
  Printf.printf "-- done polling...\n%!"

let () =
  let cmd = "bash test.sh" in

  let pid, stdout_ch, stderr_ch = start_ps cmd in
  Printf.printf "Child %d started.\n%!" pid;

  let exit_code = poll_ps_every pid 0.25 (report stdout_ch) stderr_ch in
  Printf.printf "Exit code: %d\n%!" exit_code;

  Printf.printf "Done\n%!";
  close_in stdout_ch;
  close_in stderr_ch;

Let's make the pipes non-blocking:

let int_of_status s =
  match s with
  | Unix.WEXITED n -> n
  | Unix.WSIGNALED n -> n
  | Unix.WSTOPPED n -> n

let shell cmd out err =
  match Unix.fork () with
  | 0 ->
    Unix.dup2 out Unix.stdout;                                                  
    Unix.close out;                                                             
    Unix.dup2 err Unix.stderr;                                                  
    Unix.close err;                                                             
    Unix.execvp "/bin/sh" [| "/bin/sh"; "-c"; cmd |]
  | pid -> pid

let start_ps cmd =
  let (stdout_read, stdout_write) = Unix.pipe ~cloexec:true () in
  let (stderr_read, stderr_write) =
    try Unix.pipe ~cloexec:true ()
    with e ->
      Unix.close stdout_read;
      Unix.close stdout_write;
      raise e in
  Unix.set_nonblock stdout_read;
  Unix.set_nonblock stderr_read;
  let out_ch = Unix.in_channel_of_descr stdout_read in
  let err_ch = Unix.in_channel_of_descr stderr_read in
  begin
    try
      let pid = shell cmd stdout_write stderr_write in
      Unix.close stdout_write;
      Unix.close stderr_write;
      (pid, out_ch, err_ch)
    with e ->
      Unix.close stdout_read; Unix.close stdout_write;
      Unix.close stderr_read; Unix.close stderr_write;
      raise e;
  end

let poll_ps pid =
  let pid', s = Unix.waitpid [Unix.WNOHANG] pid in
  match pid' with
  | 0 -> None
  | _ -> Some (int_of_status s)

let rec poll_ps_every pid delay f x =
  let exit_code = poll_ps pid in
  match exit_code with
  | None ->
    f x;                                                                        
    Unix.sleepf delay;                                                          
    poll_ps_every pid delay f x
  | Some n ->
    f x;
    n

let read_line ch lbl =
  Printf.printf "%s - attempting to read\n%!" lbl;
  try
    let line = input_line ch in
    Printf.printf "%s line: %s\n%!" lbl line
  with Sys_blocked_io ->
      Printf.printf "%s - Sys_blocked_io\n%!" lbl

let report stdout_ch stderr_ch =
  Printf.printf "Polling...\n%!";
  read_line stdout_ch "out";
  read_line stderr_ch "err";
  Printf.printf "-- done polling...\n%!"

let () =
  let cmd = "bash test.sh" in

  let pid, stdout_ch, stderr_ch = start_ps cmd in
  Printf.printf "Child %d started.\n%!" pid;

  let exit_code = poll_ps_every pid 0.25 (report stdout_ch) stderr_ch in
  Printf.printf "Exit code: %d\n%!" exit_code;

  Printf.printf "Done\n%!";
  close_in stdout_ch;
  close_in stderr_ch;

Instead of printing out the output, let's put stdout/stderr into buffers.

let int_of_status s =
  match s with
  | Unix.WEXITED n -> n
  | Unix.WSIGNALED n -> n
  | Unix.WSTOPPED n -> n

let shell cmd out err =
  match Unix.fork () with
  | 0 ->
    Unix.dup2 out Unix.stdout;                                                  
    Unix.close out;                                                             
    Unix.dup2 err Unix.stderr;                                                  
    Unix.close err;                                                             
    Unix.execvp "/bin/sh" [| "/bin/sh"; "-c"; cmd |]
  | pid -> pid

let start_ps cmd =
  let (stdout_read, stdout_write) = Unix.pipe ~cloexec:true () in
  let (stderr_read, stderr_write) =
    try Unix.pipe ~cloexec:true ()
    with e ->
      Unix.close stdout_read;
      Unix.close stdout_write;
      raise e in
  Unix.set_nonblock stdout_read;
  Unix.set_nonblock stderr_read;
  let out_ch = Unix.in_channel_of_descr stdout_read in
  let err_ch = Unix.in_channel_of_descr stderr_read in
  begin
    try
      let pid = shell cmd stdout_write stderr_write in
      Unix.close stdout_write;
      Unix.close stderr_write;
      (pid, out_ch, err_ch)
    with e ->
      Unix.close stdout_read; Unix.close stdout_write;
      Unix.close stderr_read; Unix.close stderr_write;
      raise e;
  end

let poll_ps pid =
  let pid', s = Unix.waitpid [Unix.WNOHANG] pid in
  match pid' with
  | 0 -> None
  | _ -> Some (int_of_status s)

let rec poll_ps_every pid delay f x =
  let exit_code = poll_ps pid in
  match exit_code with
  | None ->
    f x;                                                                        
    Unix.sleepf delay;                                                          
    poll_ps_every pid delay f x
  | Some n ->
    f x;
    n

let ch_buf ch len = (ch, Buffer.create len)

let read_buf cb =
  let ch = fst(cb) and buf = snd(cb) in
  try   
    while true do
      Buffer.add_channel buf ch 1
    done
  with
  | Sys_blocked_io -> ()
  | End_of_file -> ()

let string_of_buf cb =
  let buf = snd(cb) in
  Buffer.contents buf

let collect out_buf err_buf =
  read_buf out_buf; read_buf err_buf

let () =
  let cmd = "bash test.sh" in

  let pid, stdout_ch, stderr_ch = start_ps cmd in
  Printf.printf "Child %d started.\n%!" pid;

  let out_buf = ch_buf stdout_ch 80 and
      err_buf = ch_buf stderr_ch 80 in

  let exit_code = poll_ps_every pid 0.25 (collect out_buf) err_buf in
  Printf.printf "Exit code: %d\n%!" exit_code;

  Printf.printf "\nOut buffer: %s\n%!" (string_of_buf out_buf);
  Printf.printf "\nErr buffer: %s\n%!" (string_of_buf err_buf);
  Printf.printf "Done\n%!";
  close_in stdout_ch;
  close_in stderr_ch;

Packaging it up into modules

module Proc = struct

  let int_of_status s =
    match s with
    | Unix.WEXITED n -> n
    | Unix.WSIGNALED n -> n
    | Unix.WSTOPPED n -> n

  let shell cmd out err =
    match Unix.fork () with
    | 0 ->
      Unix.dup2 out Unix.stdout;                                                
      Unix.close out;                                                           
      Unix.dup2 err Unix.stderr;                                                
      Unix.close err;                                                           
      Unix.execvp "/bin/sh" [| "/bin/sh"; "-c"; cmd |]
    | pid -> pid

  let popen cmd =
    let (stdout_read, stdout_write) = Unix.pipe ~cloexec:true () in
    let (stderr_read, stderr_write) =
      try Unix.pipe ~cloexec:true ()
      with e ->
        Unix.close stdout_read;
        Unix.close stdout_write;
        raise e in
    Unix.set_nonblock stdout_read;
    Unix.set_nonblock stderr_read;
    let out_ch = Unix.in_channel_of_descr stdout_read in
    let err_ch = Unix.in_channel_of_descr stderr_read in
    begin
      try
        let pid = shell cmd stdout_write stderr_write in
        Unix.close stdout_write;
        Unix.close stderr_write;
        (pid, out_ch, err_ch)
      with e ->
        Unix.close stdout_read; Unix.close stdout_write;
        Unix.close stderr_read; Unix.close stderr_write;
        raise e;
    end

  let poll pid =
    let pid', s = Unix.waitpid [Unix.WNOHANG] pid in
    match pid' with
    | 0 -> None
    | _ -> Some (int_of_status s)

end

module Buff = struct

    let create ch len = (ch, Buffer.create len)

    let contents b =
      let buf = snd(b) in
      Buffer.contents buf

    let read b =
      let ch = fst(b) and buf = snd(b) in
      try
        while true do
          Buffer.add_channel buf ch 1
        done
      with
      | Sys_blocked_io -> ()
      | End_of_file -> ()

end

module Cmd = struct

  let rec while_waiting pid delay f x =
    match Proc.poll pid with
    | None ->
      f x;                                                                      
      Unix.sleepf delay;                                                        
      while_waiting pid delay f x
    | Some n ->
      f x;
      n

  let collect out_buf err_buf =
    Buff.read out_buf; Buff.read err_buf

  let run cmd =
    let pid, stdout_ch, stderr_ch = Proc.popen cmd in
    let out_buf = Buff.create stdout_ch 80 in
    let err_buf = Buff.create stderr_ch 80 in
    let exit_code = while_waiting pid 0.25 (collect out_buf) err_buf in
    close_in stdout_ch;
    close_in stderr_ch;
    (exit_code, out_buf, err_buf)

end

let () =
  let cmd = "bash test.sh" in
  let exit_code, out_buf, err_buf = Cmd.run cmd in

  Printf.printf "Out buffer:\n%s\n%!" (Buff.contents out_buf);
  Printf.printf "Err buffer:\n%s\n%!" (Buff.contents err_buf);
  Printf.printf "Exit code: %d\n%!" exit_code;

Handle signals

let () =
  match Unix.fork () with
    | 0 ->
        (* child ignores INT and does its thing *)
        Sys.set_signal Sys.sigint Sys.Signal_ignore;
        Unix.execv "/bin/sleep" [| "/bin/sleep"; "10" |]
    | pid ->
        (* parent catches INT and berates user *)
        Sys.set_signal Sys.sigint
          (Sys.Signal_handle
            (fun _ -> print_endline "Tsk tsk, no process interruptus"));
        let running = ref true in
        while !running do
          try (ignore (Unix.waitpid [] pid); running := false)
          with Unix.Unix_error _ -> ()
        done;
        Sys.set_signal Sys.sigint Sys.Signal_default

Measuring elapsed time

Create a file called measuresleep.ml:

let repeats = 5 in
let total_time = ref 0.0 in

for i = 1 to repeats do
  let before = Unix.gettimeofday () in
  Unix.sleep 1;
  let after = Unix.gettimeofday () in
  let delta = after -. before in
  Printf.printf "Delta: %.5f\n" delta;
  total_time := !total_time +. delta
done;

let avg = !total_time /. (float_of_int repeats) in
Printf.printf "Total: %.5f - Avg: %.5f\n" !total_time avg

Try it:

ocamlc -c measuresleep.ml
ocamlc -o measuresleep.byte unix.cma measuresleep.cmo
./measuresleep.byte

Timing functions

Save the above file as ps.ml, and remove the final let () = ..., so that it only contains the modules.

Then create a new file called timeit.ml:

let time_one f x =
  let start = Unix.gettimeofday () in
  let result = f x in
  let stop = Unix.gettimeofday () in
  let delta = stop -. start in
  (delta, result)

let () =
  Printf.printf "Starting...\n%!";                                              

  let cmd = "bash test.sh" in
  let delta, result = time_one Ps.Cmd.run cmd in

  let exit_code (n, _, _) = n in
  let stdout (_, n, _) = n in
  Printf.printf "exit code: %d\n%!" (exit_code result);                         
  Printf.printf "stdout: %s\n%!" (Ps.Buff.contents (stdout result));            

  Printf.printf "Time: %.3f\n%!" delta

Compile the modules:

ocamlc -c ps.ml
ocamlc -c timeit.ml

Then link it:

ocamlc -o timeit.byte unix.cma ps.cmo timeit.cmo

And run it:

./timeit.byte

Let's add the ability to run a function many times, collect the results, and average the running time:

let time f x =
  let start = Unix.gettimeofday () in
  let result = f x in
  let stop = Unix.gettimeofday () in
  let delta = stop -. start in
  (delta, result)

let rec time_r count n results f x =
  let result = time f x in
  match (count < n) with
  | false -> results
  | true ->
    time_r (count + 1) n (results @ [result]) f x

let rec sumf l acc =
  match l with
  | [] -> acc
  | h :: t -> sumf t (acc +. h)

let deltas l = List.map (fun x -> fst(x)) l

let avg l =
  let times = deltas l in
  let total_time = sumf times 0.0 in
  total_time /. (float_of_int (List.length times))

let () =
  Printf.printf "Starting...\n%!";                                              

  let cmd = "bash test.sh" in
  let trials = time_r 0 3 [] Ps.Cmd.run cmd in

  List.iter (fun x ->
    let get_delta (n, _) = n in
    let get_result (_, n) = n in
    let get_exit_code (n, _, _) = n in
    let get_stdout (_, n, _) = n in
    let delta = get_delta x in
    let result = get_result x in
    Printf.printf "----\n%!";                                                   
    Printf.printf "exit code: %d\n%!" (get_exit_code result);                   
    Printf.printf "stdout:\n%s\n%!" (Ps.Buff.contents (get_stdout result));     
    Printf.printf "Time: %.3f\n%!" delta)
    trials;                                                                     

  let avg_time = avg trials in
  Printf.printf "Avg time: %.3f\n%!" avg_time;                                  
  let total_time = sumf (deltas trials) 0.0 in
  Printf.printf "Total time: %.3f\n%!" total_time

Let's package up some of the functions into a module:

module Timer = struct

  let time f x =
    let start = Unix.gettimeofday () in
    let result = f x in
    let stop = Unix.gettimeofday () in
    let delta = stop -. start in
    (delta, result)

  let rec repeat count n results f x =
    let result = time f x in
    match (count < n) with
    | false -> results
    | true ->
      repeat (count + 1) n (results @ [result]) f x

  let rec sumf l acc =
    match l with
    | [] -> acc
    | h :: t -> sumf t (acc +. h)

  let deltas l = List.map (fun x -> fst(x)) l

  let total_time l =
    let times = deltas l in
    sumf times 0.0

  let avg l =
    let num_trials = List.length l in
    let total = total_time l in
    total /. (float_of_int (num_trials))

end

let () =
  Printf.printf "Starting...\n%!";                                              

  let cmd = "bash test.sh" in
  let trials = Timer.repeat 0 3 [] Ps.Cmd.run cmd in

  List.iter (fun x ->
    let get_delta (n, _) = n in
    let get_result (_, n) = n in
    let get_exit_code (n, _, _) = n in
    let get_stdout (_, n, _) = n in
    let delta = get_delta x in
    let result = get_result x in
    Printf.printf "----\n%!";                                                   
    Printf.printf "exit code: %d\n%!" (get_exit_code result);                   
    Printf.printf "stdout:\n%s\n%!" (Ps.Buff.contents (get_stdout result));     
    Printf.printf "Time: %.3f\n%!" delta)
    trials;

  let avg_time = Timer.avg trials in
  Printf.printf "Avg time: %.3f\n%!" avg_time;
  let total_time = Timer.total_time trials in
  Printf.printf "Total time: %.3f\n%!" total_time

Logging

Open a file, write a message to it, and close the file:

let main () =
  let ch = open_out "log.txt" in
  Printf.fprintf  ch "Lorem ipsum...\n%!";
  close_out ch

let () = Unix.handle_unix_error main ()

Let's put the writing operation into a function:

let log ch msg = Printf.fprintf ch "%s\n%!" msg

let main () =
  let ch = open_out "/root/test-log.txt" in
  log ch "Lorem ipsum...";
  close_out ch

let () = Unix.handle_unix_error main ()

Let's write the channel (with a name) to a hash table, and then let's close it by looking it up from the hash table.

let log ch msg = Printf.fprintf ch "%s\n%!" msg

let main () =
  let log_hashtbl = Hashtbl.create 256 in

  let ch = open_out "verbose.log" in
  Hashtbl.add log_hashtbl "verbose_log" ch;                                                        

  log ch "Lorem ipsum...";                                                                         

  let log_ch = Hashtbl.find log_hashtbl "verbose_log" in
  close_out log_ch

let () = Unix.handle_unix_error main ()

Now let's put the opening and closing into functions:

let log_hashtbl = Hashtbl.create 256

let log ch msg = Printf.fprintf ch "%s\n%!" msg

let open_log name filename =
  let ch = open_out filename in
  Hashtbl.add log_hashtbl name ch;                                                                 
  ch

let close_log name =
  let ch = Hashtbl.find log_hashtbl name in
  close_out ch

let main () =
  let verbose_log = open_log "verbose_log" "out.log" in
  log verbose_log "Lorem ipsum...";                                                                
  close_log "verbose_log"

let () = Unix.handle_unix_error main ()

Let's register a function that will close the log at exit. Then we don't have to explicitly close the log (unless we want to).

let log_hashtbl = Hashtbl.create 256

let log ch msg = Printf.fprintf ch "%s\n%!" msg

let close_log name =
  let ch = Hashtbl.find log_hashtbl name in
  close_out ch

let open_log name filename =
  let ch = open_out filename in
  Hashtbl.add log_hashtbl name ch;                                                                 
  at_exit (fun _ -> close_log name);                                                               
  ch

let main () =
  let verbose_log = open_log "verbose_log" "out.log" in
  log verbose_log "Lorem ipsum..."

let () = Unix.handle_unix_error main ()

Let's add the ability to open a channel to stdout, stderr, or a file:

let log_hashtbl = Hashtbl.create 256

let log ch msg = Printf.fprintf ch "%s\n%!" msg

let close_log name =
  let ch = Hashtbl.find log_hashtbl name in
  close_out ch

let log_channel target =
  match target with
  | "stdout" -> stdout
  | "stderr" -> stderr
  | filename -> open_out filename

let open_log name target =
  let ch = log_channel target in
  Hashtbl.add log_hashtbl name ch;
  at_exit (fun _ -> close_log name);
  ch

let main () =
  let verbose_log = open_log "verbose_log" "out.log" in
  log verbose_log "Lorem ipsum...";

  let program_log = open_log "program_log" "stdout" in
  log program_log "Logging to stdout..."

let () = Unix.handle_unix_error main ()

Let's package it up into a module:

module Logs = struct

  let hash = Hashtbl.create 256

  let log name msg =
    let ch = Hashtbl.find hash name in
    Printf.fprintf ch "%s\n%!" msg

  let close name =
    let ch = Hashtbl.find hash name in
    close_out ch

  let channel target =
    match target with
    | "stdout" -> stdout
    | "stderr" -> stderr
    | filename -> open_out_gen [Open_creat; Open_text; Open_append] 0o640 filename

  let create name target =
    let ch = channel target in
    Hashtbl.add hash name ch;
    at_exit (fun _ -> close name)

end

let main () =
  Logs.create "verbose_log" "verbose.log";
  Logs.log "verbose_log" "Logging to verbose log...";

  Logs.create "other_verbose_log" "verbose.log";
  Logs.log "other_verbose_log" "Logging to other verbose log...";

  Logs.create "main_log" "stdout";
  Logs.log "main_log" "Logging to stdout...";
  Printf.printf "Done\n%!"

let () = Unix.handle_unix_error main ()

Resources

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment