lib/exec: intro `ProcessDuplex::write_and_read`
[nit.git] / lib / standard / exec.nit
index c98a39c..3e0d97a 100644 (file)
 #
 # This file is free software, which comes along with NIT.  This software is
 # distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
-# without  even  the implied warranty of  MERCHANTABILITY or  FITNESS FOR A 
+# without  even  the implied warranty of  MERCHANTABILITY or  FITNESS FOR A
 # PARTICULAR PURPOSE.  You can modify it is you want,  provided this header
 # is kept unaltered, and a notification of the changes is added.
 # You  are  allowed  to  redistribute it and sell it, alone or is a part of
 # another product.
 
-# This module handle simple system calls
-# Standard input and output can be handleb trougth streams.
-package exec 
+# Invocation and management of operating system sub-processes.
+# Standard input and output can be handled through streams.
+module exec
 
-import stream
+import file
 
-# Simple sub-processus
-class Process
+in "C" `{
+       #include <stdlib.h>
+       #include <string.h>
+       #include <errno.h>
+       #include <stdio.h>
+       #include <unistd.h>
+       #include <sys/wait.h>
+       #include <signal.h>
+`}
+
+in "C Header" `{
+       #include <sys/types.h>
+
+       // FIXME this should be in the "C" block when bug on module blocks is fixed
+       // or, even better, replace the C structure by a Nit object.
+       typedef struct se_exec_data se_exec_data_t;
+       struct se_exec_data {
+               pid_t id;
+               int running;
+               int status;
+               int in_fd;
+               int out_fd;
+               int err_fd;
+       };
+`}
 
-       # The pid of the processus
-       meth id: Int do return _data.id
+# Simple sub-process
+class Process
+       # The pid of the process
+       fun id: Int do return data.id
 
-       # Is the processus finished?
-       meth is_finished: Bool do return _data.is_finished
+       # Is the process finished?
+       fun is_finished: Bool do return data.is_finished
 
-       # wait the terminaison of the process
-       meth wait
+       # Wait the termination of the process
+       fun wait
        do
-               _data.wait
+               data.wait
                assert is_finished
        end
-       
+
        # The status once finished
-       meth status: Int
+       fun status: Int
        do
                assert is_finished
-               return _data.status
+               return data.status
        end
 
-       # send a signal to the process
-       meth kill(signal: Int) do _data.kill(signal)
+       # The executable run
+       # Is a filepath, or a executable found in PATH
+       var command: String
 
-       # send the TERM (15) signal
-       meth term do kill(15)
+       # The arguments of the command
+       # Starts with the first real arguments---ie. does not include the progname (`argv[0]`, in C)
+       var arguments: nullable Array[String]
 
-       # launch a command with some arguments
-       init(command: String, arguments: String...)
-       do
-               execute(command, arguments, 0)
+       # Launch a command with some arguments
+       init(command: String, arguments: String...) is old_style_init do
+               self.command = command
+               self.arguments = arguments
+               execute
        end
 
-       # launch a simple command without arguments
-       init init_(command: String)
+       # Launch a simple command with arguments passed as an array
+       init from_a(command: String, arguments: nullable Array[String])
        do
-               execute(command, null, 0)
+               self.command = command
+               self.arguments = arguments
+               execute
        end
 
-       # Internal code to handle execusion
-       protected init execute(command: String, arguments: Array[String], pipeflags: Int)
+       # flags used internally to know whith pipe to open
+       private fun pipeflags: Int do return 0
+
+       # Internal code to handle execution
+       protected fun execute
        do
-               var args = new String
+               # The pass the arguments as a big C string where elements are separated with '\0'
+               var args = new FlatBuffer
                var l = 1 # Number of elements in args
                args.append(command)
                if arguments != null then
                        for a in arguments do
                                args.add('\0')
+                               #a.output_class_name
                                args.append(a)
                        end
                        l += arguments.length
                end
-               _data = basic_exec_execute(command.to_cstring, args.to_cstring, l, pipeflags)
+               data = basic_exec_execute(command.to_cstring, args.to_s.to_cstring, l, pipeflags)
        end
