Fix variable scope in StatusIcon.activate
[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 *  CRITICAL - needs this change to gir in GLib-2.0.gir g_spawn_async_with_pipes
31 *
32     <parameter name="argv" transfer-ownership="none">
33          <array c:type="gchar**">
34             <type name="utf8"/>
35           </array>
36         </parameter>
37         <parameter name="envp" transfer-ownership="none" allow-none="1">
38           <array c:type="gchar**">
39             <type name="utf8"/>
40           </array>
41         </parameter>
42 *
43 *
44 *<method name="read_line"
45               c:identifier="g_io_channel_read_line"
46               throws="1">
47         <return-value transfer-ownership="none">
48           <type name="IOStatus" c:type="GIOStatus"/>
49         </return-value>
50         <parameters>
51           <parameter name="str_return" transfer-ownership="full" direction="out">
52             <type name="utf8" c:type="gchar**"/>
53           </parameter>
54           <parameter name="length" transfer-ownership="none" direction="out">
55             <type name="gsize" c:type="gsize*"/>
56           </parameter>
57           <parameter name="terminator_pos" transfer-ownership="none"  direction="out">
58             <type name="gsize" c:type="gsize*"/>
59           </parameter>
60         </parameters>
61       </method>
62 *
63 *
64 *
65
66 */
67
68
69 /**
70  * @class Spawn
71  * @param cfg {Object} settings - see properties.
72  * 
73  * @arg cwd {String}            working directory. (defaults to home directory)
74  * @arg args {Array}            arguments eg. [ 'ls', '-l' ]
75  * @arg listeners {Object} (optional) handlers for output, stderr, input
76  *     stderr/output both receive output line as argument
77  *     input should return any standard input
78  *     finish recieves result as argument.
79  * @arg env {Array}             enviroment eg. [ 'GITDIR=/home/test' ]
80  * @arg async {Boolean} (optional)return instantly, or wait for exit. (default no)
81  * @arg exceptions {Boolean}    throw exception on failure (default no)
82  * @arg debug {Boolean}    print out what's going on.. (default no)
83  * 
84  */
85 function Spawn(cfg) {
86     for(var i in cfg) {
87         this[i] = cfg[i];
88     }
89     // set defaults?
90     this.listeners = this.listeners || {};
91     this.cwd =  this.cwd || GLib.get_home_dir();
92     if (!this.args || !this.args.length) {
93         throw "No arguments";
94     }
95     
96 }
97
98
99 Spawn.prototype = {
100     
101     listeners : false,
102     async : false,
103     env : null,
104     cwd: false,
105     args: false,
106     exceptions : false,
107     debug : true,
108     /**
109      * @property output {String} resulting output
110      */
111     output  : '',
112     /**
113      * @property stderr {String} resulting output from stderr
114      */
115     stderr  : '',
116      /**
117      * @property result {Number} execution result.
118      */
119     result: 0,
120     /**
121      * @property pid {Number} pid of child process (of false if it's not running)
122      */
123     pid : false,
124     
125    
126     
127     /**
128      * @property in_ch {GLib.IOChannel} input io channel
129      */
130     in_ch : false,
131     /**
132      * @property out_ch {GLib.IOChannel} output io channel
133      */
134     out_ch : false,
135     /**
136      * @property err_ch {GLib.IOChannel} stderr io channel
137      */
138     err_ch : false,
139     
140     /**
141      * 
142      * @method run
143      * Run the configured command.
144      * 
145      */
146     
147     
148     run : function()
149     {
150         
151         var _this = this;
152         
153         var err_src = false;
154         var out_src = false;
155         var ctx = false; 
156         var ret = {};
157         
158         if (this.debug) {
159             print("cd " + this.cwd +";" + this.args.join(" "));
160         }
161         
162         GLib.spawn_async_with_pipes(this.cwd, this.args, this.env, 
163             GLib.SpawnFlags.DO_NOT_REAP_CHILD + GLib.SpawnFlags.SEARCH_PATH , 
164             null, null, ret);
165             
166         print(JSON.stringify(ret));    
167         this.pid = ret.child_pid;
168         
169         if (this.debug) {
170             print("PID: " + this.pid);
171         }
172         
173         function tidyup()
174         {
175             if (_this.pid) {
176                 GLib.spawn_close_pid(_this.pid); // hopefully kills it..
177                 _this.pid = false;
178             }
179             if (_this.in_ch)  _this.in_ch.close();
180             if (_this.out_ch)  _this.out_ch.close();
181             if (_this.err_ch)  _this.err_ch.close();
182             // blank out channels
183             _this.in_ch = false;
184             _this.err_ch = false;
185             _this.out_ch = false;
186             // rmeove listeners !! important otherwise we kill the CPU
187             if (err_src !== false) GLib.source_remove(err_src);
188             if (out_src !== false) GLib.source_remove(out_src);
189             err_src = false;
190             out_src = false;
191             
192         }
193         
194         
195         this.in_ch = GLib.io_channel_unix_new(ret.standard_input);
196         this.out_ch = GLib.io_channel_unix_new(ret.standard_output);
197         this.err_ch = GLib.io_channel_unix_new(ret.standard_error);
198         
199         // make everything non-blocking!
200         this.in_ch.set_flags (GLib.IOFlags.NONBLOCK);
201         this.out_ch.set_flags (GLib.IOFlags.NONBLOCK);
202         this.err_ch.set_flags (GLib.IOFlags.NONBLOCK);
203         
204         
205         GLib.child_watch_add(GLib.PRIORITY_DEFAULT, this.pid, function(pid, result) {
206             _this.result = result;
207             if (_this.debug) {
208                 print("child_watch_add : result: " + result);
209             }
210             _this.read(_this.out_ch);
211             _this.read(_this.err_ch);
212             
213             GLib.spawn_close_pid(_this.pid);
214             _this.pid = false;
215             if (ctx) {
216                 ctx.quit();
217             }
218             tidyup();
219             if (_this.listeners.finish) {
220                 _this.listeners.finish.call(this, _this.result);
221             }
222         });
223         
224         
225        
226
227       
228         // add handlers for output and stderr.
229         out_src= GLib.io_add_watch(this.out_ch, GLib.PRIORITY_DEFAULT, 
230             GLib.IOCondition.OUT + GLib.IOCondition.IN  + GLib.IOCondition.PRI, function()
231         {
232             _this.read(_this.out_ch);
233             
234         });
235         err_src= GLib.io_add_watch(this.err_ch, GLib.PRIORITY_DEFAULT, 
236             GLib.IOCondition.ERR + GLib.IOCondition.IN + GLib.IOCondition.PRI + GLib.IOCondition.OUT, 
237             function()
238         {
239             _this.read(_this.err_ch);
240              
241         });
242         
243       
244         
245         // call input.. 
246         if (this.pid !== false) {
247             // child can exit before we get this far..
248             if (this.listeners.input) {
249                 try {
250                     this.write(this.listeners.input.call(this));
251                 } catch (e) {
252                     tidyup();
253                     throw e;
254                     
255                 }
256                 
257             }
258         }
259         // async - if running - return..
260         if (this.async && this.pid) {
261             return this;
262         }
263         
264         
265         // start mainloop if not async..
266         
267         if (this.pid !== false) {
268             if (this.debug) {
269                 print("starting main loop");
270             }
271             
272             ctx = new GLib.MainLoop.c_new (null, false);
273             ctx.run(false); // wait fore exit?
274             
275         } else {
276             tidyup(); // tidyup get's called in main loop. 
277         }
278         
279         if (this.exceptions && this.result != 0) {
280             this.toString = function() { return this.stderr; };
281             throw this; // we throw self...
282         }
283         
284         // finally throw, or return self..
285         
286         return this;
287     
288     },
289     /**
290      * write to stdin of process
291      * @arg str {String} string to write to stdin of process
292      * @returns GLib.IOStatus (0 == error, 1= NORMAL)
293      */
294     write : function(str) // write a line to 
295     {
296         if (!this.in_ch) {
297             return 0; // input is closed
298         }
299         var ret = {};
300         var res = this.in_ch.write_chars(str, str.length);
301         if (res != GLib.IOStatus.NORMAL) {
302             throw "Write failed";
303         }
304         return ret.bytes_written;
305         
306     },
307     
308     /**
309      * read from pipe and call appropriate listerner and add to output or stderr string.
310      * @arg giochannel to read from.
311      * @returns none
312      */
313     read: function(ch) 
314     {
315         var prop = ch == this.out_ch ? 'output' : 'stderr';
316         print("prop: " + prop);
317         var _this = this;
318         
319         //print(JSON.stringify(ch, null,4));
320         while (true) {
321             var x = {};
322             var status = ch.read_line(x);
323             //print("READ LINE STRING STATUS: " + status);
324              //print("got: " + JSON.stringify(x, null,4));
325
326             switch(status) {
327                 case GLib.IOStatus.NORMAL:
328                 
329                     //write(fn, x.str);
330                     if (this.listeners[prop]) {
331                         this.listeners[prop].call(this, x.str_return);
332                     }
333                     _this[prop] += x.str_return;
334                     if (_this.debug) {
335                         print(prop + ':' + x.str_return);
336                     }
337                    continue;
338                 case GLib.IOStatus.AGAIN:   
339                     break;
340                 case GLib.IOStatus.ERROR:    
341                 case GLib.IOStatus.EOF:   
342                    break;
343                 
344             }
345             break;
346         }
347     }
348     
349 };
350 /**
351  * @function run 
352  * 
353  * simple run a process - returns result, or throws stderr result...
354  * @param cfg {Object}  see spawn
355  * @return {string} stdout output.
356  */
357 function run(cfg) {
358     cfg.exceptions = true;
359     cfg.async = false;
360     var s = new Spawn(cfg);
361     var ret = s.run();
362     return s.output;
363 }
364  /*
365 // test
366 try { 
367     Seed.print(run({
368         args: ['ls', '/tmp'],
369         debug : true
370     }));
371 } catch (e) { print(JSON.stringify(e)); }
372  
373 var secs = (new Date()).getSeconds() 
374
375 try {      
376 Seed.print(run({
377     args: ['/bin/touch', '/tmp/spawntest-' + secs ],
378     debug : true
379 }));
380 } catch (e) { print( 'Error: ' + JSON.stringify(e)); }
381
382  */