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