class Loom::Shell::Core

Attributes

dry_run[R]
mod_loader[R]
session[R]
shell_api[R]

Public Class Methods

new(mod_loader, sshkit_backend, dry_run=false) click to toggle source
# File lib/loom/shell/core.rb, line 9
def initialize(mod_loader, sshkit_backend, dry_run=false)
  @dry_run = dry_run
  @mod_loader = mod_loader
  @sshkit_backend = sshkit_backend

  @session = Session.new
  @shell_api = Api.new self

  @cmd_wrappers = []
  @sudo_users = []

  # TODO: @sudo_dirs is a smelly workaround for not having a better
  # understanding of sudo security policies and inheriting environments.
  @sudo_dirs = []
end

Public Instance Methods

capture(*cmd_parts) click to toggle source
# File lib/loom/shell/core.rb, line 149
def capture(*cmd_parts)
  if @dry_run
    # TODO: I'm not sure what to do about this.
    Loom.log.warn "`capture` during dry run won't do what you want"
  end
  execute *cmd_parts
  @session.last.stdout.strip
end
cd(path, &block) click to toggle source
  Loom.log.debug3(self) { "sudo legacy... the other way" }
  sudo_stack_and_wrap(user, *sudo_cmd, &block)
else
  Loom.log.debug3(self) { "sudo legacy" }
  sudo_stack_and_wrap(user, *sudo_cmd)
end

end

# File lib/loom/shell/core.rb, line 135
def cd(path, &block)
  Loom.log.debug1(self) { "cd => #{path} #{block}" }

  # TODO: this might creates problems with relative paths, e.g.
  # loom.cd foo => cd ./foo
  # loom.sudo user => cd ./foo; sudo user
  @sudo_dirs.push path
  begin
    @sshkit_backend.within path, &block
  ensure
    @sudo_dirs.pop
  end
end
exec(*cmd_parts, is_test: false, **cmd_opts)
Alias for: execute
execute(*cmd_parts, is_test: false, **cmd_opts) click to toggle source
# File lib/loom/shell/core.rb, line 167
def execute(*cmd_parts, is_test: false, **cmd_opts)
  cmd_parts.compact!
  raise "empty command passed to execute" if cmd_parts.empty?

  sshkit_result = if @dry_run
                    wrap(:printf, first: true) do
                      r = execute_internal(*cmd_parts, **cmd_opts)
                      Loom.log.info { "\t%s" % prompt_fmt(cmd_result.full_stdout.strip) }
                      r
                    end
                  else
                    execute_internal(*cmd_parts, **cmd_opts)
                  end
  result =
    CmdResult.create_from_sshkit_command(sshkit_result, is_test, self)

  @session << result
  Loom.log.debug result.stdout unless result.stdout.empty?
  Loom.log.debug result.stderr unless result.stderr.empty?
  result
end
Also aliased as: exec
is_sudo?() click to toggle source
# File lib/loom/shell/core.rb, line 27
def is_sudo?
  !@sudo_users.empty?
end
local() click to toggle source
# File lib/loom/shell/core.rb, line 31
def local
  @local ||= LocalShell.new @mod_loader, @session, @dry_run
end
pipe(*cmds) click to toggle source
# File lib/loom/shell/core.rb, line 158
def pipe(*cmds)
  cmd = CmdWrapper.pipe *cmds.map { |*cmd| CmdWrapper.new *cmd }
  execute cmd
end
sudo(user=nil, *sudo_cmd) { || ... } click to toggle source
# File lib/loom/shell/core.rb, line 82
def sudo(user=nil, *sudo_cmd, &block)
  user ||= :root
  Loom.log.debug1(self) { "sudo => #{user} #{sudo_cmd} #{block}" }

  is_new_sudoer = @sudo_users.last.to_sym != user.to_sym rescue true

  @sudo_dirs.push(capture :pwd)
  @sudo_users.push << user if is_new_sudoer

  sudo_wrapper = [:sudo, "-u", user, "--", "/bin/sh", "-c"]
  sudo_cmd.compact!
  begin
    wrap *sudo_wrapper, :should_quote => true do
      execute *sudo_cmd unless sudo_cmd.empty?
      yield if block_given?
    end
  ensure
    @sudo_users.pop if is_new_sudoer
    @sudo_dirs.pop
  end
