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