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