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                 this.ctx = null;
247             }
248             this.tidyup();
249         //print("DONE TIDYUP");
250             if (this.cfg.finish) {
251                 this.cfg.finish(this.result);
252             }
253         });
254             
255                           
256         
257         
258         this.in_ch = new GLib.IOChannel.unix_new(standard_input);
259         this.out_ch = new GLib.IOChannel.unix_new(standard_output);
260         this.err_ch = new GLib.IOChannel.unix_new(standard_error);
261         
262         // make everything non-blocking!
263         
264         
265             
266                   // using NONBLOCKING only works if io_add_watch
267           //returns true/false in right conditions
268           this.in_ch.set_flags (GLib.IOFlags.NONBLOCK);
269           this.out_ch.set_flags (GLib.IOFlags.NONBLOCK);
270           this.err_ch.set_flags (GLib.IOFlags.NONBLOCK);
271                    
272       
273             
274             // add handlers for output and stderr.
275         
276         this.out_src = this.out_ch.add_watch (
277             IOCondition.OUT | IOCondition.IN  | IOCondition.PRI |  IOCondition.HUP |  IOCondition.ERR  ,
278             (channel, condition) => {
279                return this.read(_this.out_ch);
280             }
281         );
282         this.err_src = this.err_ch.add_watch (
283             IOCondition.OUT | IOCondition.IN  | IOCondition.PRI |  IOCondition.HUP |  IOCondition.ERR  ,
284             (channel, condition) => {
285                return this.read(_this.err_ch);
286             }
287         );
288               
289         
290         // call input.. 
291         if (this.pid > -1) {
292             // child can exit before 1we get this far..
293             if (this.cfg.input != null) {
294                 if (this.cfg.debug) print("Trying to call listeners");
295                 try {
296                     this.write(this.cfg.input());
297                      // this probably needs to be a bit smarter...
298                     //but... let's close input now..
299                     this.in_ch.close();
300                     this.in_ch = -1;
301                      
302                     
303                 } catch (Error e) {
304                     this.tidyup();
305                     throw e;
306                     
307                 }
308                 
309             }
310         }
311         // async - if running - return..
312         if (this.cfg.async && this.pid > -1) {
313             return;
314         }
315          
316         // start mainloop if not async..
317         
318         if (this.pid > -1) {
319             if (this.cfg.debug) {
320                 print("starting main loop");
321             }
322             this.ctx = new MainLoop ();
323             loop.run(); // wait fore exit?
324             
325             //print("main_loop done!");
326         } else {
327             this.tidyup(); // tidyup get's called in main loop. 
328         }
329         
330         if (this.cfg.exceptions && this.result != 0) {
331             //this.toString = function() { return this.stderr; };
332             ///throw new Exception this; // we throw self...
333         }
334         
335         // finally throw, or return self..
336         
337         return;
338     
339     }
340     
341     
342
343     private void tidyup()
344     {
345         if (this.pid > -1) {
346             Process.close_pid(this.pid); // hopefully kills it..
347             this.pid = -1;
348         }
349         try {
350             if (this.in_ch != null)  this.in_ch.shutdown(true);
351             if (this.out_ch != null)  this.out_ch.shutdown(true);
352             if (this.err_ch != null)  this.err_ch.shutdown(true);
353         } catch (Error e) {
354             // error shutting donw.
355         }
356         // blank out channels
357         this.in_ch = null;
358         this.err_ch = null;
359         this.out_ch = null;
360         // rmeove listeners !! important otherwise we kill the CPU
361         //if (this.err_src > -1 ) GLib.source_remove(this.err_src);
362         //if (this.out_src > -1 ) GLib.source_remove(this.out_src);
363         this.err_src = -1;
364         this.out_src = -1;
365         
366     }
367     
368     
369     /**
370      * write to stdin of process
371      * @arg str {String} string to write to stdin of process
372      * @returns GLib.IOStatus (0 == error, 1= NORMAL)
373      */
374     private int write(string str) // write a line to 
375     {
376         if (this.in_ch == null) {
377             return 0; // input is closed
378         }
379         //print("write: " + str);
380         // NEEDS GIR FIX! for return value.. let's ignore for the time being..
381         //var ret = {};
382             //var res = this.in_ch.write_chars(str, str.length, ret);
383         var res = this.in_ch.write_chars(str, str.length);
384         
385         //print("write_char retunred:" + JSON.stringify(res) +  ' ' +JSON.stringify(ret)  );
386         
387         if (res != GLib.IOStatus.NORMAL) {
388             throw "Write failed";
389         }
390         //return ret.value;
391         return str.length;
392         
393     }
394     
395     /**
396      * read from pipe and call appropriate listerner and add to output or stderr string.
397      * @arg giochannel to read from.
398      * @returns none
399      */
400     private bool read(IOChannel ch) 
401     {
402         string prop = (ch == this.out_ch) ? "output" : "stderr";
403        // print("prop: " + prop);
404         var _this = this;
405         string str_return;
406         
407         //print(JSON.stringify(ch, null,4));
408         while (true) {
409             string buffer;
410             size_t term_pos;
411             size_t len;
412             IOStatus status;
413             try {
414                 status = ch.read_line( out buffer,  out len,  out term_pos );
415             } catch (Error e) {
416                 //FIXme
417                 break; // ??
418                 
419             }
420             // print('status: '  +JSON.stringify(status));
421             // print(JSON.stringify(x));
422              switch(status) {
423                 case GLib.IOStatus.NORMAL:
424                 
425                     //write(fn, x.str);
426                     //if (this.listeners[prop]) {
427                     //    this.listeners[prop].call(this, x.str_return);
428                     //}
429                     if (ch == this.out_ch) {
430                         this.output += buffer;
431                     } else {
432                         this.stderr += buffer;
433                     }
434                     //_this[prop] += x.str_return;
435                     if (this.cfg.debug) {
436                         stdout.printf("%s : %s", prop , str_return);
437                     }
438                     if (this.cfg.async) {
439                         try {
440                             if ( Gtk.events_pending()) {
441                                  Gtk.main_iteration();
442                             }
443                         } catch(Error e) {
444                             
445                         }
446                     }
447                     
448                     //this.ctx.iteration(true);
449                    continue;
450                 case GLib.IOStatus.AGAIN:
451                     //print("Should be called again.. waiting for more data..");
452                     return true;
453                     break;
454                 case GLib.IOStatus.ERROR:    
455                 case GLib.IOStatus.EOF:
456                     return false;
457                    break;
458                 
459             }
460             break;
461         }
462        
463         //print("RETURNING");
464          return false; // allow it to be called again..
465     }
466     
467 }
468   /*
469 // test
470 try { 
471     Seed.print(run({
472         args: ['ls', '/tmp'],
473         debug : true
474     }));
475 } catch (e) { print(JSON.stringify(e)); }
476  
477 var secs = (new Date()).getSeconds() 
478
479 try {      
480 Seed.print(run({
481     args: ['/bin/touch', '/tmp/spawntest-' + secs ],
482     debug : true
483 }));
484 } catch (e) { print( 'Error: ' + JSON.stringify(e)); }
485
486  
487  */
488