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