Class: Sunshine::Shell

Inherits:
Object
  • Object
show all
Includes:
Open4
Defined in:
lib/sunshine/shell.rb

Overview

The Shell class handles local input, output and execution to the shell.

Direct Known Subclasses

RemoteShell

Constant Summary collapse

LOCAL_USER =
`whoami`.chomp
LOCAL_HOST =
`hostname`.chomp

Class Attribute Summary collapse

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(output = $stdout, options = {}) ⇒ Shell

Returns a new instance of Shell.



31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
# File 'lib/sunshine/shell.rb', line 31

def initialize output = $stdout, options={}
  @output = output

  $stdin.sync
  @input = HighLine.new $stdin

  @user = LOCAL_USER
  @host = LOCAL_HOST

  @sudo     = options[:sudo]
  @env      = options[:env] || {}
  @password = options[:password]

  @timeout = options[:timeout] || Sunshine.timeout
  @idle_time = options[:idle_after] || 1

  @mutex = @pid = nil
end

Class Attribute Details

.sudo_failed_matcherObject

The message to match in stderr to determine logging in has failed. Defaults to:

/^Sorry, try again./


17
18
19
# File 'lib/sunshine/shell.rb', line 17

def sudo_failed_matcher
  @sudo_failed_matcher
end

.sudo_prompt_matcherObject

The message to match in stderr to determine a password is required. Defaults to:

/^Password:/


22
23
24
# File 'lib/sunshine/shell.rb', line 22

def sudo_prompt_matcher
  @sudo_prompt_matcher
end

Instance Attribute Details

#envObject

Returns the value of attribute env.



29
30
31
# File 'lib/sunshine/shell.rb', line 29

def env
  @env
end

#hostObject (readonly)

Returns the value of attribute host.



28
29
30
# File 'lib/sunshine/shell.rb', line 28

def host
  @host
end

#inputObject (readonly)

Returns the value of attribute input.



28
29
30
# File 'lib/sunshine/shell.rb', line 28

def input
  @input
end

#mutexObject (readonly)

Returns the value of attribute mutex.



28
29
30
# File 'lib/sunshine/shell.rb', line 28

def mutex
  @mutex
end

#outputObject (readonly)

Returns the value of attribute output.



28
29
30
# File 'lib/sunshine/shell.rb', line 28

def output
  @output
end

#passwordObject (readonly)

Returns the value of attribute password.



28
29
30
# File 'lib/sunshine/shell.rb', line 28

def password
  @password
end

#pidObject (readonly)

Returns the value of attribute pid.



28
29
30
# File 'lib/sunshine/shell.rb', line 28

def pid
  @pid
end

#sudoObject

Returns the value of attribute sudo.



29
30
31
# File 'lib/sunshine/shell.rb', line 29

def sudo
  @sudo
end

#timeoutObject

Returns the value of attribute timeout.



29
30
31
# File 'lib/sunshine/shell.rb', line 29

def timeout
  @timeout
end

#userObject (readonly)

Returns the value of attribute user.



28
29
30
# File 'lib/sunshine/shell.rb', line 28

def user
  @user
end

Instance Method Details

#==(shell) ⇒ Object

Checks for equality



54
55
56
# File 'lib/sunshine/shell.rb', line 54

def == shell
  @host == shell.host && @user == shell.user rescue false
end

#agree(*args, &block) ⇒ Object

Prompt the user to agree.



70
71
72
# File 'lib/sunshine/shell.rb', line 70

def agree(*args, &block)
  sync{ @input.agree(*args, &block) }
end

#ask(*args, &block) ⇒ Object

Prompt the user for input.



62
63
64
# File 'lib/sunshine/shell.rb', line 62

def ask(*args, &block)
  sync{ @input.ask(*args, &block) }
end

#call(cmd, options = {}, &block) ⇒ Object

Execute a command on the local system and return the output.



78
79
80
81
82
# File 'lib/sunshine/shell.rb', line 78

def call cmd, options={}, &block
  Sunshine.logger.info @host, "Running: #{cmd}" do
    execute sudo_cmd(cmd, options), &block
  end
end

#choose(&block) ⇒ Object

Prompt the user to make a choice.



88
89
90
# File 'lib/sunshine/shell.rb', line 88

def choose &block
  sync{ @input.choose(&block) }
end

#closeObject

