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