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