sync
[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            stdout.printf("cd %s; %s\n" , this.cfg.cwd , string.joinv(" ", this.cfg.args));
227         }
228         
229         Process.spawn_async_with_pipes (
230                 this.cfg.cwd,
231                 this.cfg.args,
232                 this.cfg.env,
233                 SpawnFlags.SEARCH_PATH | SpawnFlags.DO_NOT_REAP_CHILD,
234                 null,
235                 out this.pid,
236                 out standard_input,
237                 out standard_output,
238                             out standard_error);
239
240                 // stdout:
241         
242                 
243         //print(JSON.stringify(gret));    
244          
245         if (this.cfg.debug) {
246             
247             stdout.printf("PID: %d\n" ,this.pid);
248         }
249         
250         this.ref(); // additional ref - cleared on tidyup...
251         
252         this.in_ch = new GLib.IOChannel.unix_new(standard_input);
253         this.out_ch = new GLib.IOChannel.unix_new(standard_output);
254         this.err_ch = new GLib.IOChannel.unix_new(standard_error);
255         
256         // make everything non-blocking!
257         
258         
259             
260         // using NONBLOCKING only works if io_add_watch
261           //returns true/false in right conditions
262           this.in_ch.set_flags (GLib.IOFlags.NONBLOCK);
263           this.out_ch.set_flags (GLib.IOFlags.NONBLOCK);
264           this.err_ch.set_flags (GLib.IOFlags.NONBLOCK);
265                    
266       
267
268  
269         ChildWatch.add (this.pid, this.on_child_watch);
270             
271                           
272         
273         
274        
275             
276             // add handlers for output and stderr.
277         
278         this.out_src = (int) this.out_ch.add_watch (
279             IOCondition.OUT | IOCondition.IN  | IOCondition.PRI |  IOCondition.HUP |  IOCondition.ERR  ,
280             (channel, condition) => {
281                 return this.read(channel);
282                 //return this.out_ch != null ? this.read(this.out_ch) : true;
283             }
284         );
285         this.err_src = (int) this.err_ch.add_watch (
286                  IOCondition.OUT | IOCondition.IN  | IOCondition.PRI |  IOCondition.HUP |  IOCondition.ERR  ,
287             (channel, condition) => {
288                return this.read(channel);
289                //return this.err_ch != null ? this.read(this.err_ch)  : true;
290             }
291         );
292               
293         
294         // call input.. 
295         if (this.pid > -1) {
296             // child can exit before we get this far..
297             if (this.cfg.input != null) {
298                         if (this.cfg.debug) print("Trying to call listeners");
299                 try {
300                     this.write(this.cfg.input());
301                      // this probably needs to be a bit smarter...
302                     //but... let's close input now..
303                     this.in_ch.shutdown(true);
304                     this.in_ch = null;
305                     
306                     
307                 } catch (Error e) {
308                     this.tidyup();
309                     return;
310                   //  throw e;
311                     
312                 }
313                 
314             }
315             
316         }
317                 // async - if running - return..
318         if (this.cfg.async && this.pid > -1) {
319                //this.ref();
320             return;
321         }
322          
323         // start mainloop if not async..
324         
325         if (this.pid > -1) {
326              //print("starting main loop");
327              //if (this.cfg.debug) {
328              //  
329              // }
330                 this.ctx =  new  MainLoop ();
331             this.ctx.run(); // wait fore exit?
332             
333             //print("main_loop done!");
334         } else {
335             this.tidyup(); // tidyup get's called in main loop. 
336         }
337         
338         
339         if (this.cfg.exceptions && this.result != 0) {
340                         var errstr = string.joinv(" ", this.cfg.args) + "\n";
341                         errstr += this.output;
342                         errstr += this.output.length > 0 ? "\n" : "";
343                         errstr += this.stderr;
344                         //print("Throwing execute error:%s\n", errstr);
345             throw   new SpawnError.EXECUTE_ERROR(errstr);
346             //this.toString = function() { return this.stderr; };
347             ///throw new Exception this; // we throw self...
348         }
349         // finally throw, or return self..
350         return;
351     
352     }
353     
354     void on_child_watch(GLib.Pid  w_pid, int result)  {
355             
356             this.result = result;
357             if (this.cfg.debug) {
358                 stdout.printf("child_watch_add : result:%d\n", result);
359             }
360            
361             this.read(this.out_ch);
362             this.read(this.err_ch);
363             
364             
365             Process.close_pid(this.pid);
366             this.pid = -1;
367             if (this.ctx != null) {
368                 this.ctx.quit();
369                 this.ctx = null;
370
371             }
372             //print("child process done - running callback, then tidyup");
373             this.on_finished(this.result, this.output + (this.output.length > 0 ? "\n" : "") + this.stderr);
374            // this.unref();
375             this.tidyup();
376             
377         //print("DONE TIDYUP");
378
379
380         }
381         
382     
383
384     private void tidyup()
385     {
386         //print("Tidyup\n"); 
387         if (this.pid > -1) {
388             Process.close_pid(this.pid); // hopefully kills it..
389             this.pid = -1;
390         }
391         try {
392             if (this.in_ch != null)  this.in_ch.shutdown(true);
393             if (this.out_ch != null)  this.out_ch.shutdown(true);
394             if (this.err_ch != null)  this.err_ch.shutdown(true);
395         } catch (Error e) {
396             // error shutting down
397         }
398         // blank out channels
399         this.in_ch = null;
400         this.err_ch = null;
401         this.out_ch = null;
402         // rmeove listeners !! important otherwise we kill the CPU
403         //if (this.err_src > -1 ) GLib.source_remove(this.err_src);
404         //if (this.out_src > -1 ) GLib.source_remove(this.out_src);
405         this.err_src = -1;
406         this.out_src = -1;
407         //this.unref();
408     }
409     
410     
411     /**
412      * write to stdin of process
413      * @arg str {String} string to write to stdin of process
414      * @returns GLib.IOStatus (0 == error, 1= NORMAL)
415      */
416     private int write(string str) throws Error // write a line to 
417     {
418         if (this.in_ch == null) {
419             return 0; // input is closed
420         }
421         //print("write: " + str);
422         // NEEDS GIR FIX! for return value.. let's ignore for the time being..
423         //var ret = {};
424         size_t written;
425         var res = this.in_ch.write_chars(str.to_utf8(), out written);
426         
427         //print("write_char retunred:" + JSON.stringify(res) +  ' ' +JSON.stringify(ret)  );
428         
429         if (res != GLib.IOStatus.NORMAL) {
430             throw new SpawnError.WRITE_ERROR("Write failed");
431         }
432         //return ret.value;
433         return str.length;
434         
435     }
436  
437     
438     /**
439      * read from pipe and call appropriate listerner and add to output or stderr string.
440      * @arg giochannel to read from.
441      * @returns none
442      */
443     private bool read(IOChannel ch) 
444     {
445         string prop = (ch == this.out_ch) ? "output" : "stderr";
446        // print("prop: " + prop);
447         //print ("spawn.read: %s\n", prop);
448         
449         //print(JSON.stringify(ch, null,4));
450         while (true) {
451             string buffer;
452             size_t term_pos;
453             size_t len;
454             IOStatus status;
455             
456             if (this.pid < 0) {
457                 return false; // spawn complete + closed... can't read any more.
458             }
459             
460             try {
461                                 var cond = ch.get_buffer_condition();
462                                 //if ((cond & GLib.IOCondition.ERR) > 0) {
463                                 //      return false;
464                                 //}
465                                 //if ((cond & GLib.IOCondition.IN) < 1) {
466                                 //      return false;
467                                 //}
468                 status = ch.read_line( out buffer,  out len,  out term_pos );
469             } catch (Error e) {
470                 //FIXme
471                return false;
472                 
473             }
474             if (buffer == null) {
475                         return false;
476                 }
477             //print("got buffer of %s\n", buffer);
478             // print('status: '  +JSON.stringify(status));
479             // print(JSON.stringify(x));
480              switch(status) {
481                 case GLib.IOStatus.NORMAL:
482                 
483                     //write(fn, x.str);
484                     
485                     //if (this.listeners[prop]) {
486                     //    this.listeners[prop].call(this, x.str_return);
487                     //}
488                     if (ch == this.out_ch) {
489                         this.output += buffer;
490                         if (this.cfg.output != null) {
491                                 this.cfg.output(  buffer);                  
492                         }
493                     } else {
494                         this.stderr += buffer;
495                     }
496                     //_this[prop] += x.str_return;
497                     //if (this.cfg.debug) {
498                        // stdout.printf("%s : %s", prop , buffer);
499                     //}
500                     if (this.cfg.async) {
501                          
502                         if ( Gtk.events_pending()) {
503                              Gtk.main_iteration();
504                         }
505                     }
506                     
507                     //this.ctx.iteration(true);
508                    continue;
509                 case GLib.IOStatus.AGAIN:
510                     //print("Should be called again.. waiting for more data..");
511                             return true;
512                     //break;
513                 case GLib.IOStatus.ERROR:    
514                 case GLib.IOStatus.EOF:
515                             return false;
516                     //break;
517                 
518             }
519             break;
520         }
521        
522         //print("RETURNING");
523          return false; // allow it to be called again..
524     }
525     
526 }
527   /*
528 // test
529 try { 
530     Seed.print(run({
531         args: ['ls', '/tmp'],
532         debug : true
533     }));
534 } catch (e) { print(JSON.stringify(e)); }
535  
536 var secs = (new Date()).getSeconds() 
537
538 try {      
539 Seed.print(run({
540     args: ['/bin/touch', '/tmp/spawntest-' + secs ],
541     debug : true
542 }));
543 } catch (e) { print( 'Error: ' + JSON.stringify(e)); }
544
545  
546  */
547