Close the output IO. (Required by the Logger class)



96
97
98
# File 'lib/sunshine/shell.rb', line 96

def close
  @output.close
end

#connectObject

Returns true. Compatibility method with RemoteShell.



104
105
106
# File 'lib/sunshine/shell.rb', line 104

def connect
  true
end

#connected?Boolean

Returns true. Compatibility method with RemoteShell.

Returns:

  • (Boolean)


112
113
114
# File 'lib/sunshine/shell.rb', line 112

def connected?
  true
end

#disconnectObject

Returns true. Compatibility method with RemoteShell.



120
121
122
# File 'lib/sunshine/shell.rb', line 120

def disconnect
  true
end

#download(from_path, to_path, options = {}, &block) ⇒ Object Also known as: upload

Copies a file. Compatibility method with RemoteShell.



128
129
130
131
132
# File 'lib/sunshine/shell.rb', line 128

def download from_path, to_path, options={}, &block
  Sunshine.logger.info @host, "Copying #{from_path} -> #{to_path}" do
    FileUtils.cp_r from_path, to_path
  end
end

#env_cmd(cmd, env_hash = @env) ⇒ Object

Build an env command if an env_hash is passed



223
224
225
226
227
228
229
# File 'lib/sunshine/shell.rb', line 223

def env_cmd cmd, env_hash=@env
  if env_hash && !env_hash.empty?
    env_vars = env_hash.map{|e| e.join("=")}
    cmd = ["env", env_vars, cmd].flatten
  end
  cmd
end

#execute(cmd) ⇒ Object

Execute a command with open4 and loop until the process exits. The cmd argument may be a string or an array. If a block is passed, it will be called when data is received and passed the stream type and stream string value:

shell.execute "test -s 'blah' && echo 'true'" do |stream, str|
  stream    #=> :stdout
  string    #=> 'true'
end

The method returns the output from the stdout stream by default, and raises a CmdError if the exit status of the command is not zero.



354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
# File 'lib/sunshine/shell.rb', line 354

def execute cmd
  cmd = [cmd] unless Array === cmd
  @pid, inn, out, err = popen4(*cmd)

  inn.sync = true
  log_methods = {out => :debug, err => :error}

  result, status = process_streams(@pid, out, err) do |stream, data|
    stream_name = :out if stream == out
    stream_name = :err if stream == err
    stream_name = :inn if stream == inn

    Sunshine.logger.send log_methods[stream],
      "#{@host}:#{stream_name}", data

    # User blocks should run with sync threads to avoid badness.
    sync do
      yield(stream_name, data, inn) if block_given?
    end


    if password_required?(stream_name, data) then

      kill_process(@pid) unless Sunshine.interactive?

      send_password_to_stream(inn, data)
    end
  end

  raise_command_failed(status, cmd) unless status.success?

  result[out].join.chomp

ensure
  inn.close rescue nil
  out.close rescue nil
  err.close rescue nil
  @pid = nil
end

#expand_path(path) ⇒ Object

Expands the path. Compatibility method with RemoteShell.



140
141
142
# File 'lib/sunshine/shell.rb', line 140

def expand_path path
  File.expand_path path
end

#file?(filepath) ⇒ Boolean

Checks if file exists. Compatibility method with RemoteShell.

Returns:

  • (Boolean)


148
149
150
# File 'lib/sunshine/shell.rb', line 148

def file? filepath
  File.file? filepath
end

#idle?(start_time = @cmd_activity, max_time = @idle_time) ⇒ Boolean

Checks if shell is still receiving data.

Returns:

  • (Boolean)


173
174
175
# File 'lib/sunshine/shell.rb', line 173

def idle? start_time=@cmd_activity, max_time=@idle_time
  timed_out? start_time, max_time
end

#make_file(filepath, content, options = {}) ⇒ Object

Write a file. Compatibility method with RemoteShell.



196
197
198
# File 'lib/sunshine/shell.rb', line 196

def make_file filepath, content, options={}
  File.open(filepath, "w+"){|f| f.write(content)}
end

#os_nameObject

Get the name of the OS



204
205
206
# File 'lib/sunshine/shell.rb', line 204

def os_name
  @os_name ||= call("uname -s").strip.downcase
end

#prompt_for_passwordObject

Prompt the user for a password



212
213
214
215
216
217
# File 'lib/sunshine/shell.rb', line 212

