Spawn.js
[gitlive] / Spawn.js
1 ///<script type="text/javascript">
2
3 Gio      = imports.gi.Gio;
4 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 Spawn.prototype = {
61     
62     listeners : false,
63     async : false,
64     env : null,
65     cwd: false,
66     args: false,
67     exceptions : false,
68     debug : false,
69     /**
70      * @property output {String} resulting output
71      */
72     output  : '',
73     /**
74      * @property stderr {String} resulting output from stderr
75      */
76     stderr  : '',
77      /**
78      * @property result {Number} execution result.
79      */
80     result: 0,
81     /**
82      * @property pid {Number} pid of child process (of false if it's not running)
83      */
84     pid : false,
85     
86    
87     
88     /**
89      * @property in_ch {GLib.IOChannel} input io channel
90      */
91     in_ch : false,
92     /**
93      * @property out_ch {GLib.IOChannel} output io channel
94      */
95     out_ch : false,
96     /**
97      * @property err_ch {GLib.IOChannel} stderr io channel
98      */
99     err_ch : false,
100     
101     /**
102      * 
103      * @method run
104      * Run the configured command.
105      * 
106      */
107     
108     
109     run : function()
110     {
111         
112         var _this = this;
113         
114         var err_src = false;
115         var out_src = false;
116         var ctx = false; 
117         var ret = {};
118         
119         if (this.debug) {
120             print("spawn : " + this.args.join(" "));
121         }
122         
123         GLib.spawn_async_with_pipes(this.cwd, this.args, this.env, 
124             GLib.SpawnFlags.DO_NOT_REAP_CHILD + GLib.SpawnFlags.SEARCH_PATH , 
125             null, null, ret);
126             
127         this.pid = ret.child_pid;
128         
129         if (this.debug) {
130             print("PID: " + this.pid);
131         }
132         function tidyup()
133         {
134             if (_this.pid) {
135                 GLib.spawn_close_pid(_this.pid); // hopefully kills it..
136                 _this.pid = false;
137             }
138             if (_this.in_ch)  GLib.io_channel_close(_this.in_ch);
139             if (_this.out_ch)  GLib.io_channel_close(_this.out_ch);
140             if (_this.err_ch)  GLib.io_channel_close(_this.err_ch);
141             // blank out channels
142             _this.in_ch = false;
143             _this.err_ch = false;
144             _this.out_ch = false;
145             // rmeove listeners !! important otherwise we kill the CPU
146             if (err_src !== false) GLib.source_remove(err_src);
147             if (out_src !== false) GLib.source_remove(out_src);
148             err_src = false;
149             out_src = false;
150             
151         }
152         
153         
154         
155         GLib.child_watch_add(GLib.PRIORITY_DEFAULT, this.pid, function(pid, result) {
156             _this.result = result;
157             if (_this.debug) {
158                 print("result: " + result);
159             }
160             _this.read(_this.out_ch);
161             _this.read(_this.err_ch);
162             
163             GLib.spawn_close_pid(_this.pid);
164             _this.pid = false;
165             if (ctx) {
166                 GLib.main_loop_quit(ctx);
167             }
168             tidyup();
169             if (_this.listeners.finish) {
170                 _this.listeners.finish.call(this, _this.result);
171             }
172         });
173         
174         
175         this.in_ch = GLib.io_channel_unix_new(ret.standard_input);
176         this.out_ch = GLib.io_channel_unix_new(ret.standard_output);
177         this.err_ch = GLib.io_channel_unix_new(ret.standard_error);
178        
179         // make everything non-blocking!
180         GLib.io_channel_set_flags (this.in_ch,GLib.IOFlags.NONBLOCK);
181         GLib.io_channel_set_flags (this.out_ch,GLib.IOFlags.NONBLOCK);
182         GLib.io_channel_set_flags (this.err_ch,GLib.IOFlags.NONBLOCK);
183
184       
185         // add handlers for output and stderr.
186         out_src= GLib.io_add_watch(this.out_ch, GLib.PRIORITY_DEFAULT, 
187             GLib.IOCondition.OUT + GLib.IOCondition.IN  + GLib.IOCondition.PRI, function()
188         {
189             _this.read(_this.out_ch);
190             
191         });
192         err_src= GLib.io_add_watch(this.err_ch, GLib.PRIORITY_DEFAULT, 
193             GLib.IOCondition.ERR + GLib.IOCondition.IN + GLib.IOCondition.PRI + GLib.IOCondition.OUT, 
194             function()
195         {
196             _this.read(_this.err_ch);
197              
198         });
199         
200       
201         
202         // call input.. 
203         if (this.pid !== false) {
204             // child can exit before we get this far..
205             if (this.listeners.input) {
206                 try {
207                     this.write(this.listeners.input.call(this));
208                 } catch (e) {
209                     tidyup();
210                     throw e;
211                     
212                 }
213                 
214             }
215         }
216         // async - if running - return..
217         if (this.async && this.pid) {
218             return this;
219         }
220         
221         // start mainloop if not async..
222         
223         if (this.pid !== false) {
224             if (this.debug) {
225                 print("starting main loop");
226             }
227             ctx = GLib.main_loop_new (null, false);
228             GLib.main_loop_run(ctx, false); // wait fore exit?
229             
230         } else {
231             tidyup(); // tidyup get's called in main loop. 
232         }
233         
234         if (this.exceptions && this.result != 0) {
235             throw this; // we throw self...
236         }
237         
238         // finally throw, or return self..
239         
240         return this;
241     
242     },
243     /**
244      * write to stdin of process
245      * @arg str {String} string to write to stdin of process
246      * @returns GLib.IOStatus (0 == error, 1= NORMAL)
247      */
248     write : function(str) // write a line to 
249     {
250         if (!this.in_ch) {
251             return; // input is closed
252         }
253         var ret = {};
254         var res = GLib.io_channel_write_chars(this.in_ch, str, str.length);
255         if (res != GLib.IOStatus.NORMAL) {
256             throw "Write failed";
257         }
258         return ret.bytes_written;
259         
260     },
261     /**
262      * read from pipe and call appropriate listerner and add to output or stderr string.
263      * @arg giochannel to read from.
264      * @returns none
265      */
266     read: function(ch) 
267     {
268         var prop = ch == this.out_ch ? 'output' : 'stderr';
269         var _this = this;
270         while (true) {
271             var x = new GLib.String();
272             var status = GLib.io_channel_read_line_string (ch, x);
273             switch(status) {
274                 case GLib.IOStatus.NORMAL:
275                     //write(fn, x.str);
276                     if (this.listeners[prop]) {
277                         this.listeners[prop].call(this, x.str);
278                     }
279                     _this[prop] += x.str;
280                     if (_this.debug) {
281                         print(prop + ':' + x.str);
282                     }
283                    continue;
284                 case GLib.IOStatus.AGAIN:   
285                     break;
286                 case GLib.IOStatus.ERROR:    
287                 case GLib.IOStatus.EOF:   
288                    break;
289                 
290             }
291             break;
292         }
293     }
294     
295 };
296 /**
297  * @function run 
298  * 
299  * simple run a process - returns result, or throws stderr result...
300  * @param cfg {Object}  see spawn
301  */
302 function run(cfg) {
303     cfg.exceptions = true;
304     cfg.async = false;
305     var s = new Spawn(cfg);
306     var ret = s.run();
307     return ret.output;
308 }
309
310 // test
311
312
313 //Seed.print(run({
314 //    args: ['ls', '/tmp'],
315 //    //debug : true
316 //}));
317
318