src/Spawn.vala
[app.Builder.js] / src / Spawn.vala
1
2 /// # valac  --pkg gio-2.0    --pkg posix Spawn.vala -o /tmp/Spawn
3
4 using GLib;
5  
6
7 /**
8  * Revised version?
9  * 
10  * x = new Spawn( "/tmp", {"ls", "-l" })
11  * 
12  * // these are optionall..
13  * x.env = ..... (if you need to set one...
14  * x.output_line.connect((string str) => { 
15  *              if ( Gtk.events_pending()) { Gtk.main_iteration(); } 
16  * });
17  * x.input_line.connect(() => { return string });
18  * 
19  * x.run((int res, string output, string stderr) => { ... });
20
21  * 
22  * 
23  */
24
25
26  
27 public errordomain SpawnError {
28     NO_ARGS,
29     WRITE_ERROR,
30     EXECUTE_ERROR
31
32 }
33
34 /**
35  * @class Spawn
36  * @param cwd {String}            working directory. (defaults to home directory)
37  * @param args {Array}            arguments eg. [ 'ls', '-l' ]
38  * 
39  
40  * @arg env {Array}             enviroment eg. [ 'GITDIR=/home/test' ]
41  * @arg is_async {Boolean} (optional)return instantly, or wait for exit. (default no)
42  * @arg trhow_exceptions {Boolean}    throw exception on failure (default no)
43  
44  * 
45  */
46  
47  
48 public delegate void SpawnFinish (int res, string str, string stderr);
49
50 public class Spawn : Object
51 {
52         /**
53          * @signal input called at start to send input when process starts?
54          * @return the string or null 
55          */
56         public signal string? input();
57         /**
58          * @signal input called at start to send input when process starts?
59          * @return the string or null 
60          */
61         public signal void complete(int res, string str, string stderr);
62         /**
63          * @signal output_line called when a line is recieved from the process.
64          * Note you may want to connect this and run 
65          *   if ( Gtk.events_pending()) { Gtk.main_iteration(); }
66          * 
67          * @param {string} str 
68          */
69     public signal void output_line(string str);
70     
71         public string cwd;
72         public string[] args;
73         public string[] env;
74         
75         public bool is_async = true;
76         public bool throw_exceptions = false;
77
78     public Spawn(string cwd, string[] args) throws Error
79     {
80        
81      
82         this.cwd = cwd;
83         this.args = args;
84         this.env = {};
85         
86         this.output = "";
87         this.stderr = "";
88     
89         this.cwd =  this.cwd.length  < 1 ? GLib.Environment.get_home_dir() : this.cwd;
90         if (this.args.length < 0) {
91             throw new SpawnError.NO_ARGS("No arguments");
92         }
93         
94     
95     }
96
97     
98     MainLoop ctx = null; // the mainloop ctx.
99     
100     /**
101      * @property output {String} resulting output
102      */
103     public string output;
104     /**
105      * @property stderr {String} resulting output from stderr
106      */
107     public string stderr;
108      /**
109      * @property result {Number} execution result.
110      */
111     public int result= 0;
112     /**
113      * @property pid {Number} pid of child process (of false if it's not running)
114      */
115     int  pid = -1;
116     /**
117      * @property in_ch {GLib.IOChannel} input io channel
118      */
119     IOChannel in_ch = null;
120     /**
121      * @property out_ch {GLib.IOChannel} output io channel
122      */
123     IOChannel out_ch = null;
124     /**
125      * @property err_ch {GLib.IOChannel} stderr io channel
126      */
127     IOChannel err_ch = null;
128     /**
129      * @property err_src {int} the watch for errors
130      */
131     
132     int err_src = -1;
133       /**
134      * @property err_src {int} the watch for output
135      */
136     int out_src = -1;
137     
138     /**
139      * 
140      * @method run
141      * Run the configured command.
142      * result is applied to object properties (eg. '?' or 'stderr')
143      * @returns {Object} self.
144      */
145         public void run(SpawnFinish finish) throws SpawnError, GLib.SpawnError, GLib.IOChannelError
146         {
147                 
148                  
149                 err_src = -1;
150                 out_src = -1;
151                 int standard_input;
152                 int standard_output;
153                 int standard_error;
154
155
156                  
157                 GLib.debug("cd %s; %s" , this.cwd , string.joinv(" ", this.args));
158                 
159                 
160                 Process.spawn_async_with_pipes (
161                                 this.cwd,
162                                 this.args,
163                                 this.env,
164                                 SpawnFlags.SEARCH_PATH | SpawnFlags.DO_NOT_REAP_CHILD,
165                                 null,
166                                 out this.pid,
167                                 out standard_input,
168                                 out standard_output,
169                                 out standard_error);
170
171                 // stdout:
172
173                         
174                 //print(JSON.stringify(gret));    
175                  
176                 GLib.debug("PID: %d" ,this.pid);
177                  
178                 
179                 this.in_ch = new GLib.IOChannel.unix_new(standard_input);
180                 this.out_ch = new GLib.IOChannel.unix_new(standard_output);
181                 this.err_ch = new GLib.IOChannel.unix_new(standard_error);
182                 
183                 // make everything non-blocking!
184
185
186
187                           // using NONBLOCKING only works if io_add_watch
188                 //returns true/false in right conditions
189                 this.in_ch.set_flags (GLib.IOFlags.NONBLOCK);
190                 this.out_ch.set_flags (GLib.IOFlags.NONBLOCK);
191                 this.err_ch.set_flags (GLib.IOFlags.NONBLOCK);
192                            
193
194
195  
196                 ChildWatch.add (this.pid, (w_pid, result) => {
197                 
198                         this.result = result;
199                         GLib.debug("child_watch_add : result:%d ", result);
200                         
201                    
202                         this.read(this.out_ch);
203                         this.read(this.err_ch);
204                         
205                         
206                         Process.close_pid(this.pid);
207                         this.pid = -1;
208                         if (this.ctx != null) {
209                                 this.ctx.quit();
210                                 this.ctx = null;
211                         }
212                         this.tidyup();
213                         //print("DONE TIDYUP");
214                         
215                         finish(this.result, this.output, this.stderr);
216                         
217                 });
218             
219                           
220         
221         
222        
223             
224             // add handlers for output and stderr.
225         
226         this.out_src = (int) this.out_ch.add_watch (
227             IOCondition.OUT | IOCondition.IN  | IOCondition.PRI |  IOCondition.HUP |  IOCondition.ERR  ,
228             (channel, condition) => {
229                return this.read(this.out_ch);
230             }
231         );
232         this.err_src = (int) this.err_ch.add_watch (
233                  IOCondition.OUT | IOCondition.IN  | IOCondition.PRI |  IOCondition.HUP |  IOCondition.ERR  ,
234             (channel, condition) => {
235                return this.read(this.err_ch);
236             }
237         );
238               
239         
240         // call input.. 
241         if (this.pid > -1) {
242             // child can exit before we get this far..
243             var input = this.input();
244             if (input != null) {
245                         
246                 try {
247                     this.write(input);
248                      // this probably needs to be a bit smarter...
249                     //but... let's close input now..
250                     this.in_ch.shutdown(true);
251                     this.in_ch = null;
252                      
253                     
254                 } catch (Error e) {
255                     this.tidyup();
256                     return;
257                   //  throw e;
258                     
259                 }
260                 
261             }
262             
263         }
264         // async - if running - return..
265         if (this.is_async && this.pid > -1) {
266             return;
267         }
268          
269         // start mainloop if not async..
270         
271         if (this.pid > -1) {
272             GLib.debug("starting main loop");
273              //if (this.cfg.debug) {
274              //  
275              // }
276                 this.ctx = new MainLoop ();
277             this.ctx.run(); // wait fore exit?
278             
279             GLib.debug("main_loop done!");
280         } else {
281             this.tidyup(); // tidyup get's called in main loop. 
282         }
283         
284         if (this.throw_exceptions && this.result != 0) {
285             
286             throw new SpawnError.EXECUTE_ERROR(this.stderr);
287             //this.toString = function() { return this.stderr; };
288             ///throw new Exception this; // we throw self...
289         }
290         
291         // finally throw, or return self..
292         
293         return;
294     
295     }
296     
297     
298
299     private void tidyup()
300     {
301         if (this.pid > -1) {
302             Process.close_pid(this.pid); // hopefully kills it..
303             this.pid = -1;
304         }
305         try {
306             if (this.in_ch != null)  this.in_ch.shutdown(true);
307             if (this.out_ch != null)  this.out_ch.shutdown(true);
308             if (this.err_ch != null)  this.err_ch.shutdown(true);
309         } catch (Error e) {
310             // error shutting donw.
311         }
312         // blank out channels
313         this.in_ch = null;
314         this.err_ch = null;
315         this.out_ch = null;
316         // rmeove listeners !! important otherwise we kill the CPU
317         //if (this.err_src > -1 ) GLib.source_remove(this.err_src);
318         //if (this.out_src > -1 ) GLib.source_remove(this.out_src);
319         this.err_src = -1;
320         this.out_src = -1;
321         
322     }
323     
324     
325     /**
326      * write to stdin of process
327      * @arg str {String} string to write to stdin of process
328      * @returns GLib.IOStatus (0 == error, 1= NORMAL)
329      */
330     private int write(string str) throws Error // write a line to 
331     {
332         if (this.in_ch == null) {
333             return 0; // input is closed
334         }
335         //print("write: " + str);
336         // NEEDS GIR FIX! for return value.. let's ignore for the time being..
337         //var ret = {};
338         size_t written;
339         var res = this.in_ch.write_chars(str.to_utf8(), out written);
340         
341         //print("write_char retunred:" + JSON.stringify(res) +  ' ' +JSON.stringify(ret)  );
342         
343         if (res != GLib.IOStatus.NORMAL) {
344             throw new SpawnError.WRITE_ERROR("Write failed");
345         }
346         //return ret.value;
347         return str.length;
348         
349     }
350
351
352     
353     /**
354      * read from pipe and call appropriate listerner and add to output or stderr string.
355      * @arg giochannel to read from.
356      * @returns none
357      */
358     private bool read(IOChannel ch) 
359     {
360         string prop = (ch == this.out_ch) ? "output" : "stderr";
361        // print("prop: " + prop);
362
363         
364         //print(JSON.stringify(ch, null,4));
365         while (true) {
366             string buffer;
367             size_t term_pos;
368             size_t len;
369             IOStatus status;
370             try {
371                 status = ch.read_line( out buffer,  out len,  out term_pos );
372             } catch (Error e) {
373                 //FIXme
374                 break; // ??
375                 
376             }
377
378             // print('status: '  +JSON.stringify(status));
379             // print(JSON.stringify(x));
380              switch(status) {
381                 case GLib.IOStatus.NORMAL:
382                 
383                     //write(fn, x.str);
384                     
385                     //if (this.listeners[prop]) {
386                     //    this.listeners[prop].call(this, x.str_return);
387                     //}
388                     if (ch == this.out_ch) {
389                         this.output += buffer;
390                         this.output_line(  buffer);                  
391                         
392                     } else {
393                         this.stderr += buffer;
394                     }
395                     //_this[prop] += x.str_return;
396                     //if (this.cfg.debug) {
397                         GLib.debug("%s : %s", prop , buffer);
398                     //}
399                     if (this.is_async) {
400                          
401                         //if ( Gtk.events_pending()) {
402                         //     Gtk.main_iteration();
403                         //}
404                          
405                     }
406                     
407                     //this.ctx.iteration(true);
408                    continue;
409                 case GLib.IOStatus.AGAIN:
410                                         //print("Should be called again.. waiting for more data..");
411                             return true;
412                     //break;
413                 case GLib.IOStatus.ERROR:    
414                 case GLib.IOStatus.EOF:
415                             return false;
416                     //break;
417                 
418             }
419             break;
420         }
421        
422         //print("RETURNING");
423          return false; // allow it to be called again..
424     }
425     
426 }
427 /*
428  
429 int main (string[] args) {
430         GLib.Log.set_handler(null, 
431                 GLib.LogLevelFlags.LEVEL_DEBUG | GLib.LogLevelFlags.LEVEL_WARNING, 
432                 (dom, lvl, msg) => {
433                 print("%s: %s\n", dom, msg);
434         });
435
436         var ctx = new GLib.MainLoop ();
437         var a = new Spawn("", { "ls" , "-l"});
438         a.run((res, str, stderr) => {
439                 print(str);
440                 ctx.quit();
441         });
442         
443         
444         ctx.run(); // wait for exit?
445             
446         return 0;
447 }
448  */