Spawn.vala
[gitlive] / Spawn.vala
1
2 /// # valac  --pkg gio-2.0 --pkg gtk+-3.0  --pkg posix Spawn.vala -o /tmp/Spawn
3
4 using GLib;
5 using Gtk;
6 // compile valac 
7
8
9
10 ///using Gee; // for array list?
11 /*
12 static int main (string[] args) {
13     // A reference to our file
14     
15     var cfg = new SpawnConfig("", { "ls" } , { "" });
16     cfg.setHandlers(
17             (line) => {
18                     stdout.printf("%s\n", line);
19             },
20             null,null,null );
21     cfg.setOptions(
22         false, // async
23         false, // exceptions?? needed??
24         false  // debug???
25     );
26     try {
27         new Spawn(cfg);
28        
29     } catch (Error e) {
30         stdout.printf("Error %s", e.message);
31     }
32     
33     return 0;
34
35 }
36 */
37 //var Gio      = imports.gi.Gio;
38 //var GLib      = imports.gi.GLib;
39
40
41 /**
42 * @namespace Spawn
43
44 * Library to wrap GLib.spawn_async_with_pipes
45
46 * usage:
47 * v 
48 *
49 *var output = new Spawn( SpawnConfig() {
50     cwd = "/home",  // empty string to default to homedirectory.
51     args = {"ls", "-l" },
52     evn = {},
53     ouput  = (line) => { stdout.printf("%d\n", line); }
54     stderr  = (line) => { stdout.printf("%d\n", line); }
55     input  = () => { return "xxx"; }
56 };
57 *
58 *
59 */
60 public delegate void SpawnOutput(string line);
61 public delegate void SpawnErr(string line);
62 public delegate string SpawnInput();
63 public delegate void SpawnFinish(int result, string output);
64  
65
66 public class  SpawnConfig {
67     public string cwd;
68     public string[] args;
69     public string[]  env;
70     public bool async;
71     public bool exceptions; // fire exceptions.
72     public bool debug; // fire exceptions.
73     
74     public SpawnOutput output;
75     public SpawnErr stderr;
76     public SpawnInput input;
77  
78     // defaults..
79     public SpawnConfig(string cwd,
80             string[] args,
81             string[] env
82         ) {
83         this.cwd = cwd;
84         this.args = args;
85         this.env = env;
86          
87         async = false;
88         exceptions = true;
89         debug = false;
90         
91         output = null;
92         stderr = null;
93         input = null;
94         
95     }
96     
97   
98  
99     public void setHandlers(
100             SpawnOutput? output,
101             SpawnErr? stderr,
102             SpawnInput? input 
103  
104          ) {
105         this.output = output;
106         this.stderr = stderr;
107         this.input = input;
108         
109     }
110     
111     
112 }
113
114 public errordomain SpawnError {
115     NO_ARGS,
116     WRITE_ERROR,
117     EXECUTE_ERROR
118
119 }
120
121 /**
122  * @class Spawn
123  * @param cfg {SpawnConfig} settings - see properties.
124  * 
125  * @arg cwd {String}            working directory. (defaults to home directory)
126  * @arg args {Array}            arguments eg. [ 'ls', '-l' ]
127  * @arg listeners {Object} (optional) handlers for output, stderr, input
128  *     stderr/output both receive output line as argument
129  *     input should return any standard input
130  *     finish recieves result as argument.
131  * @arg env {Array}             enviroment eg. [ 'GITDIR=/home/test' ]
132  * @arg async {Boolean} (optional)return instantly, or wait for exit. (default no)
133  * @arg exceptions {Boolean}    throw exception on failure (default no)
134  * @arg debug {Boolean}    print out what's going on.. (default no)
135  * 
136  */
137
138
139 public class Spawn : Object
140 {
141
142     SpawnConfig cfg;
143
144     public Spawn(SpawnConfig cfg) throws Error
145     {
146        
147      
148         this.cfg = cfg;
149         this.output = "";
150         this.stderr = "";
151     
152         this.cfg.cwd =  this.cfg.cwd.length  < 1 ? GLib.Environment.get_home_dir() : this.cfg.cwd;
153         if (this.cfg.args.length < 0) {
154             throw new SpawnError.NO_ARGS("No arguments");
155         }
156         if (!this.cfg.async) {
157                 this.run((res, output) => { });
158         }
159     
160     }
161
162     
163     MainLoop ctx = null; // the mainloop ctx.
164     
165     /**
166      * @property output {String} resulting output
167      */
168     public string output;
169     /**
170      * @property stderr {String} resulting output from stderr
171      */
172     public string stderr;
173      /**
174      * @property result {Number} execution result.
175      */
176     int result= 0;
177     /**
178      * @property pid {Number} pid of child process (of false if it's not running)
179      */
180     int  pid = -1;
181     /**
182      * @property in_ch {GLib.IOChannel} input io channel
183      */
184     IOChannel in_ch = null;
185     /**
186      * @property out_ch {GLib.IOChannel} output io channel
187      */
188     IOChannel out_ch = null;
189     /**
190      * @property err_ch {GLib.IOChannel} stderr io channel
191      */
192     IOChannel err_ch = null;
193     /**
194      * @property err_src {int} the watch for errors
195      */
196     
197     int err_src = -1;
198       /**
199      * @property err_src {int} the watch for output
200      */
201     int out_src = -1;
202     
203     
204     unowned SpawnFinish on_finished;
205     
206     /**
207      * 
208      * @method run
209      * Run the configured command.
210      * result is applied to object properties (eg. '?' or 'stderr')
211      * @returns {Object} self.
212      */
213     public void run(  SpawnFinish  finished_cb) throws SpawnError, GLib.SpawnError, GLib.IOChannelError
214     {
215         
216         this.on_finished   = finished_cb;
217         err_src = -1;
218         out_src = -1;
219         int standard_input;
220         int standard_output;
221         int standard_error;
222
223
224         
225         if (this.cfg.debug) {
226            GLib.debug("cd %s; %s\n" , this.cfg.cwd , string.joinv(" ", this.cfg.args));
227         }
228         
229                 // stdout:
230         if (!this.cfg.async) {
231                         string ls_stdout;
232                         string ls_stderr;
233                         int ls_status;
234
235                         Process.spawn_sync (
236                                             this.cfg.cwd,
237                                         this.cfg.args,
238                                             this.cfg.env,
239                                                         SpawnFlags.SEARCH_PATH,
240                                                         null,
241                                                         out ls_stdout,
242                                                         out ls_stderr,
243                                                         out ls_status
244                         );
245                         
246                 }
247                 
248                     
249                     
250         
251         Process.spawn_async_with_pipes (
252                 this.cfg.cwd,
253                 this.cfg.args,
254                 this.cfg.env,
255                 SpawnFlags.SEARCH_PATH | SpawnFlags.DO_NOT_REAP_CHILD,
256                 null,
257                 out this.pid,
258                 out standard_input,
259                 out standard_output,
260                             out standard_error);
261
262         
263
264                 
265         //print(JSON.stringify(gret));    
266          
267         if (this.cfg.debug) {
268             
269             GLib.debug("PID: %d\n" ,this.pid);
270         }
271         
272         this.ref(); // additional ref - cleared on tidyup...
273         
274         this.in_ch = new GLib.IOChannel.unix_new(standard_input);
275         this.out_ch = new GLib.IOChannel.unix_new(standard_output);
276         this.err_ch = new GLib.IOChannel.unix_new(standard_error);
277         
278         // make everything non-blocking!
279         
280         
281             
282         // using NONBLOCKING only works if io_add_watch
283           //returns true/false in right conditions
284           this.in_ch.set_flags (GLib.IOFlags.NONBLOCK);
285           this.out_ch.set_flags (GLib.IOFlags.NONBLOCK);
286           this.err_ch.set_flags (GLib.IOFlags.NONBLOCK);
287                    
288       
289
290  
291         ChildWatch.add (this.pid, this.on_child_watch);
292             
293                           
294         
295         
296        
297             
298             // add handlers for output and stderr.
299         
300         this.out_src = (int) this.out_ch.add_watch (
301             IOCondition.OUT | IOCondition.IN  | IOCondition.PRI |  IOCondition.HUP |  IOCondition.ERR  ,
302             (channel, condition) => {
303                 return this.read(channel);
304                 //return this.out_ch != null ? this.read(this.out_ch) : true;
305             }
306         );
307         this.err_src = (int) this.err_ch.add_watch (
308                  IOCondition.OUT | IOCondition.IN  | IOCondition.PRI |  IOCondition.HUP |  IOCondition.ERR  ,
309             (channel, condition) => {
310                return this.read(channel);
311                //return this.err_ch != null ? this.read(this.err_ch)  : true;
312             }
313         );
314               
315         
316         // call input.. 
317         if (this.pid > -1) {
318             // child can exit before we get this far..
319             if (this.cfg.input != null) {
320                         if (this.cfg.debug) GLib.debug("Trying to call listeners");
321                 try {
322                     this.write(this.cfg.input());
323                      // this probably needs to be a bit smarter...
324                     //but... let's close input now..
325                     this.in_ch.shutdown(true);
326                     this.in_ch = null;
327                     
328                     
329                 } catch (Error e) {
330                     this.tidyup();
331                     return;
332                   //  throw e;
333                     
334                 }
335                 
336             }
337             
338         }
339                 // async - if running - return..
340         if (this.cfg.async && this.pid > -1) {
341                //this.ref();
342             return;
343         }
344          
345         // start mainloop if not async..
346         
347         if (this.pid > -1) {
348              //print("starting main loop");
349              if (this.cfg.debug) {
350                 GLib.debug("starting main loop");
351              }
352                 this.ctx =  new  MainLoop ();
353             this.ctx.run(); // wait fore exit?
354              if (this.cfg.debug) {
355                 GLib.debug(" main loop done");
356              }
357             //print("main_loop done!");
358         } else {
359             this.tidyup(); // tidyup get's called in main loop. 
360         }
361         
362         
363         if (this.cfg.exceptions && this.result != 0) {
364                         var errstr = string.joinv(" ", this.cfg.args) + "\n";
365                         errstr += this.output;
366                         errstr += this.output.length > 0 ? "\n" : "";
367                         errstr += this.stderr;
368                         //print("Throwing execute error:%s\n", errstr);
369             throw   new SpawnError.EXECUTE_ERROR(errstr);
370             //this.toString = function() { return this.stderr; };
371             ///throw new Exception this; // we throw self...
372         }
373         // finally throw, or return self..
374         return;
375     
376     }
377     
378     void on_child_watch(GLib.Pid  w_pid, int result)  {
379             
380             this.result = result;
381             if (this.cfg.debug) {
382                 stdout.printf("child_watch_add : result:%d\n", result);
383             }
384            
385             this.read(this.out_ch);
386             this.read(this.err_ch);
387             
388             
389             Process.close_pid(this.pid);
390             this.pid = -1;
391             if (this.ctx != null) {
392                 this.ctx.quit();
393                 this.ctx = null;
394
395             }
396             //print("child process done - running callback, then tidyup");
397             this.on_finished(this.result, this.output + (this.output.length > 0 ? "\n" : "") + this.stderr);
398            // this.unref();
399             this.tidyup();
400             
401         //print("DONE TIDYUP");
402
403
404         }
405         
406     
407
408     private void tidyup()
409     {
410         if (this.cfg.debug)  {
411                 GLib.debug("tidyup");
412         }
413         
414         //print("Tidyup\n"); 
415         if (this.pid > -1) {
416             Process.close_pid(this.pid); // hopefully kills it..
417             this.pid = -1;
418         }
419         try {
420             if (this.in_ch != null)  this.in_ch.shutdown(true);
421             if (this.out_ch != null)  this.out_ch.shutdown(true);
422             if (this.err_ch != null)  this.err_ch.shutdown(true);
423         } catch (Error e) {
424             // error shutting down
425         }
426         // blank out channels
427         this.in_ch = null;
428         this.err_ch = null;
429         this.out_ch = null;
430         // rmeove listeners !! important otherwise we kill the CPU
431         //if (this.err_src > -1 ) GLib.source_remove(this.err_src);
432         //if (this.out_src > -1 ) GLib.source_remove(this.out_src);
433         this.err_src = -1;
434         this.out_src = -1;
435         //this.unref();
436     }
437     
438     
439     /**
440      * write to stdin of process
441      * @arg str {String} string to write to stdin of process
442      * @returns GLib.IOStatus (0 == error, 1= NORMAL)
443      */
444     private int write(string str) throws Error // write a line to 
445     {
446         if (this.in_ch == null) {
447             return 0; // input is closed
448         }
449         //print("write: " + str);
450         // NEEDS GIR FIX! for return value.. let's ignore for the time being..
451         //var ret = {};
452         size_t written;
453         var res = this.in_ch.write_chars(str.to_utf8(), out written);
454         
455         //print("write_char retunred:" + JSON.stringify(res) +  ' ' +JSON.stringify(ret)  );
456         
457         if (res != GLib.IOStatus.NORMAL) {
458             throw new SpawnError.WRITE_ERROR("Write failed");
459         }
460         //return ret.value;
461         return str.length;
462         
463     }
464  
465     
466     /**
467      * read from pipe and call appropriate listerner and add to output or stderr string.
468      * @arg giochannel to read from.
469      * @returns none
470      */
471     private bool read(IOChannel ch) 
472     {
473         string prop = (ch == this.out_ch) ? "output" : "stderr";
474        // print("prop: " + prop);
475         //print ("spawn.read: %s\n", prop);
476         
477         //print(JSON.stringify(ch, null,4));
478         while (true) {
479             string buffer;
480             size_t term_pos;
481             size_t len;
482             IOStatus status;
483             
484             if (this.pid < 0) {
485                 return false; // spawn complete + closed... can't read any more.
486             }
487             
488             try {
489                                 var cond = ch.get_buffer_condition();
490                                 //if ((cond & GLib.IOCondition.ERR) > 0) {
491                                 //      return false;
492                                 //}
493                                 //if ((cond & GLib.IOCondition.IN) < 1) {
494                                 //      return false;
495                                 //}
496                 status = ch.read_line( out buffer,  out len,  out term_pos );
497             } catch (Error e) {
498                 //FIXme
499                return false;
500                 
501             }
502             if (buffer == null) {
503                         return false;
504                 }
505             //print("got buffer of %s\n", buffer);
506             // print('status: '  +JSON.stringify(status));
507             // print(JSON.stringify(x));
508              switch(status) {
509                 case GLib.IOStatus.NORMAL:
510                 
511                     //write(fn, x.str);
512                     
513                     //if (this.listeners[prop]) {
514                     //    this.listeners[prop].call(this, x.str_return);
515                     //}
516                     if (ch == this.out_ch) {
517                         this.output += buffer;
518                         if (this.cfg.output != null) {
519                                 this.cfg.output(  buffer);                  
520                         }
521                     } else {
522                         this.stderr += buffer;
523                     }
524                     //_this[prop] += x.str_return;
525                     //if (this.cfg.debug) {
526                        // stdout.printf("%s : %s", prop , buffer);
527                     //}
528                     if (this.cfg.async) {
529                          
530                         if ( Gtk.events_pending()) {
531                              Gtk.main_iteration();
532                         }
533                     }
534                     
535                     //this.ctx.iteration(true);
536                    continue;
537                 case GLib.IOStatus.AGAIN:
538                     //print("Should be called again.. waiting for more data..");
539                             return true;
540                     //break;
541                 case GLib.IOStatus.ERROR:    
542                 case GLib.IOStatus.EOF:
543                             return false;
544                     //break;
545                 
546             }
547             break;
548         }
549        
550         //print("RETURNING");
551          return false; // allow it to be called again..
552     }
553     
554 }
555   /*
556 // test
557 try { 
558     Seed.print(run({
559         args: ['ls', '/tmp'],
560         debug : true
561     }));
562 } catch (e) { print(JSON.stringify(e)); }
563  
564 var secs = (new Date()).getSeconds() 
565
566 try {      
567 Seed.print(run({
568     args: ['/bin/touch', '/tmp/spawntest-' + secs ],
569     debug : true
570 }));
571 } catch (e) { print( 'Error: ' + JSON.stringify(e)); }
572
573  
574  */
575