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