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     public void onFinish( SpawnFinish? finish ) {
98                 this.finish = finish;
99     }
100
101  
102     public void setHandlers(
103             SpawnOutput? output,
104             SpawnErr? stderr,
105             SpawnInput? input 
106  
107          ) {
108         this.output = output;
109         this.stderr = stderr;
110         this.input = input;
111         
112     }
113     
114     
115 }
116
117 public errordomain SpawnError {
118     NO_ARGS,
119     WRITE_ERROR,
120     EXECUTE_ERROR
121
122 }
123
124 /**
125  * @class Spawn
126  * @param cfg {SpawnConfig} settings - see properties.
127  * 
128  * @arg cwd {String}            working directory. (defaults to home directory)
129  * @arg args {Array}            arguments eg. [ 'ls', '-l' ]
130  * @arg listeners {Object} (optional) handlers for output, stderr, input
131  *     stderr/output both receive output line as argument
132  *     input should return any standard input
133  *     finish recieves result as argument.
134  * @arg env {Array}             enviroment eg. [ 'GITDIR=/home/test' ]
135  * @arg async {Boolean} (optional)return instantly, or wait for exit. (default no)
136  * @arg exceptions {Boolean}    throw exception on failure (default no)
137  * @arg debug {Boolean}    print out what's going on.. (default no)
138  * 
139  */
140
141
142 public class Spawn : Object
143 {
144
145     SpawnConfig cfg;
146
147     public Spawn(SpawnConfig cfg) throws Error
148     {
149        
150      
151         this.cfg = cfg;
152         this.output = "";
153         this.stderr = "";
154     
155         this.cfg.cwd =  this.cfg.cwd.length  < 1 ? GLib.Environment.get_home_dir() : this.cfg.cwd;
156         if (this.cfg.args.length < 0) {
157             throw new SpawnError.NO_ARGS("No arguments");
158         }
159         if (!this.cfg.async) {
160                 this.run((res, output) => { });
161         }
162     
163     }
164
165     
166     MainLoop ctx = null; // the mainloop ctx.
167     
168     /**
169      * @property output {String} resulting output
170      */
171     public string output;
172     /**
173      * @property stderr {String} resulting output from stderr
174      */
175     public string stderr;
176      /**
177      * @property result {Number} execution result.
178      */
179     int result= 0;
180     /**
181      * @property pid {Number} pid of child process (of false if it's not running)
182      */
183     int  pid = -1;
184     /**
185      * @property in_ch {GLib.IOChannel} input io channel
186      */
187     IOChannel in_ch = null;
188     /**
189      * @property out_ch {GLib.IOChannel} output io channel
190      */
191     IOChannel out_ch = null;
192     /**
193      * @property err_ch {GLib.IOChannel} stderr io channel
194      */
195     IOChannel err_ch = null;
196     /**
197      * @property err_src {int} the watch for errors
198      */
199     
200     int err_src = -1;
201       /**
202      * @property err_src {int} the watch for output
203      */
204     int out_src = -1;
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(unowned SpawnFinish  finished_cb) throws SpawnError, GLib.SpawnError, GLib.IOChannelError
214     {
215         
216          
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, (w_pid, result) => {
270             
271             this.result = result;
272             if (this.cfg.debug) {
273                 stdout.printf("child_watch_add : result:%d\n", result);
274             }
275            
276             this.read(this.out_ch);
277             this.read(this.err_ch);
278             
279             
280             Process.close_pid(this.pid);
281             this.pid = -1;
282             if (this.ctx != null) {
283                 this.ctx.quit();
284                 this.ctx = null;
285             }
286             this.tidyup();
287         //print("DONE TIDYUP");
288             if (this.cfg.finish != null) {
289                 this.cfg.finish(this.result);
290             }
291         });
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) print("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             return;
342         }
343          
344         // start mainloop if not async..
345         
346         if (this.pid > -1) {
347              //print("starting main loop");
348              //if (this.cfg.debug) {
349              //  
350              // }
351                 this.ctx =   MainLoop ();
352             this.ctx.run(); // wait fore exit?
353             
354             //print("main_loop done!");
355         } else {
356             this.tidyup(); // tidyup get's called in main loop. 
357         }
358         
359         
360         if (this.cfg.exceptions && this.result != 0) {
361                         var errstr = string.joinv(" ", this.cfg.args) + "\n";
362                         errstr += this.output;
363                         errstr += this.output.length > 0 ? "\n" : "";
364                         errstr += this.stderr;
365                         //print("Throwing execute error:%s\n", errstr);
366             throw   SpawnError.EXECUTE_ERROR(errstr);
367             //this.toString = function() { return this.stderr; };
368             ///throw new Exception this; // we throw self...
369         }
370         // finally throw, or return self..
371         return;
372     
373     }
374
375     private void tidyup()
376     {
377         //print("Tidyup\n"); 
378         if (this.pid > -1) {
379             Process.close_pid(this.pid); // hopefully kills it..
380             this.pid = -1;
381         }
382         try {
383             if (this.in_ch != null)  this.in_ch.shutdown(true);
384             if (this.out_ch != null)  this.out_ch.shutdown(true);
385             if (this.err_ch != null)  this.err_ch.shutdown(true);
386         } catch (Error e) {
387             // error shutting down
388         }
389         // blank out channels
390         this.in_ch = null;
391         this.err_ch = null;
392         this.out_ch = null;
393         // rmeove listeners !! important otherwise we kill the CPU
394         //if (this.err_src > -1 ) GLib.source_remove(this.err_src);
395         //if (this.out_src > -1 ) GLib.source_remove(this.out_src);
396         this.err_src = -1;
397         this.out_src = -1;
398         this.unref();
399     }
400     
401     
402     /**
403      * write to stdin of process
404      * @arg str {String} string to write to stdin of process
405      * @returns GLib.IOStatus (0 == error, 1= NORMAL)
406      */
407     private int write(string str) throws Error // write a line to 
408     {
409         if (this.in_ch == null) {
410             return 0; // input is closed
411         }
412         //print("write: " + str);
413         // NEEDS GIR FIX! for return value.. let's ignore for the time being..
414         //var ret = {};
415         size_t written;
416         var res = this.in_ch.write_chars(str.to_utf8(), out written);
417         
418         //print("write_char retunred:" + JSON.stringify(res) +  ' ' +JSON.stringify(ret)  );
419         
420         if (res != GLib.IOStatus.NORMAL) {
421             throw new SpawnError.WRITE_ERROR("Write failed");
422         }
423         //return ret.value;
424         return str.length;
425         
426     }
427  
428     
429     /**
430      * read from pipe and call appropriate listerner and add to output or stderr string.
431      * @arg giochannel to read from.
432      * @returns none
433      */
434     private bool read(IOChannel ch) 
435     {
436         string prop = (ch == this.out_ch) ? "output" : "stderr";
437        // print("prop: " + prop);
438         //print ("spawn.read: %s\n", prop);
439         
440         //print(JSON.stringify(ch, null,4));
441         while (true) {
442             string buffer;
443             size_t term_pos;
444             size_t len;
445             IOStatus status;
446             
447             if (this.pid < 0) {
448                 return false; // spawn complete + closed... can't read any more.
449             }
450             
451             try {
452                                 var cond = ch.get_buffer_condition();
453                                 //if ((cond & GLib.IOCondition.ERR) > 0) {
454                                 //      return false;
455                                 //}
456                                 //if ((cond & GLib.IOCondition.IN) < 1) {
457                                 //      return false;
458                                 //}
459                 status = ch.read_line( out buffer,  out len,  out term_pos );
460             } catch (Error e) {
461                 //FIXme
462                return false;
463                 
464             }
465             if (buffer == null) {
466                         return false;
467                 }
468             //print("got buffer of %s\n", buffer);
469             // print('status: '  +JSON.stringify(status));
470             // print(JSON.stringify(x));
471              switch(status) {
472                 case GLib.IOStatus.NORMAL:
473                 
474                     //write(fn, x.str);
475                     
476                     //if (this.listeners[prop]) {
477                     //    this.listeners[prop].call(this, x.str_return);
478                     //}
479                     if (ch == this.out_ch) {
480                         this.output += buffer;
481                         if (this.cfg.output != null) {
482                                 this.cfg.output(  buffer);                  
483                         }
484                     } else {
485                         this.stderr += buffer;
486                     }
487                     //_this[prop] += x.str_return;
488                     //if (this.cfg.debug) {
489                        // stdout.printf("%s : %s", prop , buffer);
490                     //}
491                     if (this.cfg.async) {
492                          
493                         if ( Gtk.events_pending()) {
494                              Gtk.main_iteration();
495                         }
496                     }
497                     
498                     //this.ctx.iteration(true);
499                    continue;
500                 case GLib.IOStatus.AGAIN:
501                     //print("Should be called again.. waiting for more data..");
502                             return true;
503                     //break;
504                 case GLib.IOStatus.ERROR:    
505                 case GLib.IOStatus.EOF:
506                             return false;
507                     //break;
508                 
509             }
510             break;
511         }
512        
513         //print("RETURNING");
514          return false; // allow it to be called again..
515     }
516     
517 }
518   /*
519 // test
520 try { 
521     Seed.print(run({
522         args: ['ls', '/tmp'],
523         debug : true
524     }));
525 } catch (e) { print(JSON.stringify(e)); }
526  
527 var secs = (new Date()).getSeconds() 
528
529 try {      
530 Seed.print(run({
531     args: ['/bin/touch', '/tmp/spawntest-' + secs ],
532     debug : true
533 }));
534 } catch (e) { print( 'Error: ' + JSON.stringify(e)); }
535
536  
537  */
538