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