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