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)  _this.in_ch.close();
139             if (_this.out_ch)  _this.out_ch.close();
140             if (_this.err_ch)  _this.err_ch.close();
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                 ctx.quit();
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         this.in_ch.set_flags (GLib.IOFlags.NONBLOCK);
181         this.out_ch.set_flags (GLib.IOFlags.NONBLOCK);
182         this.err_ch.set_flags (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         
222         // start mainloop if not async..
223         
224         if (this.pid !== false) {
225             if (this.debug) {
226                 print("starting main loop");
227             }
228             ctx = new GLib.MainLoop.c_new (null, false);
229             ctx.run(false); // wait fore exit?
230             
231         } else {
232             tidyup(); // tidyup get's called in main loop. 
233         }
234         
235         if (this.exceptions && this.result != 0) {
236             this.toString = function() { return this.stderr; };
237             throw this; // we throw self...
238         }
239         
240         // finally throw, or return self..
241         
242         return this;
243     
244     },
245     /**
246      * write to stdin of process
247      * @arg str {String} string to write to stdin of process
248      * @returns GLib.IOStatus (0 == error, 1= NORMAL)
249      */
250     write : function(str) // write a line to 
251     {
252         if (!this.in_ch) {
253             return; // input is closed
254         }
255         var ret = {};
256         var res = this.in_ch.write_chars(str, str.length);
257         if (res != GLib.IOStatus.NORMAL) {
258             throw "Write failed";
259         }
260         return ret.bytes_written;
261         
262     },
263     
264     /**
265      * read from pipe and call appropriate listerner and add to output or stderr string.
266      * @arg giochannel to read from.
267      * @returns none
268      */
269     read: function(ch) 
270     {
271         var prop = ch == this.out_ch ? 'output' : 'stderr';
272         var _this = this;
273         while (true) {
274             var x = new GLib.String();
275             var status = ch.read_line_string( x);
276             switch(status) {
277                 case GLib.IOStatus.NORMAL:
278                     //write(fn, x.str);
279                     if (this.listeners[prop]) {
280                         this.listeners[prop].call(this, x.str);
281                     }
282                     _this[prop] += x.str;
283                     if (_this.debug) {
284                         print(prop + ':' + x.str);
285                     }
286                    continue;
287                 case GLib.IOStatus.AGAIN:   
288                     break;
289                 case GLib.IOStatus.ERROR:    
290                 case GLib.IOStatus.EOF:   
291                    break;
292                 
293             }
294             break;
295         }
296     }
297     
298 };
299 /**
300  * @function run 
301  * 
302  * simple run a process - returns result, or throws stderr result...
303  * @param cfg {Object}  see spawn
304  * @return {string} stdout output.
305  */
306 function run(cfg) {
307     cfg.exceptions = true;
308     cfg.async = false;
309     var s = new Spawn(cfg);
310     var ret = s.run();
311     return s.output;
312 }
313
314 // test
315
316
317 //Seed.print(run({
318 //    args: ['ls', '/tmp'],
319 //    //debug : true
320 //}));
321
322