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         GLib.spawn_async_with_pipes(this.cwd, this.args, null, 
120             GLib.SpawnFlags.DO_NOT_REAP_CHILD + GLib.SpawnFlags.SEARCH_PATH , 
121             null, null, ret);
122             
123         this.pid = ret.child_pid;
124         
125         
126         function tidyup()
127         {
128             if (_this.pid) {
129                 GLib.spawn_close_pid(_this.pid); // hopefully kills it..
130                 _this.pid = false;
131             }
132             if (_this.in_ch)  GLib.io_channel_close(_this.in_ch);
133             if (_this.out_ch)  GLib.io_channel_close(_this.out_ch);
134             if (_this.err_ch)  GLib.io_channel_close(_this.err_ch);
135             // blank out channels
136             _this.in_ch = false;
137             _this.err_ch = false;
138             _this.out_ch = false;
139             // rmeove listeners !! important otherwise we kill the CPU
140             if (err_src !== false) GLib.source_remove(err_src);
141             if (out_src !== false) GLib.source_remove(out_src);
142             err_src = false;
143             out_src = false;
144             
145         }
146         
147         
148         
149         GLib.child_watch_add(GLib.PRIORITY_DEFAULT, this.pid, function(pid, result) {
150             _this.result = result;
151             
152             GLib.spawn_close_pid(_this.pid);
153             _this.pid = false;
154             if (ctx) {
155                 GLib.main_loop_quit(ctx);
156             }
157             tidyup();
158             if (_this.listeners.finish) {
159                 _this.listeners.finish.call(this, _this.result);
160             }
161         });
162         
163         
164         this.in_ch = GLib.io_channel_unix_new(ret.standard_input);
165         this.out_ch = GLib.io_channel_unix_new(ret.standard_output);
166         this.err_ch = GLib.io_channel_unix_new(ret.standard_error);
167        
168         // make everything non-blocking!
169         GLib.io_channel_set_flags (this.in_ch,GLib.IOFlags.NONBLOCK);
170         GLib.io_channel_set_flags (this.out_ch,GLib.IOFlags.NONBLOCK);
171         GLib.io_channel_set_flags (this.err_ch,GLib.IOFlags.NONBLOCK);
172
173       
174         // add handlers for output and stderr.
175         out_src= GLib.io_add_watch(this.out_ch, GLib.PRIORITY_DEFAULT, 
176             GLib.IOCondition.OUT + GLib.IOCondition.IN  + GLib.IOCondition.PRI, function()
177         {
178             _this.read(this.out_ch);
179             
180         });
181         err_src= GLib.io_add_watch(this.err_ch, GLib.PRIORITY_DEFAULT, 
182             GLib.IOCondition.ERR + GLib.IOCondition.IN + GLib.IOCondition.PRI + GLib.IOCondition.OUT, 
183             function()
184         {
185             _this.read(this.err_ch);
186              
187         });
188         
189       
190         
191         // call input.. 
192         if (this.pid !== false) {
193             // child can exit before we get this far..
194             if (this.listeners.input) {
195                 try {
196                     this.write(this.listeners.input.call(this));
197                 } catch (e) {
198                     tidyup();
199                     throw e;
200                     
201                 }
202                 
203             }
204         }
205         // start mainloop if not async..
206         if (!this.async) {
207             if (this.pid !== false) {
208                 ctx = GLib.main_loop_new (null, false);
209                 GLib.main_loop_run(ctx, false); // wait fore exit?
210             } else {
211                 tidyup(); // tidyup get's called in main loop. 
212             }
213             
214             if (this.exceptions && this.result != 0) {
215                 throw this; // we throw self...
216             }
217         }
218          
219         
220         // finally throw, or return self..
221         
222         return this;
223     
224     },
225     /**
226      * write to stdin of process
227      * @arg str {String} string to write to stdin of process
228      * @returns GLib.IOStatus (0 == error, 1= NORMAL)
229      */
230     write : function(str) // write a line to 
231     {
232         if (!this.in_ch) {
233             return; // input is closed
234         }
235         var ret = {};
236         var res = GLib.io_channel_write_chars(this.in_ch, str, str.length);
237         if (res != GLib.IOStatus.NORMAL) {
238             throw "Write failed";
239         }
240         return ret.bytes_written;
241         
242     },
243     /**
244      * read from pipe and call appropriate listerner and add to output or stderr string.
245      * @arg giochannel to read from.
246      * @returns none
247      */
248     read: function(ch) 
249     {
250         var prop = ch == this.out_ch ? 'output' : 'stderr';
251         while (true) {
252             var x = new GLib.String();
253             var status = GLib.io_channel_read_line_string (ch, x);
254             switch(status) {
255                 case GLib.IOStatus.NORMAL:
256                     //write(fn, x.str);
257                     if (this.listeners[prop]) {
258                         this.listeners[prop].call(this, x.str);
259                     }
260                     _this[prop] += x.str;
261                     if (_this.debug) {
262                         print(prop + ':' + x.str);
263                     }
264                    continue;
265                 case GLib.IOStatus.AGAIN:   
266                     break;
267                 case GLib.IOStatus.ERROR:    
268                 case GLib.IOStatus.EOF:   
269                    break;
270                 
271             }
272             break;
273         }
274     }
275     
276 };
277 /**
278  * @function run 
279  * 
280  * simple run a process - returns result, or throws stderr result...
281  * @param cfg {Object}  see spawn
282  */
283 function run(cfg) {
284     cfg.exceptions = true;
285     cfg.async = false;
286     var s = new Spawn(cfg);
287     var ret = s.run();
288     return ret.output;
289 }
290
291 // test
292
293
294 Seed.print(run({
295     args: ['ls', '/tmp']
296 }));
297
298