summaryrefslogtreecommitdiffstats
path: root/cucumber/features/support/helpers/remote_shell.rb
diff options
context:
space:
mode:
Diffstat (limited to 'cucumber/features/support/helpers/remote_shell.rb')
-rw-r--r--cucumber/features/support/helpers/remote_shell.rb171
1 files changed, 171 insertions, 0 deletions
diff --git a/cucumber/features/support/helpers/remote_shell.rb b/cucumber/features/support/helpers/remote_shell.rb
new file mode 100644
index 00000000..b890578b
--- /dev/null
+++ b/cucumber/features/support/helpers/remote_shell.rb
@@ -0,0 +1,171 @@
+require 'base64'
+require 'json'
+require 'socket'
+require 'timeout'
+
+module RemoteShell
+ class ServerFailure < StandardError
+ end
+
+ # Used to differentiate vs Timeout::Error, which is thrown by
+ # try_for() (by default) and often wraps around remote shell usage
+ # -- in that case we don't want to catch that "outer" exception in
+ # our handling of remote shell timeouts below.
+ class Timeout < ServerFailure
+ end
+
+ DEFAULT_TIMEOUT = 20*60
+
+ # Counter providing unique id:s for each communicate() call.
+ @@request_id ||= 0
+
+ def communicate(vm, *args, **opts)
+ opts[:timeout] ||= DEFAULT_TIMEOUT
+ socket = TCPSocket.new("127.0.0.1", vm.get_remote_shell_port)
+ id = (@@request_id += 1)
+ # Since we already have defined our own Timeout in the current
+ # scope, we have to be more careful when referring to the Timeout
+ # class from the 'timeout' module. However, note that we want it
+ # to throw our own Timeout exception.
+ Object::Timeout.timeout(opts[:timeout], Timeout) do
+ socket.puts(JSON.dump([id] + args))
+ socket.flush
+ loop do
+ line = socket.readline("\n").chomp("\n")
+ response_id, status, *rest = JSON.load(line)
+ if response_id == id
+ if status != "success"
+ if status == "error" and rest.class == Array and rest.size == 1
+ msg = rest.first
+ raise ServerFailure.new("#{msg}")
+ else
+ raise ServerFailure.new("Uncaught exception: #{status}: #{rest}")
+ end
+ end
+ return rest
+ else
+ debug_log("Dropped out-of-order remote shell response: " +
+ "got id #{response_id} but expected id #{id}")
+ end
+ end
+ end
+ ensure
+ socket.close if defined?(socket) && socket
+ end
+
+ module_function :communicate
+ private :communicate
+
+ class ShellCommand
+ # If `:spawn` is false the server will block until it has finished
+ # executing `cmd`. If it's true the server won't block, and the
+ # response will always be [0, "", ""] (only used as an
+ # ACK). execute() will always block until a response is received,
+ # though. Spawning is useful when starting processes in the
+ # background (or running scripts that does the same) or any
+ # application we want to interact with.
+ def self.execute(vm, cmd, **opts)
+ opts[:user] ||= "root"
+ opts[:spawn] = false unless opts.has_key?(:spawn)
+ type = opts[:spawn] ? "spawn" : "call"
+ debug_log("#{type}ing as #{opts[:user]}: #{cmd}")
+ ret = RemoteShell.communicate(vm, 'sh_' + type, opts[:user], cmd, **opts)
+ debug_log("#{type} returned: #{ret}") if not(opts[:spawn])
+ return ret
+ end
+
+ attr_reader :cmd, :returncode, :stdout, :stderr
+
+ def initialize(vm, cmd, **opts)
+ @cmd = cmd
+ @returncode, @stdout, @stderr = self.class.execute(vm, cmd, **opts)
+ end
+
+ def success?
+ return @returncode == 0
+ end
+
+ def failure?
+ return not(success?)
+ end
+
+ def to_s
+ "Return status: #{@returncode}\n" +
+ "STDOUT:\n" +
+ @stdout +
+ "STDERR:\n" +
+ @stderr
+ end
+ end
+
+ class PythonCommand
+ def self.execute(vm, code, **opts)
+ opts[:user] ||= "root"
+ show_code = code.chomp
+ if show_code["\n"]
+ show_code = "\n" + show_code.lines.map { |l| " "*4 + l.chomp } .join("\n")
+ end
+ debug_log("executing Python as #{opts[:user]}: #{show_code}")
+ ret = RemoteShell.communicate(
+ vm, 'python_execute', opts[:user], code, **opts
+ )
+ debug_log("execution complete")
+ return ret
+ end
+
+ attr_reader :code, :exception, :stdout, :stderr
+
+ def initialize(vm, code, **opts)
+ @code = code
+ @exception, @stdout, @stderr = self.class.execute(vm, code, **opts)
+ end
+
+ def success?
+ return @exception == nil
+ end
+
+ def failure?
+ return not(success?)
+ end
+
+ def to_s
+ "Exception: #{@exception}\n" +
+ "STDOUT:\n" +
+ @stdout +
+ "STDERR:\n" +
+ @stderr
+ end
+ end
+
+ # An IO-like object that is more or less equivalent to a File object
+ # opened in rw mode.
+ class File
+ def self.open(vm, mode, path, *args, **opts)
+ debug_log("opening file #{path} in '#{mode}' mode")
+ ret = RemoteShell.communicate(vm, 'file_' + mode, path, *args, **opts)
+ if ret.size != 1
+ raise ServerFailure.new("expected 1 value but got #{ret.size}")
+ end
+ debug_log("#{mode} complete")
+ return ret.first
+ end
+
+ attr_reader :vm, :path
+
+ def initialize(vm, path)
+ @vm, @path = vm, path
+ end
+
+ def read()
+ Base64.decode64(self.class.open(@vm, 'read', @path))
+ end
+
+ def write(data)
+ self.class.open(@vm, 'write', @path, Base64.encode64(data))
+ end
+
+ def append(data)
+ self.class.open(@vm, 'append', @path, Base64.encode64(data))
+ end
+ end
+end