merged cont.
[opensuse:yast-rest-service.git] / webyast / lib / session.rb
1 # Source: http://raa.ruby-lang.org/project/session
2 # Author "Ara T. Howard"
3 # Email  "ara.t.howard@noaa.gov"
4 # Homepage http://raa.ruby-lang.org/project/session
5 # licence http://www.ruby-lang.org/en/LICENSE.txt
6
7 # used by app/models/account.rb
8
9 require 'open3'
10 require 'tmpdir'
11 require 'thread'
12 require 'yaml'
13 require 'tempfile'
14
15 module Session 
16 #--{{{
17   VERSION = '2.4.0'
18
19   @track_history = ENV['SESSION_HISTORY'] || ENV['SESSION_TRACK_HISTORY']
20   @use_spawn     = ENV['SESSION_USE_SPAWN']
21   @use_open3     = ENV['SESSION_USE_OPEN3']
22   @debug         = ENV['SESSION_DEBUG']
23
24   class << self
25 #--{{{
26     attr :track_history, true
27     attr :use_spawn, true
28     attr :use_open3, true
29     attr :debug, true
30     def new(*a, &b)
31 #--{{{
32       Sh::new(*a, &b)
33 #--}}}
34     end
35     alias [] new
36 #--}}}
37   end
38
39   class PipeError < StandardError; end
40   class ExecutionError < StandardError; end
41
42   class History
43 #--{{{
44     def initialize; @a = []; end
45     def method_missing(m,*a,&b); @a.send(m,*a,&b); end
46     def to_yaml(*a,&b); @a.to_yaml(*a,&b); end
47     alias to_s to_yaml 
48     alias to_str to_yaml 
49 #--}}}
50   end # class History
51   class Command
52 #--{{{
53     class << self
54 #--{{{
55       def cmdno; @cmdno ||= 0; end
56       def cmdno= n; @cmdno = n; end
57 #--}}}
58     end
59
60     # attributes
61 #--{{{
62     attr :cmd
63     attr :cmdno
64     attr :out,true
65     attr :err,true
66     attr :cid
67     attr :begin_out
68     attr :end_out
69     attr :begin_out_pat
70     attr :end_out_pat
71     attr :begin_err
72     attr :end_err
73     attr :begin_err_pat
74     attr :end_err_pat
75 #--}}}
76
77     def initialize(command)
78 #--{{{
79       @cmd = command.to_s
80       @cmdno = self.class.cmdno
81       self.class.cmdno += 1
82       @err = ''
83       @out = ''
84       @cid = "%d_%d_%d" % [$$, cmdno, rand(Time.now.usec)]
85       @begin_out = "__CMD_OUT_%s_BEGIN__" % cid
86       @end_out = "__CMD_OUT_%s_END__" % cid
87       @begin_out_pat = %r/#{ Regexp.escape(@begin_out) }/
88       @end_out_pat = %r/#{ Regexp.escape(@end_out) }/
89       @begin_err = "__CMD_ERR_%s_BEGIN__" % cid
90       @end_err = "__CMD_ERR_%s_END__" % cid
91       @begin_err_pat = %r/#{ Regexp.escape(@begin_err) }/
92       @end_err_pat = %r/#{ Regexp.escape(@end_err) }/
93 #--}}}
94     end
95     def to_hash
96 #--{{{
97       %w(cmdno cmd out err cid).inject({}){|h,k| h.update k => send(k) }
98 #--}}}
99     end
100     def to_yaml(*a,&b)
101 #--{{{
102       to_hash.to_yaml(*a,&b)
103 #--}}}
104     end
105     alias to_s to_yaml 
106     alias to_str to_yaml 
107 #--}}}
108   end # class Command
109   class AbstractSession 
110 #--{{{
111
112   # class methods
113     class << self
114 #--{{{
115       def default_prog
116 #--{{{
117         return @default_prog if defined? @default_prog and @default_prog
118         if defined? self::DEFAULT_PROG
119           return @default_prog = self::DEFAULT_PROG 
120         else
121           @default_prog = ENV["SESSION_#{ self }_PROG"]
122         end
123         nil
124 #--}}}
125       end
126       def default_prog= prog
127 #--{{{
128         @default_prog = prog 
129 #--}}}
130       end
131       attr :track_history, true
132       attr :use_spawn, true
133       attr :use_open3, true
134       attr :debug, true
135       def init
136 #--{{{
137         @track_history = nil
138         @use_spawn = nil
139         @use_open3 = nil
140         @debug = nil
141 #--}}}
142       end
143       alias [] new
144 #--}}}
145     end
146
147   # class init
148     init
149
150   # attributes
151 #--{{{
152     attr :opts
153     attr :prog
154     attr :stdin
155     alias i stdin
156     attr :stdout
157     alias o stdout
158     attr :stderr
159     alias e stderr
160     attr :history
161     attr :track_history
162     attr :outproc, true
163     attr :errproc, true
164     attr :use_spawn
165     attr :use_open3
166     attr :debug, true
167     alias debug? debug
168     attr :threads
169 #--}}}
170
171   # instance methods
172     def initialize(*args)
173 #--{{{
174       @opts = hashify(*args)
175
176       @prog = getopt('prog', opts, getopt('program', opts, self.class::default_prog))
177
178       raise(ArgumentError, "no program specified") unless @prog
179
180       @track_history = nil
181       @track_history = Session::track_history unless Session::track_history.nil?
182       @track_history = self.class::track_history unless self.class::track_history.nil?
183       @track_history = getopt('history', opts) if hasopt('history', opts) 
184       @track_history = getopt('track_history', opts) if hasopt('track_history', opts) 
185
186       @use_spawn = nil
187       @use_spawn = Session::use_spawn unless Session::use_spawn.nil?
188       @use_spawn = self.class::use_spawn unless self.class::use_spawn.nil?
189       @use_spawn = getopt('use_spawn', opts) if hasopt('use_spawn', opts)
190
191       @use_open3 = nil
192       @use_open3 = Session::use_open3 unless Session::use_open3.nil?
193       @use_open3 = self.class::use_open3 unless self.class::use_open3.nil?
194       @use_open3 = getopt('use_open3', opts) if hasopt('use_open3', opts) 
195
196       @debug = nil
197       @debug = Session::debug unless Session::debug.nil?
198       @debug = self.class::debug unless self.class::debug.nil?
199       @debug = getopt('debug', opts) if hasopt('debug', opts) 
200
201       @history = nil
202       @history = History::new if @track_history 
203
204       @outproc = nil
205       @errproc = nil
206
207       @stdin, @stdout, @stderr =
208         if @use_spawn
209           Spawn::spawn @prog
210         elsif @use_open3
211           Open3::popen3 @prog
212         else
213           __popen3 @prog
214         end
215
216       @threads = []
217
218       clear
219
220       if block_given?
221         ret = nil
222         begin
223           ret = yield self
224         ensure
225           self.close!
226         end
227         return ret
228       end
229
230       return self
231 #--}}}
232     end
233     def getopt opt, hash, default = nil
234 #--{{{
235       key = opt
236       return hash[key] if hash.has_key? key
237       key = "#{ key }"
238       return hash[key] if hash.has_key? key
239       key = key.intern
240       return hash[key] if hash.has_key? key
241       return default
242 #--}}}
243     end
244     def hasopt opt, hash
245 #--{{{
246       key = opt
247       return key if hash.has_key? key
248       key = "#{ key }"
249       return key if hash.has_key? key
250       key = key.intern
251       return key if hash.has_key? key
252       return false 
253 #--}}}
254     end
255     def __popen3(*cmd)
256 #--{{{
257       pw = IO::pipe   # pipe[0] for read, pipe[1] for write
258       pr = IO::pipe
259       pe = IO::pipe
260
261       pid =
262         __fork{
263           # child
264           pw[1].close
265           STDIN.reopen(pw[0])
266           pw[0].close
267
268           pr[0].close
269           STDOUT.reopen(pr[1])
270           pr[1].close
271
272           pe[0].close
273           STDERR.reopen(pe[1])
274           pe[1].close
275
276           exec(*cmd)
277         }
278
279       Process::detach pid   # avoid zombies
280
281       pw[0].close
282       pr[1].close
283       pe[1].close
284       pi = [pw[1], pr[0], pe[0]]
285       pw[1].sync = true
286       if defined? yield
287         begin
288           return yield(*pi)
289         ensure
290           pi.each{|p| p.close unless p.closed?}
291         end
292       end
293       pi
294 #--}}}
295     end
296     def __fork(*a, &b)
297 #--{{{
298       verbose = $VERBOSE
299       begin
300         $VERBOSE = nil 
301         Kernel::fork(*a, &b)
302       ensure
303         $VERBOSE = verbose
304       end
305 #--}}}
306     end
307
308   # abstract methods
309     def clear
310 #--{{{
311       raise NotImplementedError
312 #--}}}
313     end
314     alias flush clear
315     def path 
316 #--{{{
317       raise NotImplementedError
318 #--}}}
319     end
320     def path= 
321 #--{{{
322       raise NotImplementedError
323 #--}}}
324     end
325     def send_command cmd
326 #--{{{
327       raise NotImplementedError
328 #--}}}
329     end
330
331   # concrete methods
332     def track_history= bool
333 #--{{{
334       @history ||= History::new
335       @track_history = bool
336 #--}}}
337     end
338     def ready?
339 #--{{{
340       (stdin and stdout and stderr) and
341       (IO === stdin and IO === stdout and IO === stderr) and
342       (not (stdin.closed? or stdout.closed? or stderr.closed?))
343 #--}}}
344     end
345     def close!
346 #--{{{
347       [stdin, stdout, stderr].each{|pipe| pipe.close}
348       stdin, stdout, stderr = nil, nil, nil
349       true
350 #--}}}
351     end
352     alias close close!
353     def hashify(*a)
354 #--{{{
355       a.inject({}){|o,h| o.update(h)}
356 #--}}}
357     end
358     private :hashify
359     def execute(command, redirects = {})
360 #--{{{
361       $session_command = command if @debug
362
363       raise(PipeError, command) unless ready? 
364
365     # clear buffers
366       clear
367
368     # setup redirects
369       rerr = redirects[:e] || redirects[:err] || redirects[:stderr] || 
370              redirects['stderr'] || redirects['e'] || redirects['err'] ||
371              redirects[2] || redirects['2']
372
373       rout = redirects[:o] || redirects[:out] || redirects[:stdout] || 
374              redirects['stdout'] || redirects['o'] || redirects['out'] ||
375              redirects[1] || redirects['1']
376
377     # create cmd object and add to history
378       cmd = Command::new command.to_s
379
380     # store cmd if tracking history
381       history << cmd if track_history
382
383     # mutex for accessing shared data
384       mutex = Mutex::new
385
386     # io data for stderr and stdout 
387       err = {
388         :io        => stderr,
389         :cmd       => cmd.err,
390         :name      => 'stderr',
391         :begin     => false,
392         :end       => false,
393         :begin_pat => cmd.begin_err_pat,
394         :end_pat   => cmd.end_err_pat,
395         :redirect  => rerr,
396         :proc      => errproc,
397         :yield     => lambda{|buf| yield(nil, buf)},
398         :mutex     => mutex,
399       }
400       out = {
401         :io        => stdout,
402         :cmd       => cmd.out,
403         :name      => 'stdout',
404         :begin     => false,
405         :end       => false,
406         :begin_pat => cmd.begin_out_pat,
407         :end_pat   => cmd.end_out_pat,
408         :redirect  => rout,
409         :proc      => outproc,
410         :yield     => lambda{|buf| yield(buf, nil)},
411         :mutex     => mutex,
412       }
413
414     begin
415       # send command in the background so we can begin processing output
416       # immediately - thanks to tanaka akira for this suggestion
417         threads << Thread::new { send_command cmd }
418
419       # init 
420         main       = Thread::current
421         exceptions = []
422
423       # fire off reader threads
424         [err, out].each do |iodat|
425           threads <<
426             Thread::new(iodat, main) do |iodat, main|
427
428               loop do
429                 main.raise(PipeError, command) unless ready? 
430                 main.raise ExecutionError, iodat[:name] if iodat[:end] and not iodat[:begin]
431
432                 break if iodat[:end] or iodat[:io].eof?
433
434                 line = iodat[:io].gets
435
436                 buf = nil
437
438                 case line
439                   when iodat[:end_pat]
440                     iodat[:end] = true
441                   # handle the special case of non-newline terminated output
442                     if((m = %r/(.+)__CMD/o.match(line)) and (pre = m[1]))
443                       buf = pre
444                     end
445                   when iodat[:begin_pat]
446                     iodat[:begin] = true
447                   else
448                     next unless iodat[:begin] and not iodat[:end] # ignore chaff
449                     buf = line
450                 end
451
452                 if buf
453                   iodat[:mutex].synchronize do
454                     iodat[:cmd] << buf
455                     iodat[:redirect] << buf if iodat[:redirect]
456                     iodat[:proc].call buf  if iodat[:proc]
457                     iodat[:yield].call buf  if block_given?
458                   end
459                 end
460               end
461
462               true
463           end
464         end
465       ensure
466       # reap all threads - accumulating and rethrowing any exceptions
467         begin
468           while((t = threads.shift))
469             t.join
470             raise ExecutionError, 'iodat thread failure' unless t.value
471           end
472         rescue => e
473           exceptions << e
474           retry unless threads.empty?
475         ensure
476           unless exceptions.empty?
477             meta_message = '<' << exceptions.map{|e| "#{ e.message } - (#{ e.class })"}.join('|') << '>'
478             meta_backtrace = exceptions.map{|e| e.backtrace}.flatten
479             raise ExecutionError, meta_message, meta_backtrace 
480           end
481         end
482       end
483
484     # this should only happen if eof was reached before end pat
485       [err, out].each do |iodat|
486         raise ExecutionError, iodat[:name] unless iodat[:begin] and iodat[:end]
487       end
488
489
490     # get the exit status
491       get_status if respond_to? :get_status
492
493       out = err = iodat = nil
494
495       return [cmd.out, cmd.err]
496 #--}}}
497     end
498 #--}}}
499   end # class AbstractSession
500   class Sh < AbstractSession
501 #--{{{
502     DEFAULT_PROG    = 'sh'
503     ECHO            = 'echo'
504
505     attr :status
506     alias exit_status status
507     alias exitstatus status
508
509     def clear
510 #--{{{
511       stdin.puts "#{ ECHO } __clear__ 1>&2"
512       stdin.puts "#{ ECHO } __clear__"
513       stdin.flush
514       while((line = stderr.gets) and line !~ %r/__clear__/o); end
515       while((line = stdout.gets) and line !~ %r/__clear__/o); end
516       self
517 #--}}}
518     end
519     def send_command cmd
520 #--{{{
521       stdin.printf "%s '%s' 1>&2\n", ECHO, cmd.begin_err
522       stdin.printf "%s '%s' \n", ECHO, cmd.begin_out
523  
524       stdin.printf "%s\n", cmd.cmd
525       stdin.printf "export __exit_status__=$?\n"
526
527       stdin.printf "%s '%s' 1>&2\n", ECHO, cmd.end_err
528       stdin.printf "%s '%s' \n", ECHO, cmd.end_out
529  
530       stdin.flush
531 #--}}}
532     end
533     def get_status
534 #--{{{
535       @status = get_var '__exit_status__' 
536       unless @status =~ /^\s*\d+\s*$/o
537         raise ExecutionError, "could not determine exit status from <#{ @status.inspect }>"
538       end
539
540       @status = Integer @status
541 #--}}}
542     end
543     def set_var name, value
544 #--{{{
545       stdin.puts "export #{ name }=#{ value }"
546       stdin.flush
547 #--}}}
548     end
549     def get_var name
550 #--{{{
551       stdin.puts "#{ ECHO } \"#{ name }=${#{ name }}\""
552       stdin.flush
553
554       var = nil
555       while((line = stdout.gets))
556         m = %r/#{ name }\s*=\s*(.*)/.match line
557         if m
558           var = m[1] 
559           raise ExecutionError, "could not determine <#{ name }> from <#{ line.inspect }>" unless var
560           break
561         end
562       end
563
564       var
565 #--}}}
566     end
567     def path 
568 #--{{{
569       var = get_var 'PATH'
570       var.strip.split %r/:/o
571 #--}}}
572     end
573     def path= arg 
574 #--{{{
575       case arg
576         when Array
577           arg = arg.join ':'
578         else
579           arg = arg.to_s.strip
580       end
581
582       set_var 'PATH', "'#{ arg }'"
583       self.path
584 #--}}}
585     end
586     def execute(command, redirects = {}, &block)
587 #--{{{
588     # setup redirect on stdin
589       rin = redirects[:i] || redirects[:in] || redirects[:stdin] || 
590              redirects['stdin'] || redirects['i'] || redirects['in'] ||
591              redirects[0] || redirects['0']
592
593       if rin
594         tmp = 
595           begin
596             Tempfile::new rand.to_s
597           rescue
598             Tempfile::new rand.to_s
599           end
600
601         begin
602           tmp.write(
603             if rin.respond_to? 'read'
604               rin.read
605             elsif rin.respond_to? 'to_s'
606               rin.to_s
607             else
608               rin
609             end
610           )
611           tmp.flush
612           command = "{ #{ command } ;} < #{ tmp.path }"
613           #puts command
614           super(command, redirects, &block)
615         ensure
616           tmp.close! if tmp 
617         end
618
619       else
620         super
621       end
622 #--}}}
623     end
624 #--}}}
625   end # class Sh
626   class Bash < Sh
627 #--{{{
628     DEFAULT_PROG = 'bash'
629     class Login < Bash
630       DEFAULT_PROG = 'bash --login'
631     end
632 #--}}}
633   end # class Bash
634   class Shell < Bash; end
635   # IDL => interactive data language - see http://www.rsinc.com/
636   class IDL < AbstractSession
637 #--{{{
638     class LicenseManagerError < StandardError; end
639     DEFAULT_PROG = 'idl'
640     MAX_TRIES = 32 
641     def initialize(*args)
642 #--{{{
643       tries = 0 
644       ret = nil
645       begin
646         ret = super
647       rescue LicenseManagerError => e
648         tries += 1 
649         if tries < MAX_TRIES
650           sleep 1
651           retry
652         else
653           raise LicenseManagerError, "<#{ MAX_TRIES }> attempts <#{ e.message }>"
654         end
655       end
656       ret
657 #--}}}
658     end
659     def clear
660 #--{{{
661       stdin.puts "retall"
662       stdin.puts "printf, -2, '__clear__'"
663       stdin.puts "printf, -1, '__clear__'"
664       stdin.flush
665       while((line = stderr.gets) and line !~ %r/__clear__/o)
666         raise LicenseManagerError, line if line =~ %r/license\s*manager/io
667       end
668       while((line = stdout.gets) and line !~ %r/__clear__/o)
669         raise LicenseManagerError, line if line =~ %r/license\s*manager/io
670       end
671       self
672 #--}}}
673     end
674     def send_command cmd
675 #--{{{
676       stdin.printf "printf, -2, '%s'\n", cmd.begin_err
677       stdin.printf "printf, -1, '%s'\n", cmd.begin_out
678
679       stdin.printf "%s\n", cmd.cmd
680       stdin.printf "retall\n"
681
682       stdin.printf "printf, -2, '%s'\n", cmd.end_err
683       stdin.printf "printf, -1, '%s'\n", cmd.end_out
684       stdin.flush
685 #--}}}
686     end
687     def path 
688 #--{{{
689       stdout, stderr = execute "print, !path"
690       stdout.strip.split %r/:/o
691 #--}}}
692     end
693     def path= arg 
694 #--{{{
695       case arg
696         when Array
697           arg = arg.join ':'
698         else
699           arg = arg.to_s.strip
700       end
701       stdout, stderr = execute "!path='#{ arg }'"
702
703       self.path
704 #--}}}
705     end
706 #--}}}
707   end # class IDL
708   module Spawn
709 #--{{{
710     class << self
711       def spawn command
712 #--{{{
713         ipath = tmpfifo
714         opath = tmpfifo
715         epath = tmpfifo
716
717         cmd = "#{ command } < #{ ipath } 1> #{ opath } 2> #{ epath } &"
718         system cmd 
719
720         i = open ipath, 'w'
721         o = open opath, 'r'
722         e = open epath, 'r'
723
724         [i,o,e]
725 #--}}}
726       end
727       def tmpfifo
728 #--{{{
729         path = nil
730         42.times do |i|
731           tpath = File::join(Dir::tmpdir, "#{ $$ }.#{ rand }.#{ i }")
732           v = $VERBOSE
733           begin
734             $VERBOSE = nil
735             system "mkfifo #{ tpath }"
736           ensure
737             $VERBOSE = v 
738           end
739           next unless $? == 0
740           path = tpath
741           at_exit{ File::unlink(path) rescue STDERR.puts("rm <#{ path }> failed") }
742           break
743         end
744         raise "could not generate tmpfifo" unless path
745         path
746 #--}}}
747       end
748     end
749 #--}}}
750   end # module Spawn
751 #--}}}
752 end # module Session