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