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