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