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