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