end
test(*cmd, check: :exit_status, **cmd_opts) click to toggle source
# File lib/loom/shell/core.rb, line 35
def test(*cmd, check: :exit_status, **cmd_opts)
  # TODO: is_test smells like a hack. I can't rely on Command#is_success?
  # here (returned from execute) because I'm overriding it with :is_test =>
  # true. Fix Command#is_success? to not be a lie.. that is a lazy hack for
  # result reporting (I think the fix & feature) is to define Command
  # objects and declare style of reporting & error code handling it
  # has. Commands can be defined to ignore errors and just return their
  # results.
  # @see the TODO at Loom::Runner+execute_pattern+
  execute *cmd, :is_test => true, **cmd_opts

  case check
  when :exit_status
    @session.last.exit_status == 0
  when :stderr
    @session.last.stderr.empty?
  else
    raise "unknown test check => #{check}"
  end
end
upload(local_path, remote_path) click to toggle source
# File lib/loom/shell/core.rb, line 163
def upload(local_path, remote_path)
  @sshkit_backend.upload! local_path, remote_path
end
verify(*check) click to toggle source
# File lib/loom/shell/core.rb, line 56
def verify(*check)
  raise VerifyError, check unless test *check
end
verify_which(command) click to toggle source
# File lib/loom/shell/core.rb, line 60
def verify_which(command)
  verify :which, command
end
wrap(*wrapper, first: false, should_quote: true) { || ... } click to toggle source
# File lib/loom/shell/core.rb, line 64
def wrap(*wrapper, first: false, should_quote: true, &block)
  raise "missing block for +wrap+" unless block_given?

  cmd_wrapper = CmdWrapper.new(*wrapper, should_quote: should_quote)

  if first
    @cmd_wrappers.unshift(cmd_wrapper)
  else
    @cmd_wrappers.push(cmd_wrapper)
  end

  begin
    yield
  ensure
    first ? @cmd_wrappers.shift : @cmd_wrappers.pop
  end
end

Protected Instance Methods

prompt_label() click to toggle source
# File lib/loom/shell/core.rb, line 191
def prompt_label
  # TODO: get the real hostname.
  "remote"
end

Private Instance Methods

create_command(*cmd_parts) click to toggle source

Here be dragons. @return [String|Loom::Shell::CmdWrapper]

# File lib/loom/shell/core.rb, line 229
def create_command(*cmd_parts)
  cmd_wrapper = if cmd_parts.is_a? CmdWrapper
                  Loom.log.debug3(self) { "existing cmd from args => #{cmd_parts}" }
                  cmd_parts
                elsif cmd_parts[0].is_a? CmdWrapper
                  Loom.log.debug3(self) { "existing cmd from args[0] => #{cmd_parts[0]}" }
                  cmd_parts[0]
                else
                  Loom.log.debug3(self) { "new cmd from args => #{cmd_parts}" }
                  CmdWrapper.new *cmd_parts
                end

  # Useful for sudo, dry runs, timing a set of commands, or
  # timeout... anytime you want to prefix a group of commands.  Reverses the
  # array to wrap from inner most call to `#{wrap}` to outer most.
  cmd = @cmd_wrappers.reverse.reduce(cmd_wrapper) do |cmd_or_wrapper, wrapper|
    Loom.log.debug3(self) { "wrapping cmds => #{wrapper} => #{cmd_or_wrapper}"}
    wrapper.wrap cmd_or_wrapper
  end

  unless @sudo_dirs.empty? || @dry_run
    cmd = "cd #{@sudo_dirs.last}; " << cmd.to_s
  end
  cmd
end
execute_internal(*cmd_parts, pipe_to: []) click to toggle source
# File lib/loom/shell/core.rb, line 202
def execute_internal(*cmd_parts, pipe_to: [])
  primary_cmd = create_command *cmd_parts
  # TODO: where is piped_cmds used?
  piped_cmds = pipe_to.map { |cmd_parts| CmdWrapper.new *cmd_parts }

  cmd = CmdPipeline.new([primary_cmd].concat(piped_cmds)).to_s
  # Tests if the command looks like "echo\ hi", the trailing slash after
  # echo indicates that just 1 big string was passed in and we can't really
  # isolate the execuatable part of the command. This might be fine, but
  # it's better to be strict now and relax this later if it's OK.
  if cmd.match /^[\w\-\[]+\\/i
    raise "use array parts for command escaping => #{cmd}"
  end

  Loom.log.debug1(self) { "executing => #{cmd}" }

  # This is a big hack to get access to the SSHKit command
  # object and avoid the automatic errors thrown on non-zero
  # error codes
  @sshkit_backend.send(
    :create_command_and_execute,
    cmd,
    :raise_on_non_zero_exit => false)
end
prompt_fmt(*cmd_parts) click to toggle source
# File lib/loom/shell/core.rb, line 197
def prompt_fmt(*cmd_parts)
  output = Shellwords.join(cmd_parts).gsub /\\/, ''
  "[%s]:$ %s" % [prompt_label, output]
end