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