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