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