sync new version
[gitlive] / Spawn.js
1 ///<script type="text/javascript">
2
3 var Gio      = imports.gi.Gio;
4 var GLib      = imports.gi.GLib;
5
6
7 /**
8 * @namespace Spawn
9
10 * Library to wrap GLib.spawn_async_with_pipes
11
12 * usage:
13
14 * Spawn = import.Spawn;
15
16 * simple version..
17 * var output = Spawn.run({
18 *   cwd : '/home',
19 *   args : [ 'ls', '-l' ],
20 *   env : [], // optional
21 *   listeners : {
22         output : function (line) { Seed.print(line); },
23 *       stderr :  function (line) {Seed.print("ERROR" + line);  },
24 *       input : function() { return 'xxx' },
25 *   }
26 *  });
27
28
29 */
30
31
32 /**
33  * @class Spawn
34  * @param cfg {Object} settings - see properties.
35  * 
36  * @arg cwd {String}            working directory. (defaults to home directory)
37  * @arg args {Array}            arguments eg. [ 'ls', '-l' ]
38  * @arg listeners {Object} (optional) handlers for output, stderr, input
39  *     stderr/output both receive output line as argument
40  *     input should return any standard input
41  *     finish recieves result as argument.
42  * @arg env {Array}             enviroment eg. [ 'GITDIR=/home/test' ]
43  * @arg async {Boolean} (optional)return instantly, or wait for exit. (default no)
44  * @arg exceptions {Boolean}    throw exception on failure (default no)
45  * @arg debug {Boolean}    print out what's going on.. (default no)
46  * 
47  */
48 function Spawn(cfg) {
49     for(var i in cfg) {
50         this[i] = cfg[i];
51     }
52     // set defaults?
53     this.listeners = this.listeners || {};
54     this.cwd =  this.cwd || GLib.get_home_dir();
55     if (!this.args || !this.args.length) {
56         throw "No arguments";
57     }
58     
59 }
60
61
62 Spawn.prototype = {
63     
64     listeners : false,
65     async : false,
66     env : null,
67     cwd: false,
68     args: false,
69     exceptions : false,
70     debug : false,
71     /**
72      * @property output {String} resulting output
73      */
74     output  : '',
75     /**
76      * @property stderr {String} resulting output from stderr
77      */
78     stderr  : '',
79      /**
80      * @property result {Number} execution result.
81      */
82     result: 0,
83     /**
84      * @property pid {Number} pid of child process (of false if it's not running)
85      */
86     pid : false,
87     
88    
89     
90     /**
91      * @property in_ch {GLib.IOChannel} input io channel
92      */
93     in_ch : false,
94     /**
95      * @property out_ch {GLib.IOChannel} output io channel
96      */
97     out_ch : false,
98     /**
99      * @property err_ch {GLib.IOChannel} stderr io channel
100      */
101     err_ch : false,
102     
103     /**
104      * 
105      * @method run
106      * Run the configured command.
107      * 
108      */
109     
110     
111     run : function()
112     {
113         
114         var _this = this;
115         
116         var err_src = false;
117         var out_src = false;
118         var ctx = false; 
119         var ret = {};
120         
121         if (this.debug) {
122             print("spawn : " + this.args.join(" "));
123         }
124         
125         GLib.spawn_async_with_pipes(this.cwd, this.args, this.env, 
126             GLib.SpawnFlags.DO_NOT_REAP_CHILD + GLib.SpawnFlags.SEARCH_PATH , 
127             null, null, ret);
128             
129         this.pid = ret.child_pid;
130         
131         if (this.debug) {
132             print("PID: " + this.pid);
133         }
134         
135         function tidyup()
136         {
137             if (_this.pid) {
138                 GLib.spawn_close_pid(_this.pid); // hopefully kills it..
139                 _this.pid = false;
140             }
141             if (_this.in_ch)  _this.in_ch.close();
142             if (_this.out_ch)  _this.out_ch.close();
143             if (_this.err_ch)  _this.err_ch.close();
144             // blank out channels
145             _this.in_ch = false;
146             _this.err_ch = false;
147             _this.out_ch = false;
148             // rmeove listeners !! important otherwise we kill the CPU
149             if (err_src !== false) GLib.source_remove(err_src);
150             if (out_src !== false) GLib.source_remove(out_src);
151             err_src = false;
152             out_src = false;
153             
154         }
155         
156         
157         
158         GLib.child_watch_add(GLib.PRIORITY_DEFAULT, this.pid, function(pid, result) {
159             _this.result = result;
160             if (_this.debug) {
161                 print("result: " + result);
162             }
163             _this.read(_this.out_ch);
164             _this.read(_this.err_ch);
165             
166             GLib.spawn_close_pid(_this.pid);
167             _this.pid = false;
168             if (ctx) {
169                 ctx.quit();
170             }
171             tidyup();
172             if (_this.listeners.finish) {
173                 _this.listeners.finish.call(this, _this.result);
174             }
175         });
176         
177         
178         this.in_ch = GLib.io_channel_unix_new(ret.standard_input);
179         this.out_ch = GLib.io_channel_unix_new(ret.standard_output);
180         this.err_ch = GLib.io_channel_unix_new(ret.standard_error);
181        
182         // make everything non-blocking!
183         this.in_ch.set_flags (GLib.IOFlags.NONBLOCK);
184         this.out_ch.set_flags (GLib.IOFlags.NONBLOCK);
185         this.err_ch.set_flags (GLib.IOFlags.NONBLOCK);
186
187       
188         // add handlers for output and stderr.
189         out_src= GLib.io_add_watch(this.out_ch, GLib.PRIORITY_DEFAULT, 
190             GLib.IOCondition.OUT + GLib.IOCondition.IN  + GLib.IOCondition.PRI, function()
191         {
192             _this.read(_this.out_ch);
193             
194         });
195         err_src= GLib.io_add_watch(this.err_ch, GLib.PRIORITY_DEFAULT, 
196             GLib.IOCondition.ERR + GLib.IOCondition.IN + GLib.IOCondition.PRI + GLib.IOCondition.OUT, 
197             function()
198         {
199             _this.read(_this.err_ch);
200              
201         });
202         
203       
204         
205         // call input.. 
206         if (this.pid !== false) {
207             // child can exit before we get this far..
208             if (this.listeners.input) {
209                 try {
210                     this.write(this.listeners.input.call(this));
211                 } catch (e) {
212                     tidyup();
213                     throw e;
214                     
215                 }
216                 
217             }
218         }
219         // async - if running - return..
220         if (this.async && this.pid) {
221             return this;
222         }
223         
224         
225         // start mainloop if not async..
226         
227         if (this.pid !== false) {
228             if (this.debug) {
229                 print("starting main loop");
230             }
231             ctx = new GLib.MainLoop.c_new (null, false);
232             ctx.run(false); // wait fore exit?
233             
234         } else {
235             tidyup(); // tidyup get's called in main loop. 
236         }
237         
238         if (this.exceptions && this.result != 0) {
239             this.toString = function() { return this.stderr; };
240             throw this; // we throw self...
241         }
242         
243         // finally throw, or return self..
244         
245         return this;
246     
247     },
248     /**
249      * write to stdin of process
250      * @arg str {String} string to write to stdin of process
251      * @returns GLib.IOStatus (0 == error, 1= NORMAL)
252      */
253     write : function(str) // write a line to 
254     {
255         if (!this.in_ch) {
256             return 0; // input is closed
257         }
258         var ret = {};
259         var res = this.in_ch.write_chars(str, str.length);
260         if (res != GLib.IOStatus.NORMAL) {
261             throw "Write failed";
262         }
263         return ret.bytes_written;
264         
265     },
266     
267     /**
268      * read from pipe and call appropriate listerner and add to output or stderr string.
269      * @arg giochannel to read from.
270      * @returns none
271      */
272     read: function(ch) 
273     {
274         var prop = ch == this.out_ch ? 'output' : 'stderr';
275         var _this = this;
276         while (true) {
277             var x = new GLib.String();
278             var status = ch.read_line_string( x);
279             switch(status) {
280                 case GLib.IOStatus.NORMAL:
281                     //write(fn, x.str);
282                     if (this.listeners[prop]) {
283                         this.listeners[prop].call(this, x.str);
284                     }
285                     _this[prop] += x.str;
286                     if (_this.debug) {
287                         print(prop + ':' + x.str);
288                     }
289                    continue;
290                 case GLib.IOStatus.AGAIN:   
291                     break;
292                 case GLib.IOStatus.ERROR:    
293                 case GLib.IOStatus.EOF:   
294                    break;
295                 
296             }
297             break;
298         }
299     }
300     
301 };
302 /**
303  * @function run 
304  * 
305  * simple run a process - returns result, or throws stderr result...
306  * @param cfg {Object}  see spawn
307  * @return {string} stdout output.
308  */
309 function run(cfg) {
310     cfg.exceptions = true;
311     cfg.async = false;
312     var s = new Spawn(cfg);
313     var ret = s.run();
314     return s.output;
315 }
316
317 // test
318
319
320 //Seed.print(run({
321 //    args: ['ls', '/tmp'],
322 //    //debug : true
323 //}));
324
325