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