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