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