added
[rrq/newlisp-ftw.git] / coproc.lsp
1 ;; Utility module for coprocesses in newlisp
2 ;;
3 ;; This is a convenience implementation for running a sub process with
4 ;; its stdio via pipes. It is somewhat similar to the standard
5 ;; <process> function but the <coproc> utility combines the process
6 ;; PID together with its stdin/stdout into a FOOP object for onwards
7 ;; interactions. The <coproc> command is passed as a command line to
8 ;; /bin/sh and allows passing in environment. Further, the startup
9 ;; includes closing the unused file descriptors.
10 ;;
11 ;; @syntax: (coproc cmd [ env ] )
12 ;; Start a coprocess and returns its representation which is a FOOP
13 ;; object: ([coproc] PID outfd infd)
14 ;;
15 ;; The given <cmd> command is passed to "sh -c 'cmd'" sub process
16 ;; through a fork+execve scheme. The <env> argument should be a
17 ;; pointer to a null-terminated array of pointers to environment
18 ;; NUL-terminated strings of format "NAME=VALUE". (The caller must
19 ;; take care of providing existing pointers.
20 ;;
21 ;; @syntax: (:put0 coproc str )
22 ;; Write <str> to the given <coproc>. Returns the number of bytes
23 ;; written.
24 ;;
25 ;; @syntax: (:puts coproc str )
26 ;; Write <str> followed by newline to the given <coproc>. Returns the
27 ;; number of bytes written.
28 ;;
29 ;; @syntax: (:gets coproc [ limit [ waitms ]] )
30 ;; Read one line from the given <coproc> or up to <limit> bytes
31 ;; (default 100000) with up to <waitms> millisceonds delay (default
32 ;; 100) before reading. 
33 ;;
34 ;; @syntax: (:pug coproc str [ limit [ waitms ]] )
35 ;; Write <str> with :puts, then read once with :gets and return that.
36 ;;
37 ;; @syntax: (:pugs coproc str [ limit [ waitms ]] )
38 ;; Write <str> with :puts, then read repeatedly with :gets collating
39 ;; its returned lines into a list until :gets retuns nil. Returns the
40 ;; list of lines.
41 ;;
42 ;; @syntax: (:running? coproc [ timeout ])
43 ;; Check if coproc is running, i.e., that its stdin is still open.
44
45 (context 'coproc)
46
47 (constant 'LIBC6 "/lib/x86_64-linux-gnu/libc.so.6")
48 (import LIBC6 "execve" "int"
49         "void*" ; pathname
50         "void*" ; argv[]
51         "void*" ; env[]
52         )
53 (import LIBC6 "dup2" "int"
54         "int" ; oldfd
55         "int" ; newfd
56         )
57 (import LIBC6 "poll" "int"
58         "void*" ; struct pollfd *fds
59         "int" ; nfds_t nfds
60         "int" ; timeout
61         )
62
63 (constant 'F_GETFD 1)
64
65 ; <environ> is a 64-bit char** variable, rather than a function
66 (import LIBC6 "environ")
67
68 ; Return the 64-bit value of environ
69 (define (environ@) ((unpack "Lu" (address environ)) 0))
70
71 ; Pack a list of void* pointers into an array with a final null
72 ; pointer added.
73 (define (pack-ptrs PTRS)
74   (pack (dup "Lu" (+ 1 (length PTRS))) (append PTRS (list 0))))
75
76 ; Prepare the binary data setup for calling execve to execute <cmd>
77 ; via "sh -c 'exec <cmd>'" and the given environment <ENV>. If <ENV>
78 ; is <true>, then the current process environment is used, otherwise
79 ; it should be a list of name-value pairs (strings).
80 (define (wrapper cmd IO ENV)
81   (letn ((ARGS (list "/bin/sh" "-c" (string "exec " cmd)))
82          (ARGV (pack-ptrs (find-all nil ARGS (address $it) !=)))
83          (EV (if (list? ENV) (map (curry format "%s=%s") ENV)))
84          (ENVP (if (= true ENV) (environ@)
85                  EV (pack-ptrs (find-all nil EV (address $it) !=))))
86          )
87     (map dup2 (list (IO 0 0) (IO 1 1) (IO 1 1)) '(0 1 2))
88     (map close (flat IO))
89     (execve "/bin/sh" ARGV (or ENVP 0))
90     (exit 1)))
91
92 ; Create a coproc FOOP object which holds PID and pipe ends.
93 (define (coproc:coproc cmd ENV)
94   (let ((IO (list (pipe) (pipe))))
95     (list (context)
96           (fork (wrapper cmd IO ENV))
97           (begin (close (IO 0 0)) (close (IO 1 1)) (IO 0 1))
98           (IO 1 0))))
99
100 ; Wait up to <waitms> for input from the coproc, then read one line of
101 ; up to <limit> bytes and return. The final newline, if any, is
102 ; chopped off.
103 (define (gets limit waitms)
104   (let ((buffer "")
105         (delay (if (number? waitms) (* waitms 1000) 100000))
106         (maxsize (if (number? limit) limit 1000000))
107         )
108     (and (net-select (self 3) "r" delay)
109          (read (self 3) buffer maxsize "\n")
110          (if (empty? buffer) buffer
111            (!= "\n" (buffer -1)) buffer
112            (chop buffer))
113          )))
114
115 ; Write <str> to the coproc.
116 (define (put0 str)
117   (write (self 2) str (length str)))
118
119 ; Write <str> to the coproc plus a newline.
120 (define (puts str)
121   (write-line (self 2) str (length str)))
122
123 ; First :puts, then :gets
124 (define (pug str limit waitms) (puts str) (gets limit waitms))
125
126 ; First :puts, then collect :gets until nil
127 (define (pugs str limit waitms) (puts str) (collect (gets limit waitms)))
128
129 ; Poll the stdin pipe
130 (define (running? (timeout 1))
131   (let ((FDS (pack "Lu" (self 2))))
132     (>= 0 (poll FDS 1 timeout))))