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