Uncommited changes synced
[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   
98  
99     public void setHandlers(
100             SpawnOutput? output,
101             SpawnErr? stderr,
102             SpawnInput? input 
103  
104          ) {
105         this.output = output;
106         this.stderr = stderr;
107         this.input = input;
108         
109     }
110     
111     
112 }
113
114 public errordomain SpawnError {
115     NO_ARGS,
116     WRITE_ERROR,
117     EXECUTE_ERROR
118
119 }
120
121 /**
122  * @class Spawn
123  * @param cfg {SpawnConfig} settings - see properties.
124  * 
125  * @arg cwd {String}            working directory. (defaults to home directory)
126  * @arg args {Array}            arguments eg. [ 'ls', '-l' ]
127  * @arg listeners {Object} (optional) handlers for output, stderr, input
128  *     stderr/output both receive output line as argument
129  *     input should return any standard input
130  *     finish recieves result as argument.
131  * @arg env {Array}             enviroment eg. [ 'GITDIR=/home/test' ]
132  * @arg async {Boolean} (optional)return instantly, or wait for exit. (default no)
133  * @arg exceptions {Boolean}    throw exception on failure (default no)
134  * @arg debug {Boolean}    print out what's going on.. (default no)
135  * 
136  */
137
138
139 public class Spawn : Object
140 {
141
142     SpawnConfig cfg;
143
144     public Spawn(SpawnConfig cfg) throws Error
145     {
146        
147      
148         this.cfg = cfg;
149         this.output = "";
150         this.stderr = "";
151     
152         this.cfg.cwd =  this.cfg.cwd.length  < 1 ? GLib.Environment.get_home_dir() : this.cfg.cwd;
153         if (this.cfg.args.length < 0) {
154             throw new SpawnError.NO_ARGS("No arguments");
155         }
156         if (!this.cfg.async) {
157                 this.run((res, output) => { });
158         }
159     
160     }
161
162     
163     MainLoop ctx = null; // the mainloop ctx.
164     
165     /**
166      * @property output {String} resulting output
167      */
168     public string output;
169     /**
170      * @property stderr {String} resulting output from stderr
171      */
172     public string stderr;
173      /**
174      * @property result {Number} execution result.
175      */
176     public int result= 0;
177     /**
178      * @property pid {Number} pid of child process (of false if it's not running)
179      */
180     int  pid = -1;
181     /**
182      * @property in_ch {GLib.IOChannel} input io channel
183      */
184     IOChannel in_ch = null;
185     /**
186      * @property out_ch {GLib.IOChannel} output io channel
187      */
188     IOChannel out_ch = null;
189     /**
190      * @property err_ch {GLib.IOChannel} stderr io channel
191      */
192     IOChannel err_ch = null;
193     /**
194      * @property err_src {int} the watch for errors
195      */
196     
197     int err_src = -1;
198       /**
199      * @property err_src {int} the watch for output
200      */
201     int out_src = -1;
202     
203     
204     unowned SpawnFinish on_finished;
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(  SpawnFinish  finished_cb) throws SpawnError, GLib.SpawnError, GLib.IOChannelError
214     {
215         
216         this.on_finished   = finished_cb;
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            GLib.debug("cd %s; %s\n" , this.cfg.cwd , string.joinv(" ", this.cfg.args));
227         }
228         
229                 // stdout:
230         if (!this.cfg.async) {
231                         string ls_stdout;
232                         string ls_stderr;
233                         int ls_status;
234
235                         Process.spawn_sync (
236                                             this.cfg.cwd,
237                                         this.cfg.args,
238                                             this.cfg.env,
239                                                         SpawnFlags.SEARCH_PATH,
240                                                         null,
241                                                         out ls_stdout,
242                                                         out ls_stderr,
243                                                         out ls_status
244                         );
245                         this.output = ls_stdout;
246                         this.stderr = ls_stderr;
247                         this.result = ls_status;
248                         if (this.cfg.exceptions && this.result != 0) {
249                                 var errstr = string.joinv(" ", this.cfg.args) + "\n";
250                                 errstr += this.output;
251                                 errstr += this.output.length > 0 ? "\n" : "";
252                                 errstr += this.stderr;
253                                 //print("Throwing execute error:%s\n", errstr);
254                         throw   new SpawnError.EXECUTE_ERROR(errstr);
255                         //this.toString = function() { return this.stderr; };
256                         ///throw new Exception this; // we throw self...
257                     }
258                     return;
259                         
260                 }
261                 
262                     
263                     
264         
265         Process.spawn_async_with_pipes (
266                 this.cfg.cwd,
267                 this.cfg.args,
268                 this.cfg.env,
269                 SpawnFlags.SEARCH_PATH | SpawnFlags.DO_NOT_REAP_CHILD,
270                 null,
271                 out this.pid,
272                 out standard_input,
273                 out standard_output,
274                             out standard_error);
275
276         
277
278                 
279         //print(JSON.stringify(gret));    
280          
281         if (this.cfg.debug) {
282             
283             GLib.debug("PID: %d\n" ,this.pid);
284         }
285         
286         this.ref(); // additional ref - cleared on tidyup...
287         
288         this.in_ch = new GLib.IOChannel.unix_new(standard_input);
289         this.out_ch = new GLib.IOChannel.unix_new(standard_output);
290         this.err_ch = new GLib.IOChannel.unix_new(standard_error);
291         
292         // make everything non-blocking!
293         
294         
295             
296         // using NONBLOCKING only works if io_add_watch
297           //returns true/false in right conditions
298           this.in_ch.set_flags (GLib.IOFlags.NONBLOCK);
299           this.out_ch.set_flags (GLib.IOFlags.NONBLOCK);
300           this.err_ch.set_flags (GLib.IOFlags.NONBLOCK);
301                    
302       
303
304  
305         ChildWatch.add (this.pid, this.on_child_watch);
306             
307                           
308         
309         
310        
311             
312             // add handlers for output and stderr.
313         
314         this.out_src = (int) this.out_ch.add_watch (
315             IOCondition.OUT | IOCondition.IN  | IOCondition.PRI |  IOCondition.HUP |  IOCondition.ERR  ,
316             (channel, condition) => {
317                 return this.read(channel);
318                 //return this.out_ch != null ? this.read(this.out_ch) : true;
319             }
320         );
321         this.err_src = (int) this.err_ch.add_watch (
322                  IOCondition.OUT | IOCondition.IN  | IOCondition.PRI |  IOCondition.HUP |  IOCondition.ERR  ,
323             (channel, condition) => {
324                return this.read(channel);
325                //return this.err_ch != null ? this.read(this.err_ch)  : true;
326             }
327         );
328               
329         
330         // call input.. 
331         if (this.pid > -1) {
332             // child can exit before we get this far..
333             if (this.cfg.input != null) {
334                         if (this.cfg.debug) GLib.debug("Trying to call listeners");
335                 try {
336                     this.write(this.cfg.input());
337                      // this probably needs to be a bit smarter...
338                     //but... let's close input now..
339                     this.in_ch.shutdown(true);
340                     this.in_ch = null;
341                     
342                     
343                 } catch (Error e) {
344                     this.tidyup();
345                     return;
346                   //  throw e;
347                     
348                 }
349                 
350             }
351             
352         }
353                 // async - if running - return..
354         if (this.cfg.async && this.pid > -1) {
355                //this.ref();
356             return;
357         }
358          
359         // start mainloop if not async..
360         
361         if (this.pid > -1) {
362              //print("starting main loop");
363              if (this.cfg.debug) {
364                 GLib.debug("starting main loop");
365              }
366                 this.ctx =  new  MainLoop ();
367             this.ctx.run(); // wait fore exit?
368              if (this.cfg.debug) {
369                 GLib.debug(" main loop done");
370              }
371             //print("main_loop done!");
372         } else {
373             this.tidyup(); // tidyup get's called in main loop. 
374         }
375         
376         
377         if (this.cfg.exceptions && this.result != 0) {
378                         var errstr = string.joinv(" ", this.cfg.args) + "\n";
379                         errstr += this.output;
380                         errstr += this.output.length > 0 ? "\n" : "";
381                         errstr += this.stderr;
382                         //print("Throwing execute error:%s\n", errstr);
383             throw   new SpawnError.EXECUTE_ERROR(errstr);
384             //this.toString = function() { return this.stderr; };
385             ///throw new Exception this; // we throw self...
386         }
387         // finally throw, or return self..
388         return;
389     
390     }
391     
392     void on_child_watch(GLib.Pid  w_pid, int result)  {
393             
394             this.result = result;
395             if (this.cfg.debug) {
396                 stdout.printf("child_watch_add : result:%d\n", result);
397             }
398            
399             this.read(this.out_ch);
400             this.read(this.err_ch);
401             
402             
403             Process.close_pid(this.pid);
404             this.pid = -1;
405             if (this.ctx != null) {
406                 this.ctx.quit();
407                 this.ctx = null;
408
409             }
410             //print("child process done - running callback, then tidyup");
411             this.on_finished(this.result, this.output + (this.output.length > 0 ? "\n" : "") + this.stderr);
412            // this.unref();
413             this.tidyup();
414             
415         //print("DONE TIDYUP");
416
417
418         }
419         
420     
421
422     private void tidyup()
423     {
424         if (this.cfg.debug)  {
425                 GLib.debug("tidyup");
426         }
427         
428         //print("Tidyup\n"); 
429         if (this.pid > -1) {
430             Process.close_pid(this.pid); // hopefully kills it..
431             this.pid = -1;
432         }
433         try {
434             if (this.in_ch != null)  this.in_ch.shutdown(true);
435             if (this.out_ch != null)  this.out_ch.shutdown(true);
436             if (this.err_ch != null)  this.err_ch.shutdown(true);
437         } catch (Error e) {
438             // error shutting down
439         }
440         // blank out channels
441         this.in_ch = null;
442         this.err_ch = null;
443         this.out_ch = null;
444         // rmeove listeners !! important otherwise we kill the CPU
445         //if (this.err_src > -1 ) GLib.source_remove(this.err_src);
446         //if (this.out_src > -1 ) GLib.source_remove(this.out_src);
447         this.err_src = -1;
448         this.out_src = -1;
449         //this.unref();
450     }
451     
452     
453     /**
454      * write to stdin of process
455      * @arg str {String} string to write to stdin of process
456      * @returns GLib.IOStatus (0 == error, 1= NORMAL)
457      */
458     private int write(string str) throws Error // write a line to 
459     {
460         if (this.in_ch == null) {
461             return 0; // input is closed
462         }
463         //print("write: " + str);
464         // NEEDS GIR FIX! for return value.. let's ignore for the time being..
465         //var ret = {};
466         size_t written;
467         var res = this.in_ch.write_chars(str.to_utf8(), out written);
468         
469         //print("write_char retunred:" + JSON.stringify(res) +  ' ' +JSON.stringify(ret)  );
470         
471         if (res != GLib.IOStatus.NORMAL) {
472             throw new SpawnError.WRITE_ERROR("Write failed");
473         }
474         //return ret.value;
475         return str.length;
476         
477     }
478  
479     
480     /**
481      * read from pipe and call appropriate listerner and add to output or stderr string.
482      * @arg giochannel to read from.
483      * @returns none
484      */
485     private bool read(IOChannel ch) 
486     {
487         string prop = (ch == this.out_ch) ? "output" : "stderr";
488        // print("prop: " + prop);
489         //print ("spawn.read: %s\n", prop);
490         
491         //print(JSON.stringify(ch, null,4));
492         while (true) {
493             string buffer;
494             size_t term_pos;
495             size_t len;
496             IOStatus status;
497             
498             if (this.pid < 0) {
499                 return false; // spawn complete + closed... can't read any more.
500             }
501             
502             try {
503                                 var cond = ch.get_buffer_condition();
504                                 //if ((cond & GLib.IOCondition.ERR) > 0) {
505                                 //      return false;
506                                 //}
507                                 //if ((cond & GLib.IOCondition.IN) < 1) {
508                                 //      return false;
509                                 //}
510                 status = ch.read_line( out buffer,  out len,  out term_pos );
511             } catch (Error e) {
512                 //FIXme
513                return false;
514                 
515             }
516             if (buffer == null) {
517                         return false;
518                 }
519             //print("got buffer of %s\n", buffer);
520             // print('status: '  +JSON.stringify(status));
521             // print(JSON.stringify(x));
522              switch(status) {
523                 case GLib.IOStatus.NORMAL:
524                 
525                     //write(fn, x.str);
526                     
527                     //if (this.listeners[prop]) {
528                     //    this.listeners[prop].call(this, x.str_return);
529                     //}
530                     if (ch == this.out_ch) {
531                         this.output += buffer;
532                         if (this.cfg.output != null) {
533                                 this.cfg.output(  buffer);                  
534                         }
535                     } else {
536                         this.stderr += buffer;
537                     }
538                     //_this[prop] += x.str_return;
539                     //if (this.cfg.debug) {
540                        // stdout.printf("%s : %s", prop , buffer);
541                     //}
542                     if (this.cfg.async) {
543                          
544                         if ( Gtk.events_pending()) {
545                              Gtk.main_iteration();
546                         }
547                     }
548                     
549                     //this.ctx.iteration(true);
550                    continue;
551                 case GLib.IOStatus.AGAIN:
552                     //print("Should be called again.. waiting for more data..");
553                             return true;
554                     //break;
555                 case GLib.IOStatus.ERROR:    
556                 case GLib.IOStatus.EOF:
557                             return false;
558                     //break;
559                 
560             }
561             break;
562         }
563        
564         //print("RETURNING");
565          return false; // allow it to be called again..
566     }
567     
568 }
569   /*
570 // test
571 try { 
572     Seed.print(run({
573         args: ['ls', '/tmp'],
574         debug : true
575     }));
576 } catch (e) { print(JSON.stringify(e)); }
577  
578 var secs = (new Date()).getSeconds() 
579
580 try {      
581 Seed.print(run({
582     args: ['/bin/touch', '/tmp/spawntest-' + secs ],
583     debug : true
584 }));
585 } catch (e) { print( 'Error: ' + JSON.stringify(e)); }
586
587  
588  */
589