require 'open3' require 'io/nonblock' module Session VERSION = '2.1.1' class History #{{{ def initialize; @a = []; end def method_missing(m, *a, &b); @a.send(m, *a, &b); end def to_s @a.inject(''){|s,cmd| s << format("%d: %s\n", cmd.cmdno, cmd)} end alias to_str to_s def inspect @a.inject(''){|s,cmd| s << cmd.inspect} end #}}} end class Command #{{{ class << self @@cmdno = 0 def cmdno; @@cmdno; end def cmdno= n; @@cmdno = n; end end attr :cmd attr :cmdno attr :out,true attr :err,true attr :cid attr :begin attr :end attr :begin_pat attr :end_pat def initialize(command) @cmd = command.to_s @cmdno = self.class.cmdno self.class.cmdno += 1 @err = '' @out = '' @cid = "%d.%d.%d" % [$$, cmdno, Time.now.usec] @begin = "__COMMAND_%s_BEGIN__" % cid @end = "__COMMAND_%s_END__" % cid @begin_pat = %r/^\s*#{ Regexp.escape(@begin) }\s*$/ @end_pat = %r/^\s*#{ Regexp.escape(@end) }\s*$/ end def to_s; cmd end def to_str; cmd end def inspect format("cmd: <%s>\n\tcmdno: <%s>\n\tout: <%s>\n\terr: <%s>\n\tcid: <%s>\n", cmd, cmdno, out.inspect, err.inspect, cid) end #}}} end class AbstractSession #{{{ class PipeError < StandardError; end class ExecutionError < StandardError; end # 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 def default_timeout return @@default_timeout if defined? @@default_timeout and @@default_timeout if defined? self::DEFAULT_TIMEOUT return @@default_timeout = self::DEFAULT_TIMEOUT else @@default_timeout = ENV["SESSION_#{ self }_TIMEOUT"] end nil end def default_timeout= timeout @@default_timeout = timeout end end #}}} # instance methods attr :opts attr :prog attr :stdin alias i stdin attr :stdout alias o stdout attr :stderr alias e stderr attr :history attr :track_history, true attr :outproc, true attr :errproc, true def initialize(*args) #{{{ @opts = hashify(*args) @prog = opts[:prog] || self.class.default_prog or raise(ArgumentError, "no program specified") @history = History.new @track_history = false @track_history = opts[:history] if opts.has_key? :history @track_history = opts[:track_history] if opts.has_key? :track_history @outproc = nil @errproc = nil @stdin, @stdout, @stderr = Open3.popen3(prog) clear if block_given? ret = nil begin ret = yield self ensure self.close! end return ret end return self #}}} end def clear raise NotImplementedError end alias flush clear def execute(command, timeout = nil, track = true) raise NotImplementedError end def path raise NotImplementedError end def path= raise NotImplementedError 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 #}}} end class IDL < AbstractSession #{{{ DEFAULT_PROG = 'idl' def clear #{{{ stdin.printf "retall\n" stdin.printf "printf, -2, '__clear__'\n" stdin.printf "printf, -1, '__clear__'\n" stdin.flush while((line = stderr.gets) and line !~ %r/^\s*__clear__\s*$/o); end while((line = stdout.gets) and line !~ %r/^\s*__clear__\s*$/o); end self #}}} end def execute(command, redirects = {}) #{{{ raise(PipeError, command) unless ready? # setup redirects rerr = redirects[:e] || redirects[:err] || redirects[2] || redirects[:stderr] || redirects['stderr'] rout = redirects[:o] || redirects[:out] || redirects[1] || redirects[:stdout] || redirects['stdout'] # send command cmd = Command.new(command) history << cmd if track_history stdin.printf "retall\n" stdin.printf "printf, -1, '%s'\n", cmd.begin stdin.printf "printf, -2, '%s'\n", cmd.begin stdin.printf "retall\n" stdin.printf "%s\n", cmd.cmd stdin.printf "retall\n" stdin.printf "printf, -1, '%s'\n", cmd.end stdin.printf "printf, -2, '%s'\n", cmd.end stdin.flush # process stdout/stderr stdout_begin = false stderr_begin = false stdout_end = false stderr_end = false loop do break if stdout_end and stderr_end rfds = efds = [stdout, stderr] rfds, wfds, efds = select rfds, nil, efds unless efds.empty? msgs = [] efds.each{|io| msgs << "error on <#{ io.inspect }>"} raise PipeError, msgs.join(',') end rfds.each do |io| case io when stderr if stderr_end raise ExecutionError, "stderr" end if stderr.eof? next end buf = stderr.nonblock{ stderr.read } buf.each do |line| if line =~ cmd.begin_pat stderr_begin = true next end if line =~ cmd.end_pat stderr_end = true next end if not stderr_begin or stderr_end raise ExecutionError, "stderr begin <#{ stderr_begin }> end <#{ stderr_end}>" end cmd.err << line rerr << line if rerr @errproc.call(line) if @errproc yield(nil, line) if block_given? end when stdout if stdout_end raise ExecutionError, "stdout" end if stdout.eof? next end buf = stdout.nonblock{ stdout.read } buf.each do |line| if line =~ cmd.begin_pat stdout_begin = true next end if line =~ cmd.end_pat stdout_end = true next end if not stdout_begin or stdout_end "stdout begin <#{ stdout_begin }> end <#{ stdout_end}>" end cmd.out << line rout << line if rout @outproc.call(line) if @outproc yield(line, nil) if block_given? end end end # each end # loop unless stderr_begin and stderr_end raise ExecutionError, "stderr" end unless stdout_begin and stdout_end raise ExecutionError, "stdout" end return [cmd.out, cmd.err] #}}} 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 Shell < AbstractSession #{{{ DEFAULT_PROG = 'bash' ECHO = 'echo' attr :status alias exit_status status alias exitstatus status def clear #{{{ stdin.printf "#{ ECHO } __clear__ 1>&2\n" stdin.printf "#{ ECHO } __clear__\n" stdin.flush while((line = stderr.gets) and line !~ %r/^\s*__clear__\s*$/o); end while((line = stdout.gets) and line !~ %r/^\s*__clear__\s*$/o); end self #}}} end def execute(command, redirects = {}) #{{{ raise(PipeError, command) unless ready? # setup redirects rerr = redirects[:e] || redirects[:err] || redirects[2] || redirects[:stderr] || redirects['stderr'] rout = redirects[:o] || redirects[:out] || redirects[1] || redirects[:stdout] || redirects['stdout'] # create cmd object and add to history cmd = Command.new(command.to_s) history << cmd if track_history # send command stdin.printf "#{ ECHO } '%s'\n", cmd.begin stdin.printf "#{ ECHO } '%s' 1>&2\n", cmd.begin stdin.printf "%s\n", cmd.cmd stdin.printf "__exit_status__=$?\n" stdin.printf "#{ ECHO } '%s'\n", cmd.end stdin.printf "#{ ECHO } '%s' 1>&2\n", cmd.end stdin.flush # process stdout/stderr stdout_begin = false stderr_begin = false stdout_end = false stderr_end = false loop do break if stdout_end and stderr_end rfds = efds = [stdout, stderr] rfds, wfds, efds = select rfds, nil, efds unless efds.empty? msgs = [] efds.each{|io| msgs << "error on <#{ io.inspect }>"} raise PipeError, msgs.join(',') end rfds.each do |io| case io when stderr if stderr_end raise ExecutionError, "stderr" end if stderr.eof? next end buf = stderr.nonblock{ stderr.read } buf.each do |line| if line =~ cmd.begin_pat stderr_begin = true next end if line =~ cmd.end_pat stderr_end = true next end if not stderr_begin or stderr_end raise ExecutionError, "stderr begin <#{ stderr_begin }> end <#{ stderr_end}>" end cmd.err << line rerr << line if rerr @errproc.call(line) if @errproc yield(nil, line) if block_given? end when stdout if stdout_end raise ExecutionError, "stdout" end if stdout.eof? next end buf = stdout.nonblock{ stdout.read } buf.each do |line| if line =~ cmd.begin_pat stdout_begin = true next end if line =~ cmd.end_pat stdout_end = true next end if not stdout_begin or stdout_end "stdout begin <#{ stdout_begin }> end <#{ stdout_end}>" end cmd.out << line rout << line if rout @outproc.call(line) if @outproc yield(line, nil) if block_given? end end end # each end # loop unless stderr_begin and stderr_end raise ExecutionError, "stderr" end unless stdout_begin and stdout_end raise ExecutionError, "stdout" end # get the exit status stdin.printf "#{ ECHO } $__exit_status__\n" stdin.flush @status = stdout.gets unless @status =~ /^\s*\d+\s*$/ raise ExecutionError, "could not determine exit status from <#{ @status.inspect }>" end @status = Integer @status return [cmd.out, cmd.err] #}}} end def path #{{{ stdout, stderr = execute "#{ ECHO } $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 "export PATH='#{ arg }'" self.path #}}} end #}}} end end