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