-       
-       attr _data: NativeProcess
-       private meth basic_exec_execute(p: NativeString, av: NativeString, ac: Int, pf: Int): NativeProcess is extern "exec_Process_Process_basic_exec_execute_4"
+
+       private var data: NativeProcess
+       private fun basic_exec_execute(prog, args: NativeString, argc: Int, pipeflag: Int): NativeProcess `{
+               se_exec_data_t* result = NULL;
+               int id;
+               int in_fd[2];
+               int out_fd[2];
+               int err_fd[2];
+               if (pipeflag & 1) {
+                       int res = pipe(in_fd);
+                       if ( res == -1 ) {
+                               return NULL;
+                       }
+               }
+               if (pipeflag & 2) {
+                       int res = pipe(out_fd);
+                       if ( res == -1 ) {
+                               return NULL;
+                       }
+               }
+               if (pipeflag & 4) {
+                       int res = pipe(err_fd);
+                       if ( res == -1 ) {
+                               return NULL;
+                       }
+               }
+
+               id = fork();
+               if (id == 0)
+                       { /* child */
+                       char **arg = malloc(sizeof(char*) * (argc+1));
+                       char *c = args;
+                       int i;
+
+                       /* Prepare args */
+                       for(i=0; i<argc; i++)
+                       {
+                               arg[i] = c;
+                               c += strlen(c) + 1;
+                       }
+                       arg[argc] = NULL;
+
+                       /* Connect pipe */
+                       if (pipeflag & 1)
+                       {
+                               close(0);
+                               dup2(in_fd[0], 0);
+                               close(in_fd[0]);
+                               close(in_fd[1]);
+                       }
+                       if (pipeflag & 2)
+                       {
+                               close(1);
+                               dup2(out_fd[1], 1);
+                               close(out_fd[0]);
+                               close(out_fd[1]);
+                       }
+                       if (pipeflag & 4)
+                       {
+                               close(2);
+                               dup2(err_fd[1], 2);
+                               close(err_fd[0]);
+                               close(err_fd[1]);
+                       }
+
+                       /* calls */
+                       execvp(prog, arg);
+                       _exit(127);
+               }
+               else if (id > 0)
+                       { /* father */
+                       result = (se_exec_data_t*)malloc(sizeof(se_exec_data_t));
+                       result->id = id;
+                       result->running = 1;
+                       if (pipeflag & 1)
+                       {
+                               result->in_fd = in_fd[1];
+                               close(in_fd[0]);
+                       } else
+                               result->in_fd = -1;
+
+                       if (pipeflag & 2)
+                       {
+                               result->out_fd = out_fd[0];
+                               close(out_fd[1]);
+                       } else
+                               result->out_fd = -1;
+
+                       if (pipeflag & 4)
+                       {
+                               result->err_fd = err_fd[0];
+                               close(err_fd[1]);
+                       } else
+                               result->err_fd = -1;
+               }
+
+               return result;
+       `}
 end
 
-# stdout of the processus is readable
-class IProcess
-special Process
-special IStream
-       attr _in: FDIStream
-       
-       redef meth close do _in.close
-       
-       redef meth read_char do return _in.read_char
+# `Process` on which the `stdout` is readable like a `Reader`
+class ProcessReader
+       super Process
+       super Reader
 
-       redef meth eof do return _in.eof
+       # File Descriptor used for the input.
+       var stream_in: FileReader is noinit
 
-       init(command: String, arguments: String...)
-       do
-               execute(command, arguments, 2)
-               _in = new FDIStream(_data.out_fd)
-       end
-       
-       init init_(command: String)
+       redef fun close do stream_in.close
+
+       redef fun read_char do return stream_in.read_char
+
+       redef fun read_byte do return stream_in.read_byte
+
+       redef fun eof do return stream_in.eof
+
+       redef fun pipeflags do return 2
+
+       redef fun execute
        do
-               execute(command, null, 2)
-               _in = new FDIStream(_data.out_fd)
+               super
+               stream_in = new FileReader.from_fd(data.out_fd)
        end
 end
 
-# stdin of the processus is writable
-class OProcess
-special Process
-special OStream
-       attr _out: OStream
+# `Process` on which `stdin` is writable like a `Writer`
+class ProcessWriter
+       super Process
+       super Writer
 
-       redef meth close do _out.close
+       # File Descriptor used for the output.
+       var stream_out: Writer is noinit
 
-       redef meth is_writable do return _out.is_writable
+       redef fun close do stream_out.close
 
-       redef meth write(s) do _out.write(s)
-       
-       init(command: String, arguments: String...)
-       do
-               execute(command, arguments, 1)
-               _out = new FDOStream(_data.in_fd)
-       end
-       
-       init init_(command: String)
+       redef fun is_writable do return stream_out.is_writable
+
+       redef fun write(s) do stream_out.write(s)
+
+       redef fun pipeflags do return 1
+
+       redef fun execute
        do
-               execute(command, null, 1)
-               _out = new FDOStream(_data.in_fd)
+               super
+               var out = new FileWriter.from_fd(data.in_fd)
+               out.set_buffering_mode(0, sys.buffer_mode_none)
+               stream_out = out
        end
 end
 
