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