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