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