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