require 'open3' require 'tmpdir' require 'thread' require 'yaml' require 'tempfile' module Session #--{{{ VERSION = '2.4.0' @track_history = ENV['SESSION_HISTORY'] || ENV['SESSION_TRACK_HISTORY'] @use_spawn = ENV['SESSION_USE_SPAWN'] @use_open3 = ENV['SESSION_USE_OPEN3'] @debug = ENV['SESSION_DEBUG'] class << self #--{{{ attr :track_history, true attr :use_spawn, true attr :use_open3, true attr :debug, true def new(*a, &b) #--{{{ Sh::new(*a, &b) #--}}} end alias [] new #--}}} end class PipeError < StandardError; end class ExecutionError < StandardError; end class History #--{{{ def initialize; @a = []; end def method_missing(m,*a,&b); @a.send(m,*a,&b); end def to_yaml(*a,&b); @a.to_yaml(*a,&b); end alias to_s to_yaml alias to_str to_yaml #--}}} end # class History class Command #--{{{ class << self #--{{{ def cmdno; @cmdno ||= 0; end def cmdno= n; @cmdno = n; end #--}}} end # attributes #--{{{ attr :cmd attr :cmdno attr :out,true attr :err,true attr :cid attr :begin_out attr :end_out attr :begin_out_pat attr :end_out_pat attr :begin_err attr :end_err attr :begin_err_pat attr :end_err_pat #--}}} def initialize(command) #--{{{ @cmd = command.to_s @cmdno = self.class.cmdno self.class.cmdno += 1 @err = '' @out = '' @cid = "%d_%d_%d" % [$$, cmdno, rand(Time.now.usec)] @begin_out = "__CMD_OUT_%s_BEGIN__" % cid @end_out = "__CMD_OUT_%s_END__" % cid @begin_out_pat = %r/#{ Regexp.escape(@begin_out) }/ @end_out_pat = %r/#{ Regexp.escape(@end_out) }/ @begin_err = "__CMD_ERR_%s_BEGIN__" % cid @end_err = "__CMD_ERR_%s_END__" % cid @begin_err_pat = %r/#{ Regexp.escape(@begin_err) }/ @end_err_pat = %r/#{ Regexp.escape(@end_err) }/ #--}}} end def to_hash #--{{{ %w(cmdno cmd out err cid).inject({}){|h,k| h.update k => send(k) } #--}}} end def to_yaml(*a,&b) #--{{{ to_hash.to_yaml(*a,&b) #--}}} end alias to_s to_yaml alias to_str to_yaml #--}}} end # class Command class AbstractSession #--{{{ # class methods class << self #--{{{ def default_prog #--{{{ return @default_prog if defined? @default_prog and @default_prog if defined? self::DEFAULT_PROG return @default_prog = self::DEFAULT_PROG else @default_prog = ENV["SESSION_#{ self }_PROG"] end nil #--}}} end def default_prog= prog #--{{{ @default_prog = prog #--}}} end attr :track_history, true attr :use_spawn, true attr :use_open3, true attr :debug, true def init #--{{{ @track_history = nil @use_spawn = nil @use_open3 = nil @debug = nil #--}}} end alias [] new #--}}} end # class init init # attributes #--{{{ attr :opts attr :prog attr :stdin alias i stdin attr :stdout alias o stdout attr :stderr alias e stderr attr :history attr :track_history attr :outproc, true attr :errproc, true attr :use_spawn attr :use_open3 attr :debug, true alias debug? debug attr :threads #--}}} # instance methods def initialize(*args) #--{{{ @opts = hashify(*args) @prog = getopt('prog', opts, getopt('program', opts, self.class::default_prog)) raise(ArgumentError, "no program specified") unless @prog @track_history = nil @track_history = Session::track_history unless Session::track_history.nil? @track_history = self.class::track_history unless self.class::track_history.nil? @track_history = getopt('history', opts) if hasopt('history', opts) @track_history = getopt('track_history', opts) if hasopt('track_history', opts) @use_spawn = nil @use_spawn = Session::use_spawn unless Session::use_spawn.nil? @use_spawn = self.class::use_spawn unless self.class::use_spawn.nil? @use_spawn = getopt('use_spawn', opts) if hasopt('use_spawn', opts) @use_open3 = nil @use_open3 = Session::use_open3 unless Session::use_open3.nil? @use_open3 = self.class::use_open3 unless self.class::use_open3.nil? @use_open3 = getopt('use_open3', opts) if hasopt('use_open3', opts) @debug = nil @debug = Session::debug unless Session::debug.nil? @debug = self.class::debug unless self.class::debug.nil? @debug = getopt('debug', opts) if hasopt('debug', opts) @history = nil @history = History::new if @track_history @outproc = nil @errproc = nil @stdin, @stdout, @stderr = if @use_spawn Spawn::spawn @prog elsif @use_open3 Open3::popen3 @prog else __popen3 @prog end @threads = [] clear if block_given? ret = nil begin ret = yield self ensure self.close! end return ret end return self #--}}} end def getopt opt, hash, default = nil #--{{{ key = opt return hash[key] if hash.has_key? key key = "#{ key }" return hash[key] if hash.has_key? key key = key.intern return hash[key] if hash.has_key? key return default #--}}} end def hasopt opt, hash #--{{{ key = opt return key if hash.has_key? key key = "#{ key }" return key if hash.has_key? key key = key.intern return key if hash.has_key? key return false #--}}} end def __popen3(*cmd) #--{{{ pw = IO::pipe # pipe[0] for read, pipe[1] for write pr = IO::pipe pe = IO::pipe pid = __fork{ # child pw[1].close STDIN.reopen(pw[0]) pw[0].close pr[0].close STDOUT.reopen(pr[1]) pr[1].close pe[0].close STDERR.reopen(pe[1]) pe[1].close exec(*cmd) } Process::detach pid # avoid zombies pw[0].close pr[1].close pe[1].close pi = [pw[1], pr[0], pe[0]] pw[1].sync = true if defined? yield begin return yield(*pi) ensure pi.each{|p| p.close unless p.closed?} end end pi #--}}} end def __fork(*a, &b) #--{{{ verbose = $VERBOSE begin $VERBOSE = nil Kernel::fork(*a, &b) ensure $VERBOSE = verbose end #--}}} end # abstract methods def clear #--{{{ raise NotImplementedError #--}}} end alias flush clear def path #--{{{ raise NotImplementedError #--}}} end def path= #--{{{ raise NotImplementedError #--}}} end def send_command cmd #--{{{ raise NotImplementedError #--}}} end # concrete methods def track_history= bool #--{{{ @history ||= History::new @track_history = bool #--}}} end def ready? #--{{{ (stdin and stdout and stderr) and (IO === stdin and IO === stdout and IO === stderr) and (not (stdin.closed? or stdout.closed? or stderr.closed?)) #--}}} end def close! #--{{{ [stdin, stdout, stderr].each{|pipe| pipe.close} stdin, stdout, stderr = nil, nil, nil true #--}}} end alias close close! def hashify(*a) #--{{{ a.inject({}){|o,h| o.update(h)} #--}}} end private :hashify def execute(command, redirects = {}) #--{{{ $session_command = command if @debug raise(PipeError, command) unless ready? # clear buffers clear # setup redirects rerr = redirects[:e] || redirects[:err] || redirects[:stderr] || redirects['stderr'] || redirects['e'] || redirects['err'] || redirects[2] || redirects['2'] rout = redirects[:o] || redirects[:out] || redirects[:stdout] || redirects['stdout'] || redirects['o'] || redirects['out'] || redirects[1] || redirects['1'] # create cmd object and add to history cmd = Command::new command.to_s # store cmd if tracking history history << cmd if track_history # mutex for accessing shared data mutex = Mutex::new # io data for stderr and stdout err = { :io => stderr, :cmd => cmd.err, :name => 'stderr', :begin => false, :end => false, :begin_pat => cmd.begin_err_pat, :end_pat => cmd.end_err_pat, :redirect => rerr, :proc => errproc, :yield => lambda{|buf| yield(nil, buf)}, :mutex => mutex, } out = { :io => stdout, :cmd => cmd.out, :name => 'stdout', :begin => false, :end => false, :begin_pat => cmd.begin_out_pat, :end_pat => cmd.end_out_pat, :redirect => rout, :proc => outproc, :yield => lambda{|buf| yield(buf, nil)}, :mutex => mutex, } begin # send command in the background so we can begin processing output # immediately - thanks to tanaka akira for this suggestion threads << Thread::new { send_command cmd } # init main = Thread::current exceptions = [] # fire off reader threads [err, out].each do |iodat| threads << Thread::new(iodat, main) do |iodat, main| loop do main.raise(PipeError, command) unless ready? main.raise ExecutionError, iodat[:name] if iodat[:end] and not iodat[:begin] break if iodat[:end] or iodat[:io].eof? line = iodat[:io].gets buf = nil case line when iodat[:end_pat] iodat[:end] = true # handle the special case of non-newline terminated output if((m = %r/(.+)__CMD/o.match(line)) and (pre = m[1])) buf = pre end when iodat[:begin_pat] iodat[:begin] = true else next unless iodat[:begin] and not iodat[:end] # ignore chaff buf = line end if buf iodat[:mutex].synchronize do iodat[:cmd] << buf iodat[:redirect] << buf if iodat[:redirect] iodat[:proc].call buf if iodat[:proc] iodat[:yield].call buf if block_given? end end end true end end ensure # reap all threads - accumulating and rethrowing any exceptions begin while((t = threads.shift)) t.join raise ExecutionError, 'iodat thread failure' unless t.value end rescue => e exceptions << e retry unless threads.empty? ensure unless exceptions.empty? meta_message = '<' << exceptions.map{|e| "#{ e.message } - (#{ e.class })"}.join('|') << '>' meta_backtrace = exceptions.map{|e| e.backtrace}.flatten raise ExecutionError, meta_message, meta_backtrace end end end # this should only happen if eof was reached before end pat [err, out].each do |iodat| raise ExecutionError, iodat[:name] unless iodat[:begin] and iodat[:end] end # get the exit status get_status if respond_to? :get_status out = err = iodat = nil return [cmd.out, cmd.err] #--}}} end #--}}} end # class AbstractSession class Sh < AbstractSession #--{{{ DEFAULT_PROG = 'sh' ECHO = 'echo' attr :status alias exit_status status alias exitstatus status def clear #--{{{ stdin.puts "#{ ECHO } __clear__ 1>&2" stdin.puts "#{ ECHO } __clear__" stdin.flush while((line = stderr.gets) and line !~ %r/__clear__/o); end while((line = stdout.gets) and line !~ %r/__clear__/o); end self #--}}} end def send_command cmd #--{{{ stdin.printf "%s '%s' 1>&2\n", ECHO, cmd.begin_err stdin.printf "%s '%s' \n", ECHO, cmd.begin_out stdin.printf "%s\n", cmd.cmd stdin.printf "export __exit_status__=$?\n" stdin.printf "%s '%s' 1>&2\n", ECHO, cmd.end_err stdin.printf "%s '%s' \n", ECHO, cmd.end_out stdin.flush #--}}} end def get_status #--{{{ @status = get_var '__exit_status__' unless @status =~ /^\s*\d+\s*$/o raise ExecutionError, "could not determine exit status from <#{ @status.inspect }>" end @status = Integer @status #--}}} end def set_var name, value #--{{{ stdin.puts "export #{ name }=#{ value }" stdin.flush #--}}} end def get_var name #--{{{ stdin.puts "#{ ECHO } \"#{ name }=${#{ name }}\"" stdin.flush var = nil while((line = stdout.gets)) m = %r/#{ name }\s*=\s*(.*)/.match line if m var = m[1] raise ExecutionError, "could not determine <#{ name }> from <#{ line.inspect }>" unless var break end end var #--}}} end def path #--{{{ var = get_var 'PATH' var.strip.split %r/:/o #--}}} end def path= arg #--{{{ case arg when Array arg = arg.join ':' else arg = arg.to_s.strip end set_var 'PATH', "'#{ arg }'" self.path #--}}} end def execute(command, redirects = {}, &block) #--{{{ # setup redirect on stdin rin = redirects[:i] || redirects[:in] || redirects[:stdin] || redirects['stdin'] || redirects['i'] || redirects['in'] || redirects[0] || redirects['0'] if rin tmp = begin Tempfile::new rand.to_s rescue Tempfile::new rand.to_s end begin tmp.write( if rin.respond_to? 'read' rin.read elsif rin.respond_to? 'to_s' rin.to_s else rin end ) tmp.flush command = "{ #{ command } ;} < #{ tmp.path }" #puts command super(command, redirects, &block) ensure tmp.close! if tmp end else super end #--}}} end #--}}} end # class Sh class Bash < Sh #--{{{ DEFAULT_PROG = 'bash' class Login < Bash DEFAULT_PROG = 'bash --login' end #--}}} end # class Bash class Shell < Bash; end # IDL => interactive data language - see http://www.rsinc.com/ class IDL < AbstractSession #--{{{ class LicenseManagerError < StandardError; end DEFAULT_PROG = 'idl' MAX_TRIES = 32 def initialize(*args) #--{{{ tries = 0 ret = nil begin ret = super rescue LicenseManagerError => e tries += 1 if tries < MAX_TRIES sleep 1 retry else raise LicenseManagerError, "<#{ MAX_TRIES }> attempts <#{ e.message }>" end end ret #--}}} end def clear #--{{{ stdin.puts "retall" stdin.puts "printf, -2, '__clear__'" stdin.puts "printf, -1, '__clear__'" stdin.flush while((line = stderr.gets) and line !~ %r/__clear__/o) raise LicenseManagerError, line if line =~ %r/license\s*manager/io end while((line = stdout.gets) and line !~ %r/__clear__/o) raise LicenseManagerError, line if line =~ %r/license\s*manager/io end self #--}}} end def send_command cmd #--{{{ stdin.printf "printf, -2, '%s'\n", cmd.begin_err stdin.printf "printf, -1, '%s'\n", cmd.begin_out stdin.printf "%s\n", cmd.cmd stdin.printf "retall\n" stdin.printf "printf, -2, '%s'\n", cmd.end_err stdin.printf "printf, -1, '%s'\n", cmd.end_out stdin.flush #--}}} end def path #--{{{ stdout, stderr = execute "print, !path" stdout.strip.split %r/:/o #--}}} end def path= arg #--{{{ case arg when Array arg = arg.join ':' else arg = arg.to_s.strip end stdout, stderr = execute "!path='#{ arg }'" self.path #--}}} end #--}}} end # class IDL module Spawn #--{{{ class << self def spawn command #--{{{ ipath = tmpfifo opath = tmpfifo epath = tmpfifo cmd = "#{ command } < #{ ipath } 1> #{ opath } 2> #{ epath } &" system cmd i = open ipath, 'w' o = open opath, 'r' e = open epath, 'r' [i,o,e] #--}}} end def tmpfifo #--{{{ path = nil 42.times do |i| tpath = File::join(Dir::tmpdir, "#{ $$ }.#{ rand }.#{ i }") v = $VERBOSE begin $VERBOSE = nil system "mkfifo #{ tpath }" ensure $VERBOSE = v end next unless $? == 0 path = tpath at_exit{ File::unlink(path) rescue STDERR.puts("rm <#{ path }> failed") } break end raise "could not generate tmpfifo" unless path path #--}}} end end #--}}} end # module Spawn #--}}} end # module Session