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