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