def prompt_for_password
  host_info = [@user, @host].compact.join("@")
  @password = ask("#{host_info} Password:") do |q|
    q.echo = false
  end
end

#quote_cmd(cmd) ⇒ Object

Wrap command in quotes and escape as needed.



235
236
237
238
# File 'lib/sunshine/shell.rb', line 235

def quote_cmd cmd
  cmd = [*cmd].join(" ")
  "'#{cmd.gsub(/'/){|s| "'\\''"}}'"
end

#sh_cmd(cmd) ⇒ Object

Build an sh -c command



244
245
246
# File 'lib/sunshine/shell.rb', line 244

def sh_cmd cmd
  ["sh", "-c", quote_cmd(cmd)]
end

#sudo_cmd(cmd, sudo_val = nil) ⇒ Object

Build a command with sudo. If sudo_val is nil, it is considered to mean “pass-through” and the default shell sudo will be used. If sudo_val is false, the cmd will be returned unchanged. If sudo_val is true, the returned command will be prefaced with sudo -H If sudo_val is a String, the command will be prefaced with sudo -H -u string_value



259
260
261
262
263
264
265
266
267
268
269
270
271
# File 'lib/sunshine/shell.rb', line 259

def sudo_cmd cmd, sudo_val=nil
  sudo_val = sudo_val[:sudo] if Hash === sudo_val
  sudo_val = @sudo if sudo_val.nil?

  case sudo_val
  when true
    ["sudo", "-H", cmd].flatten
  when String
    ["sudo", "-H", "-u", sudo_val, cmd].flatten
  else
    cmd
  end
end

Force symlinking a directory.



277
278
279
# File 'lib/sunshine/shell.rb', line 277

def symlink target, symlink_name
  call "ln -sfT #{target} #{symlink_name}" rescue false
end

#syncObject

Synchronize a block with the current mutex if it exists.



285
286
287
288
289
290
291
# File 'lib/sunshine/shell.rb', line 285

def sync
  if @mutex
    @mutex.synchronize{ yield }
  else
    yield
  end
end

#system(cmd, options = nil) ⇒ Object

Returns true if command was run successfully, otherwise returns false.



297
298
299
# File 'lib/sunshine/shell.rb', line 297

def system cmd, options=nil
  call(cmd, options) && true rescue false
end

#timed_out?(start_time = @cmd_activity, max_time = @timeout) ⇒ Boolean

Checks if timeout occurred.

Returns:

  • (Boolean)


156
157
158
159
# File 'lib/sunshine/shell.rb', line 156

def timed_out? start_time=@cmd_activity, max_time=@timeout
  return unless max_time
  Time.now.to_f - start_time.to_f > max_time
end

#tty!(cmd = nil) ⇒ Object

Start an interactive shell with preset permissions and env. Optionally pass a command to be run first.



182
183
184
185
186
187
188
189
190
# File 'lib/sunshine/shell.rb', line 182

def tty! cmd=nil
  sync do
    cmd = [cmd, "sh -il"].compact.join " && "
    pid = fork do
      exec sudo_cmd(env_cmd(cmd)).to_a.join(" ")
    end
    Process.waitpid pid
  end
end

#update_activityObject

Update the time of the last command activity



165
166
167
# File 'lib/sunshine/shell.rb', line 165

def update_activity
  @cmd_activity = Time.now
end

#with_mutex(mutex) ⇒ Object

Execute a block while setting the shell’s mutex. Sets the mutex to its original value on exit. Executing commands with a mutex is used for user prompts.



307
308
309
310
311
# File 'lib/sunshine/shell.rb', line 307

def with_mutex mutex
  old_mutex, @mutex = @mutex, mutex
  yield
  @mutex = old_mutex
end

#with_sessionObject

Runs the passed block within a connection session. If the shell is already connected, connecting and disconnecting is ignored; otherwise, the session method will ensure that the shell’s connection gets closed after the block has been executed.



321
322
323
324
325
326
327
328
# File 'lib/sunshine/shell.rb', line 321

def with_session
  prev_connection = connected?
  connect unless prev_connection

  yield

  disconnect unless prev_connection
end

#write(str) ⇒ Object Also known as: <<

Write string to stdout (by default).



334
335
336
# File 'lib/sunshine/shell.rb', line 334

def write str
  @output.write str
end