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