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