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