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