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