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         if (this.in_ch)  this.in_ch.shutdown(true);
350         if (this.out_ch)  this.out_ch.shutdown(true);
351         if (this.err_ch)  this.err_ch.shutdown(true);
352         // blank out channels
353         this.in_ch = null;
354         this.err_ch = null;
355         this.out_ch = null;
356         // rmeove listeners !! important otherwise we kill the CPU
357         if (this.err_src > -1 ) GLib.source_remove(this.err_src);
358         if (this.out_src > -1 ) GLib.source_remove(this.out_src);
359         this.err_src = -1;
360         this.out_src = -1;
361         
362     }
363     
364     
365     /**
366      * write to stdin of process
367      * @arg str {String} string to write to stdin of process
368      * @returns GLib.IOStatus (0 == error, 1= NORMAL)
369      */
370     private int write(string str) // write a line to 
371     {
372         if (this.in_ch == null) {
373             return 0; // input is closed
374         }
375         //print("write: " + str);
376         // NEEDS GIR FIX! for return value.. let's ignore for the time being..
377         //var ret = {};
378             //var res = this.in_ch.write_chars(str, str.length, ret);
379         var res = this.in_ch.write_chars(str, str.length);
380         
381         //print("write_char retunred:" + JSON.stringify(res) +  ' ' +JSON.stringify(ret)  );
382         
383         if (res != GLib.IOStatus.NORMAL) {
384             throw "Write failed";
385         }
386         //return ret.value;
387         return str.length;
388         
389     }
390     
391     /**
392      * read from pipe and call appropriate listerner and add to output or stderr string.
393      * @arg giochannel to read from.
394      * @returns none
395      */
396     private bool read(IOChannel ch) 
397     {
398         string prop = (ch == this.out_ch) ? "output" : "stderr";
399        // print("prop: " + prop);
400         var _this = this;
401         string str_return;
402         
403         //print(JSON.stringify(ch, null,4));
404         while (true) {
405             string buffer;
406             size_t term_pos;
407             size_t len;
408             IOStatus status;
409             try {
410                 status = ch.read_line( out buffer,  out len,  out term_pos );
411             } catch (Error e) {
412                 //FIXme
413                 break; // ??
414                 
415             }
416             // print('status: '  +JSON.stringify(status));
417             // print(JSON.stringify(x));
418              switch(status) {
419                 case GLib.IOStatus.NORMAL:
420                 
421                     //write(fn, x.str);
422                     //if (this.listeners[prop]) {
423                     //    this.listeners[prop].call(this, x.str_return);
424                     //}
425                     if (ch == this.out_ch) {
426                         this.output += buffer;
427                     } else {
428                         this.stderr += buffer;
429                     }
430                     //_this[prop] += x.str_return;
431                     if (this.cfg.debug) {
432                         stdout.printf("%s : %s", prop , str_return);
433                     }
434                     if (this.cfg.async) {
435                         try {
436                             if ( Gtk.events_pending()) {
437                                  Gtk.main_iteration();
438                             }
439                         } catch(Error e) {
440                             
441                         }
442                     }
443                     
444                     //this.ctx.iteration(true);
445                    continue;
446                 case GLib.IOStatus.AGAIN:
447                     //print("Should be called again.. waiting for more data..");
448                     return true;
449                     break;
450                 case GLib.IOStatus.ERROR:    
451                 case GLib.IOStatus.EOF:
452                     return false;
453                    break;
454                 
455             }
456             break;
457         }
458        
459         //print("RETURNING");
460          return false; // allow it to be called again..
461     }
462     
463 }
464   /*
465 // test
466 try { 
467     Seed.print(run({
468         args: ['ls', '/tmp'],
469         debug : true
470     }));
471 } catch (e) { print(JSON.stringify(e)); }
472  
473 var secs = (new Date()).getSeconds() 
474
475 try {      
476 Seed.print(run({
477     args: ['/bin/touch', '/tmp/spawntest-' + secs ],
478     debug : true
479 }));
480 } catch (e) { print( 'Error: ' + JSON.stringify(e)); }
481
482  
483  */
484