-# stdin and stdout are both accessible
-class IOProcess
-special IProcess
-special OProcess
-special IOStream
+# `Process` on which stdout can be read and stdin can be written to like a `Duplex`
+class ProcessDuplex
+       super ProcessReader
+       super ProcessWriter
+       super Duplex
 
-       redef meth close
+       redef fun close
        do
-               _in.close
-               _out.close
+               stream_in.close
+               stream_out.close
        end
 
-       init(command: String, arguments: String...)
-       do
-               execute(command, arguments, 3)
-               _in = new FDIStream(_data.out_fd)
-               _out = new FDOStream(_data.in_fd)
-       end
-       
-       init init_(command: String)
+       redef fun pipeflags do return 3
+
+       redef fun execute do super
+
+       # Write `input` to process and return its output
+       #
+       # Writing and reading are processed line by line,
+       # reading only when something is available.
+       #
+       # ~~~
+       # var proc = new ProcessDuplex("tr", "[:lower:]", "[:upper:]")
+       # assert proc.write_and_read("""
+       #     Alice
+       #     Bob
+       # """) == """
+       #     ALICE
+       #     BOB
+       # """
+       # ~~~
+       fun write_and_read(input: Text): String
        do
-               execute(command, null, 3)
-               _in = new FDIStream(_data.out_fd)
-               _out = new FDOStream(_data.in_fd)
+               var read = new Buffer #new Array[String]
+
+               # Main loop, read and write line by line
+               var prev = 0
+               for delimiter in input.search_all('\n') do
+                       write input.substring(prev, delimiter.after-prev)
+                       prev = delimiter.after
+
+                       while stream_in.poll_in do
+                               read.append stream_in.read_line
+                       end
+               end
+
+               # Write the last line
+               write input.substring_from(prev)
+               stream_out.close
+
+               # Read the rest, may be everything for some programs
+               read.append stream_in.read_all
+               stream_in.close
+
+               # Clean up
+               wait
+               return read.to_s
        end
 end
 
 redef class Sys
-       # Execute a shell command and return it's error code
-       meth system(command: String): Int
+       # Execute a shell command and return its error code
+       fun system(command: String): Int
        do
-               return command.to_cstring.system        
+               return command.to_cstring.system
        end
 end
 
 redef class NativeString
-       meth system: Int is extern "string_NativeString_NativeString_system_0"
+       # Execute self as a shell command.
+       #
+       # See the posix function system(3).
+       fun system: Int `{
+               int status = system(self);
+               if (WIFSIGNALED(status) && WTERMSIG(status) == SIGINT) {
+                       // system exited on SIGINT: in my opinion the user wants the main to be discontinued
+                       kill(getpid(), SIGINT);
+               }
+               return status;
+       `}
 end
 
-private universal NativeProcess
-special Pointer
-       meth id: Int is extern "exec_NativeProcess_NativeProcess_id_0"
-       meth is_finished: Bool is extern "exec_NativeProcess_NativeProcess_is_finished_0"
-       meth status: Int is extern "exec_NativeProcess_NativeProcess_status_0"
-       meth wait is extern "exec_NativeProcess_NativeProcess_wait_0"
-       meth kill(s: Int) is extern "exec_NativeProcess_NativeProcess_kill_1"
-
-       meth in_fd: Int is extern "exec_NativeProcess_NativeProcess_in_fd_0"
-       meth out_fd: Int is extern "exec_NativeProcess_NativeProcess_out_fd_0"
-       meth err_fd: Int is extern "exec_NativeProcess_NativeProcess_err_fd_0"
+private extern class NativeProcess `{ se_exec_data_t* `}
+
+       fun id: Int `{ return self->id; `}
+       fun status: Int `{ return self->status; `}
+       fun in_fd: Int `{ return self->in_fd; `}
+       fun out_fd: Int `{ return self->out_fd; `}
+       fun err_fd: Int `{ return self->err_fd; `}
+
+       fun is_finished: Bool `{
+               int result = (int)0;
+               int status;
+               if (self->running) {
+                       int id = waitpid(self->id, &status, WNOHANG);
+                       if (id != 0) {
+                               /* child is finished */
+                               result = (int)(id == self->id);
+                               self->status = WEXITSTATUS(status);
+                               self->running = 0;
+                       }
+               }
+               else{
+                       result = (int)1;
+               }
+               return result;
+       `}
+
+       fun wait `{
+               int status;
+               if (self->running) {
+                       waitpid(self->id, &status, 0);
+                       self->status = WEXITSTATUS(status);
+                       self->running = 0;
+               }
+       `}
 end