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