Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Capturing stdout of a command in SML

Tags:

posix

sml

smlnj

I'm trying to capture the output of a command run using Posix.Process.execp. I ported some C code I found on stackoverflow and can capture output for one execution, but I can't get the output for a second execution.

Here's my function:

(* Runs a command c (command and argument list) using Posix.Process.execp. *)
(* If we successfully run the program, we return the lines output to stdout *)
(* in a list, along with SOME of the exit code. *)
(* If we fail to run the program, we return the error message in the list *)
(* and NONE. *)
fun execpOutput (c : string * string list) : (string list * Posix.Process.exit_status option) =
  let fun readAll () = case TextIO.inputLine TextIO.stdIn
                    of SOME s => s :: (readAll ())
                     | NONE => []
      (* Create a new pipe *)
      val { infd = infd, outfd = outfd } = Posix.IO.pipe ()
  in case Posix.Process.fork ()
      of NONE => (
      (* We are the child. First copy outfd to stdout; they will *)
      (* point to the same file descriptor and can be used interchangeably. *)
      (* See dup(2) for details. Then close infd: we don't need it and don't *)
      (* want to block because we have open file descriptors laying around *)
      (* when we want to exit. *)
      ( Posix.IO.dup2 { old = outfd, new = Posix.FileSys.stdout }
      ; Posix.IO.close infd
      ; Posix.Process.execp c )
      handle OS.SysErr (err, _) => ([err], NONE) )
       | SOME pid =>
     (* We are the parent. This time, copy infd to stdin, and get rid of the *)
     (* outfd we don't need. *)
     let val _ = ( Posix.IO.dup2 { old = infd, new = Posix.FileSys.stdin }
                 ; Posix.IO.close outfd )
         val (_, status) = Posix.Process.waitpid (Posix.Process.W_CHILD pid, [])
     in (readAll (), SOME status) end
  end

val lsls = (#1 (execpOutput ("ls", ["ls"]))) @ (#1 (execpOutput ("ls", ["ls"])))
val _ = app print lsls

and here's the corresponding output:

rak@zeta:/tmp/test$ ls
a  b  c
rak@zeta:/tmp/test$ echo 'use "/tmp/mwe.sml";' | sml
Standard ML of New Jersey v110.79 [built: Tue Aug  8 16:57:33 2017]
- [opening /tmp/mwe.sml]
[autoloading]
[library $SMLNJ-BASIS/basis.cm is stable]
[library $SMLNJ-BASIS/(basis.cm):basis-common.cm is stable]
[autoloading done]
a
b
c
val execpOutput = fn
  : string * string list -> string list * ?.POSIX_Process.exit_status option
val lsls = ["a\n","b\n","c\n"] : string list
val it = () : unit
-

Any suggestions on what I'm doing wrong?

like image 695
Ryan Kavanagh Avatar asked Sep 12 '17 22:09

Ryan Kavanagh


1 Answers

This is not a complete answer.

If you run sml interactively, you'll notice that the interpreter exits after the first call:

$ sml
Standard ML of New Jersey v110.81 [built: Wed May 10 21:25:32 2017]
- use "mwe.sml";
[opening mwe.sml]
...
- execpOutput ("ls", ["ls"]);
val it = (["mwe.sml\n"],SOME W_EXITED)
  : string list * ?.POSIX_Process.exit_status option
- 
$ # I didn't type exit ();

Your program appears to be an adaptation of this pipe/fork/exec example in C that does work. The only apparent difference is the C line fdopen(pipefd[0], "r") where you instead write Posix.IO.dup2 { old = infd, new = Posix.FileSys.stdin }.

I might investigate if those are really intended to give the same result. You could run strace on each of the programs and see when their system calls deviate. I haven't made further sense of it, though.

I tried to run strace sml nwe.sml 2>&1 | grep -v getrusage on:

fun readAll () = case TextIO.inputLine TextIO.stdIn
                   of SOME s => s :: readAll ()
                    | NONE => []

fun execpOutput (c : string * string list) : (string list * Posix.Process.exit_status option) =
  let val { infd = infd, outfd = outfd } = Posix.IO.pipe ()
  in case Posix.Process.fork ()
          (* Child *)
       of NONE => (( Posix.IO.close infd
                   ; Posix.IO.dup2 { old = outfd, new = Posix.FileSys.stdout }
                   ; Posix.IO.dup2 { old = outfd, new = Posix.FileSys.stderr }
                   ; Posix.Process.execp c )
                  handle OS.SysErr (err, _) => ([err], NONE) )
         (* Parent *)
        | SOME pid =>
          let val _ = Posix.IO.close outfd
              val _ = Posix.IO.dup2 { old = infd, new = Posix.FileSys.stdin }
              val _ = Posix.Process.waitpid (Posix.Process.W_CHILD pid, [])
          in readAll () end
  end

val _ = app print (execpOutput ("ls", ["ls"]));
val _ = app print (execpOutput ("ls", ["ls"]));

just as I tried to run strace ./mwe on the compiled output of

#include <stdio.h>
#include <stdlib.h>
#include <spawn.h>
#include <sys/wait.h>
#include <unistd.h>

void execpOutput(char *cmd[])
{
    pid_t pid;
    int pipefd[2];
    FILE* output;
    char line[256];
    int status;

    pipe(pipefd);
    pid = fork();
    if (pid == 0) {
        close(pipefd[0]);
        dup2(pipefd[1], STDOUT_FILENO);
        dup2(pipefd[1], STDERR_FILENO);
        execvp(cmd[0], cmd);
    } else {
        close(pipefd[1]);
        output = fdopen(pipefd[0], "r");
        while (fgets(line, sizeof(line), output)) {
            printf("%s", line);
        }
        waitpid(pid, &status, 0);
    }
}

int main(int argc, char *argv[])
{
    char *cmd[] = {"ls", NULL};

    execpOutput(cmd);
    execpOutput(cmd);
    return 2;
}
like image 105
sshine Avatar answered Nov 13 '22 01:11

sshine