require 'open3' require 'io/nonblock' module Session VERSION = '2.1.2' 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_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]*/m @end_pat = %r/[\s]*#{ Regexp.escape(@end) }[\s]*/m #@begin_pat = %r/^#{ Regexp.escape(@begin) }$/ #@end_pat = %r/^#{ Regexp.escape(@end) }$/ 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 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?))# and #(not (stdout.eof? or stderr.eof?)) #}}} 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 }\n" # 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 # ## stdin.puts "printf '\\n#{ cmd.begin }\\n' 1>&2" ## stdin.puts "printf '\\n#{ cmd.begin }\\n'" ## stdin.puts cmd.cmd ## stdin.puts "export __exit_status__=$?" ## stdin.puts "printf '\\n#{ cmd.end }\\n' 1>&2" ## stdin.puts "printf '\\n#{ cmd.end }\\n'" ## 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 } #puts #puts "buf <#{ buf.inspect }>" # buf.each do |line| #puts "line <#{ line.inspect }>" # 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.puts "#{ 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 class Shell < AbstractSession #{{{ DEFAULT_PROG = 'bash' ECHO = 'echo' attr :status alias exit_status status alias exitstatus status def echo_err s = '' #{{{ stdin.puts "echo #{ s } 1>&2" stdin.flush #}}} end def echo_out s = '' #{{{ stdin.puts "echo #{ s }" stdin.flush #}}} end 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 "%s 1>&2; %s '%s' 1>&2\n", ECHO, ECHO, cmd.begin # stdin.printf "%s ; %s '%s' \n", ECHO, ECHO, cmd.begin # # stdin.printf "%s\n", cmd.cmd # stdin.printf "__exit_status__=$?\n" # # stdin.printf "%s 1>&2; %s '%s' 1>&2\n", ECHO, ECHO, cmd.end # stdin.printf "%s ; %s '%s' \n", ECHO, ECHO, cmd.end echo_err; echo_err "'#{ cmd.begin }'" echo_out; echo_out "'#{ cmd.begin }'" stdin.printf "%s\n", cmd.cmd stdin.printf "__exit_status__=$?\n" stdin.printf "%s 1>&2; %s '%s' 1>&2\n", ECHO, ECHO, cmd.end stdin.printf "%s ; %s '%s' \n", ECHO, ECHO, cmd.end stdin.flush # process stdout/stderr err = { :io => stderr, :name => 'stderr', :buf => '', :begin => nil, :idx => nil, :end => nil, :done => false, :redirect => rerr, :proc => errproc, :yield => lambda{|b| yield(nil, b)}, } out = { :io => stdout, :name => 'stdout', :buf => '', :begin => nil, :idx => nil, :end => nil, :done => false, :redirect => rout, :proc => outproc, :yield => lambda{|b| yield(b, nil)}, } loop do raise(PipeError, command) unless ready? break if err[:done] and out[:done] rfds, wfds, efds = select [stdout, stderr], nil, [stdout, stderr] unless efds.empty? msg = efds.map{|io| "err on <#{ io.inspect }>"}.join(',') raise PipeError, msg end rfds.each do |io| next if io.eof? iodat = case io when stderr err when stdout out else raise ExecutionError, "unknown IO #{ io.inspect }" end # case raise ExecutionError, iodat[:name] if iodat[:done] iodat[:buf] << iodat[:io].nonblock{ iodat[:io].read } if $DEBUG puts puts '====' puts iodat[:name] p iodat[:buf] end unless iodat[:begin] m = cmd.begin_pat.match iodat[:buf] if m iodat[:begin] = m.end(0) iodat[:idx] = iodat[:begin] iodat[:end] = -1 if $DEBUG puts 'FOUND BEGIN' end end end #else unless iodat[:done] m = cmd.end_pat.match iodat[:buf] if m iodat[:end] = m.begin(0) - 1 iodat[:done] = true if $DEBUG puts 'FOUND END' puts "m.begin <#{ m.begin 0 }>" puts "m.end <#{ m.end 0}>" end end end if $DEBUG p iodat[:begin] p iodat[:idx] p iodat[:end] if iodat[:begin] and iodat[:idx] and iodat[:end] #s = iodat[:buf].dup.gsub %r/./, ' ' puts '----' p iodat[:buf] s = iodat[:buf].dup << ' ' s[iodat[:begin]] = '^' p s s = iodat[:buf].dup << ' ' s[iodat[:idx]] = '_' p s s = iodat[:buf].dup << ' ' s[iodat[:end]] = '.' p s puts '----' end end if iodat[:begin] and iodat[:idx] < iodat[:buf].size and iodat[:end] < iodat[:buf].size and iodat[:idx] < iodat[:end] buf = iodat[:buf][iodat[:idx] .. iodat[:end]] #if buf and not buf.empty? iodat[:redirect] << buf if iodat[:redirect] iodat[:proc].call(buf) if iodat[:proc] iodat[:yield].call(buf) if block_given? #end iodat[:idx] = iodat[:buf].size end end # each end # loop raise ExecutionError, "stderr" unless err[:done] cmd.err = err[:buf][err[:begin] .. err[:end]] raise ExecutionError, "stdout" unless out[:done] cmd.out = out[:buf][out[:begin] .. out[:end]] err = out = nil # 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 clear 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 # class AbstractShell < AbstractSession ##{{{ # EXIT_SUCCESS = 0 # attr :status # alias exit_status status # alias exitstatus status # # def initialize(*args) ##{{{ # ret = super # @status = EXIT_SUCCESS # ret ##}}} # end # def echo_err s ##{{{ # stdin.printf "echo '#{ s }' 1>&2\n" # stdin.flush ##}}} # end # def echo_out s ##{{{ # stdin.printf "echo '#{ s }' \n" # stdin.flush ##}}} # end # def clear ##{{{ # echo_err '__clear__' # echo_out '__clear__' # 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 set_var name, value ##{{{ # stdin.printf "export #{ name }=\"#{ value }\"\n" # stdin.flush ##}}} # end # def get_var name ##{{{ # stdin.printf "echo \"$#{ name }\"\n" # stdin.flush # var = stdout.gets ##}}} # end # def send_command command ##{{{ # stdin.printf "%s\n", command # stdin.flush # set_var '__exit_status__', '$?' ##}}} # end # def get_status ##{{{ # clear # @status = get_var '__exit_status__' # unless @status =~ /^\s*[-]?\d+\s*$/ # raise ExecutionError, "could not determine exit status from <#{ @status.inspect }>" # end # @status = Integer @status ##}}} # 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 # # # clear # self.clear # # # send command # echo_err cmd.begin # echo_out cmd.begin # send_command cmd.cmd # echo_err cmd.end # echo_out cmd.end # # # 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 # get_status # # return [cmd.out, cmd.err] ##}}} # 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 ##}}} # end # class Sh < AbstractShell ##{{{ # DEFAULT_PROG = 'sh' ##}}} # end # class Bash < Sh ##{{{ # DEFAULT_PROG = 'bash' ##}}} # end # class Csh < AbstractShell ##{{{ # DEFAULT_PROG = 'csh' # def echo_err s # @stdin.printf "echo '#{ s }' > /dev/stderr\n" # end # def set_var name, value ##{{{ # stdin.printf "set #{ name }=\"#{ value }\"\n" # stdin.flush ##}}} # end ##}}} # end # class Tcsh < Csh ##{{{ # DEFAULT_PROG = 'tcsh' ##}}} # end