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