caliph 0.1.1 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -9,29 +9,30 @@ module Caliph
9
9
  attr_accessor :output_stream
10
10
  end
11
11
 
12
- def initialize(executable, *options)
12
+ def initialize(executable = nil, *options)
13
13
  @output_stream = self.class.output_stream || $stderr
14
- @executable = executable.to_s
14
+ @executable = executable.to_s unless executable.nil?
15
15
  @options = options
16
16
  @redirections = []
17
17
  @env = {}
18
+ @verbose = false
18
19
  yield self if block_given?
19
20
  end
20
21
 
21
- attr_accessor :name, :executable, :options, :env, :output_stream
22
+ attr_accessor :name, :executable, :options, :env, :output_stream, :verbose
22
23
  attr_reader :redirections
23
24
 
24
25
  alias_method :command_environment, :env
25
26
 
27
+ def valid?
28
+ !@executable.nil?
29
+ end
30
+
26
31
  def set_env(name, value)
27
32
  command_environment[name] = value
28
33
  return self
29
34
  end
30
35
 
31
- def verbose
32
- #::Rake.verbose && ::Rake.verbose != ::Rake::FileUtilsExt::DEFAULT
33
- end
34
-
35
36
  def name
36
37
  @name || executable
37
38
  end
@@ -58,6 +59,8 @@ module Caliph
58
59
  self
59
60
  end
60
61
 
62
+ # Waits for the process to complete. If this takes longer that
63
+ # {consume_timeout},
61
64
  def redirect_from(path, stream)
62
65
  @redirections << "#{stream}<#{path}"
63
66
  end
@@ -82,104 +85,44 @@ module Caliph
82
85
  redirect_stdout(path).redirect_stderr(path)
83
86
  end
84
87
 
85
- # Run the command, wait for termination, and collect the results.
86
- # Returns an instance of CommandRunResult that contains the output
87
- # and exit code of the command.
88
- #
89
- # This version adds some information to STDOUT to document that the
90
- # command is running. For a terser version, call #execute directly
88
+ #:nocov:
89
+ #@deprecated (see: Shell)
91
90
  def run
92
- report string_format + " ", false
93
- result = execute
94
- report "=> #{result.exit_code}"
95
- report result.format_streams if verbose
96
- return result
97
- ensure
98
- report "" if verbose
91
+ Caliph.new.run(self)
99
92
  end
100
93
 
101
- # Fork a new process for the command, then terminate our process.
94
+ #@deprecated (see: Shell)
102
95
  def run_as_replacement
103
- output_stream.puts "Ceding execution to: "
104
- output_stream.puts string_format
105
- Process.exec(command_environment, command)
96
+ Caliph.new.run_as_replacement(self)
106
97
  end
107
98
  alias replace_us run_as_replacement
108
99
 
109
- # Run the command in the background. The command can survive the caller.
100
+ #@deprecated (see: Shell)
110
101
  def run_detached
111
- pid, out, err = spawn_process
112
- Process.detach(pid)
113
- return pid, out, err
102
+ Caliph.new.run_detached(self)
114
103
  end
115
104
  alias spin_off run_detached
116
105
 
117
- # Run the command, wait for termination, and collect the results.
118
- # Returns an instance of CommandRunResult that contains the output
119
- # and exit code of the command.
120
- #
106
+ #@deprecated (see: Shell)
121
107
  def execute
122
- collect_result(*spawn_process)
108
+ Caliph.new.execute(self)
123
109
  end
124
110
 
125
- # Run the command in parallel with the parent process - will kill it if it
126
- # outlasts us
111
+ #@deprecated (see: Shell)
127
112
  def run_in_background
128
- pid, out, err = spawn_process
129
- Process.detach(pid)
130
- at_exit do
131
- kill_process(pid)
132
- end
133
- return pid, out, err
113
+ Caliph.new.run_in_background(self)
134
114
  end
135
115
  alias background run_in_background
136
116
 
137
- # Given a process ID for a running command and a pair of stdout/stdin
138
- # streams, records the results of the command and returns them in a
139
- # CommandRunResult instance.
140
- def collect_result(pid, host_stdout, host_stderr)
141
- result = CommandRunResult.new(pid, self)
142
- result.streams = {1 => host_stdout, 2 => host_stderr}
143
- result.wait
144
- return result
145
- end
146
-
147
-
148
- def kill_process(pid)
149
- Process.kill("INT", pid)
150
- end
151
-
152
- def complete(pid, out, err)
153
- kill_process(pid)
154
- collect_result(pid, out, err)
155
- end
156
-
157
- def report(message, newline=true)
158
- output_stream.print(message + (newline ? "\n" : ""))
159
- end
160
-
161
-
117
+ #@deprecated
162
118
  def succeeds?
163
119
  run.succeeded?
164
120
  end
165
121
 
122
+ #@deprecated
166
123
  def must_succeed!
167
124
  run.must_succeed!
168
125
  end
169
-
170
- def spawn_process
171
- host_stdout, cmd_stdout = IO.pipe
172
- host_stderr, cmd_stderr = IO.pipe
173
-
174
- pid = Process.spawn(command_environment, command, :out => cmd_stdout, :err => cmd_stderr)
175
- cmd_stdout.close
176
- cmd_stderr.close
177
-
178
- return pid, host_stdout, host_stderr
179
- end
180
-
181
-
126
+ #:nocov:
182
127
  end
183
-
184
-
185
128
  end
@@ -1,30 +1,55 @@
1
1
  module Caliph
2
+ # This is the Caliph handle on a run process - it can be used to send signals
3
+ # to running processes, wait for them to complete, get their exit status once
4
+ # they have, and watch their streams in either case.
2
5
  class CommandRunResult
3
- def initialize(pid, command)
6
+ def initialize(pid, command, shell)
4
7
  @command = command
5
8
  @pid = pid
9
+ @shell = shell
6
10
 
7
11
  #####
8
12
  @process_status = nil
9
13
  @streams = {}
10
14
  @consume_timeout = nil
11
15
  end
12
- attr_reader :process_status, :pid
16
+
17
+ attr_reader :process_status
18
+
19
+ attr_reader :pid, :command
13
20
  attr_accessor :consume_timeout, :streams
14
21
 
22
+ # Access the stdout of the process
15
23
  def stdout
16
24
  @streams[1]
17
25
  end
18
26
 
27
+ # Access the stderr of the process
19
28
  def stderr
20
29
  @streams[2]
21
30
  end
22
31
 
32
+ # @return [exit_code] the raw exit of the process
33
+ # @return [nil] the process is still running
23
34
  def exit_code
24
- @process_status.exitstatus
35
+ if @process_status.nil?
36
+ return nil
37
+ else
38
+ @process_status.exitstatus
39
+ end
25
40
  end
26
41
  alias exit_status exit_code
27
42
 
43
+ # Check whether the process is still running
44
+ # @return [true] the process is still running
45
+ # @return [false] the process has completed
46
+ def running?
47
+ !@process_status.nil?
48
+ end
49
+
50
+ # Confirm that the process exited with a successful exit code (i.e. 0).
51
+ # This is pretty reliable, but many applications return bad exit statuses -
52
+ # 0 when they failed, usually.
28
53
  def succeeded?
29
54
  must_succeed!
30
55
  return true
@@ -32,11 +57,14 @@ module Caliph
32
57
  return false
33
58
  end
34
59
 
60
+ # Nicely formatted output of stdout and stderr - won't be intermixed, which
61
+ # may be different than what you'd see live in the shell
35
62
  def format_streams
36
63
  "stdout:#{stdout.nil? || stdout.empty? ? "[empty]\n" : "\n#{stdout}"}" +
37
64
  "stderr:#{stderr.nil? || stderr.empty? ? "[empty]\n" : "\n#{stderr}"}---"
38
65
  end
39
66
 
67
+ # Demands that the process succeed, or else raises and error
40
68
  def must_succeed!
41
69
  case exit_code
42
70
  when 0
@@ -46,6 +74,19 @@ module Caliph
46
74
  end
47
75
  end
48
76
 
77
+ # Stop a running process. Sends SIGINT by default which about like hitting
78
+ # Control-C.
79
+ # @param signal the Unix signal to send to the process
80
+ def kill(signal = nil)
81
+ Process.kill(signal || "INT", pid)
82
+ rescue Errno::ESRCH
83
+ warn "Couldn't find process #{pid} to kill it"
84
+ end
85
+
86
+ # Waits for the process to complete. If this takes longer that
87
+ # {consume_timeout}, output on the process's streams will be output via
88
+ # {Shell#report} - very useful when compilation or network transfers are
89
+ # taking a long time.
49
90
  def wait
50
91
  @accumulators = {}
51
92
  waits = {}
@@ -73,9 +114,9 @@ module Caliph
73
114
  if !@buffered_echo.nil?
74
115
  timeout = begin_echoing - Time.now
75
116
  if timeout < 0
76
- @command.report ""
77
- @command.report "Long running command output:"
78
- @command.report @buffered_echo.join
117
+ @shell.report ""
118
+ @shell.report "Long running command output:"
119
+ @shell.report @buffered_echo.join
79
120
  @buffered_echo = nil
80
121
  end
81
122
  end
@@ -112,7 +153,7 @@ module Caliph
112
153
  begin
113
154
  while chunk = io.read_nonblock(4096)
114
155
  if @buffered_echo.nil?
115
- @command.report chunk, false
156
+ @shell.report chunk, false
116
157
  else
117
158
  @buffered_echo << chunk
118
159
  end
@@ -0,0 +1,172 @@
1
+ module Caliph
2
+ class Error < StandardError; end
3
+ class IncompleteCommand < Error; end
4
+ class InvalidCommand < Error; end
5
+
6
+ # Operates something like a command line shell, except from a Ruby object
7
+ # perspective.
8
+ #
9
+ # Basically, a Shell operates as your handle on creating, running and killing
10
+ # commands in Caliph.
11
+ #
12
+ class Shell
13
+ attr_accessor :verbose, :output_stream
14
+
15
+ def output_stream
16
+ @output_stream ||= $stderr
17
+ end
18
+
19
+ def verbose
20
+ @verbose ||= false
21
+ end
22
+
23
+ # Reports messages if verbose is true. Used internally to print messages
24
+ # about running commands
25
+ def report_verbose(message)
26
+ report(message) if verbose
27
+ end
28
+
29
+ # Prints information to {output_stream} which defaults to $stderr.
30
+ def report(message, newline=true)
31
+ output_stream.print(message + (newline ? "\n" : ""))
32
+ end
33
+
34
+ # Kill processes given a raw pid. In general, prefer
35
+ # {CommandRunResult#kill}
36
+ # @param pid the process id to kill
37
+ def kill_process(pid)
38
+ Process.kill("INT", pid)
39
+ rescue Errno::ESRCH
40
+ warn "Couldn't find process #{pid} to kill it"
41
+ end
42
+
43
+ def normalize_command_line(*args, &block)
44
+ command_line = nil
45
+ if args.empty? or args.first == nil
46
+ command_line = CommandLine.new
47
+ elsif args.all?{|arg| arg.is_a? String}
48
+ command_line = CommandLine.new(*args)
49
+ else
50
+ command_line = args.first
51
+ end
52
+ yield command_line if block_given?
53
+ #raise InvalidCommand, "not a command line: #{command_line.inspect}"
54
+ #unless command_line.is_a? CommandLine
55
+ raise IncompleteCommand, "cannot run #{command_line}" unless command_line.valid?
56
+ command_line
57
+ end
58
+ protected :normalize_command_line
59
+
60
+ # Given a process ID for a running command and a pair of stdout/stdin
61
+ # streams, records the results of the command and returns them in a
62
+ # CommandRunResult instance.
63
+ def collect_result(command, pid, host_stdout, host_stderr)
64
+ result = CommandRunResult.new(pid, command, self)
65
+ result.streams = {1 => host_stdout, 2 => host_stderr}
66
+ return result
67
+ end
68
+
69
+ # Creates a process to run a command. Handles connecting pipes to stardard
70
+ # streams, launching the process and returning a pid for it.
71
+ # @return [pid, host_stdout, host_stderr] the process id and streams
72
+ # associated with the child process
73
+ def spawn_process(command_line)
74
+ host_stdout, cmd_stdout = IO.pipe
75
+ host_stderr, cmd_stderr = IO.pipe
76
+
77
+ pid = Process.spawn(command_line.command_environment, command_line.command, :out => cmd_stdout, :err => cmd_stderr)
78
+ cmd_stdout.close
79
+ cmd_stderr.close
80
+
81
+ return pid, host_stdout, host_stderr
82
+ end
83
+
84
+ # Run the command, wait for termination, and collect the results.
85
+ # Returns an instance of CommandRunResult that contains the output
86
+ # and exit code of the command.
87
+ #
88
+ def execute(command_line)
89
+ result = collect_result(command_line, *spawn_process(command_line))
90
+ result.wait
91
+ result
92
+ end
93
+
94
+ # @!group Running Commands
95
+ # Run the command, wait for termination, and collect the results.
96
+ # Returns an instance of CommandRunResult that contains the output
97
+ # and exit code of the command. This version {#report}s some information to document that the
98
+ # command is running. For a terser version, call {#execute} directly
99
+ #
100
+ # @!macro normalized
101
+ # @yield [CommandLine] command about to be run (for configuration)
102
+ # @return [CommandRunResult] used to refer to and inspect the resulting
103
+ # process
104
+ # @overload $0(&block)
105
+ # @overload $0(cmd, &block)
106
+ # @param [CommandLine] execute
107
+ # @overload $0(*cmd_strings, &block)
108
+ # @param [Array<String>] a set of strings to parse into a {CommandLine}
109
+ def run(*args, &block)
110
+ command_line = normalize_command_line(*args, &block)
111
+
112
+ report command_line.string_format + " ", false
113
+ result = execute(command_line)
114
+ report "=> #{result.exit_code}"
115
+ report_verbose result.format_streams
116
+ return result
117
+ ensure
118
+ report_verbose ""
119
+ end
120
+
121
+ # Completely replace the running process with a command. Good for setting
122
+ # up a command and then running it, without worrying about what happens
123
+ # after that. Uses `exec` under the hood.
124
+ # @macro normalized
125
+ # @example Using replace_us
126
+ # # The last thing we'll ever do:
127
+ # shell.run_as_replacement('echo', "Everything is okay")
128
+ # # don't worry, we never get here.
129
+ # shell.run("sudo", "shutdown -h now")
130
+ def run_as_replacement(*args, &block)
131
+ command_line = normalize_command_line(*args, &block)
132
+
133
+ report "Ceding execution to: "
134
+ report command_line.string_format
135
+ Process.exec(command_line.command_environment, command_line.command)
136
+ end
137
+ alias replace_us run_as_replacement
138
+
139
+ # Run the command in the background. The command can survive the caller.
140
+ # Works, for instance, to kick off some long running processes that we
141
+ # don't care about. Note that this isn't quite full daemonization - we
142
+ # don't close the streams of the other process, or scrub its environment or
143
+ # anything.
144
+ # @macro normalized
145
+ def run_detached(*args, &block)
146
+ command_line = normalize_command_line(*args, &block)
147
+
148
+ pid, out, err = spawn_process(command_line)
149
+ Process.detach(pid)
150
+ return collect_result(command_line, pid, out, err)
151
+ end
152
+ alias spin_off run_detached
153
+
154
+ # Run the command in parallel with the parent process - will kill it if it
155
+ # outlasts us. Good for running e.g. a web server that we need to interact
156
+ # with, or the like, without cluttering the system with a bunch of zombies.
157
+ # @macro normalized
158
+ def run_in_background(*args, &block)
159
+ command_line = normalize_command_line(*args, &block)
160
+
161
+ pid, out, err = spawn_process(command_line)
162
+ Process.detach(pid)
163
+ at_exit do
164
+ kill_process(pid)
165
+ end
166
+ return collect_result(command_line, pid, out, err)
167
+ end
168
+ alias background run_in_background
169
+
170
+ # !@endgroup
171
+ end
172
+ end
data/lib/caliph.rb CHANGED
@@ -1,4 +1,13 @@
1
+ module Caliph
2
+ # Returns an instance of the default {Shell}
3
+ # @todo add alternative shells - e.g. SSHShell
4
+ def self.new
5
+ Shell.new
6
+ end
7
+ end
8
+
1
9
  require 'caliph/command-line'
2
10
  require 'caliph/command-chain'
3
11
  require 'caliph/command-line-dsl'
4
12
  require 'caliph/shell-escaped'
13
+ require 'caliph/shell'
@@ -9,11 +9,11 @@ describe Caliph::CommandLineDSL do
9
9
  end
10
10
 
11
11
  it "should define commands" do
12
- command.should be_an_instance_of(Caliph::WrappingChain)
13
- command.should have(2).commands
14
- command.commands[0].should be_an_instance_of(Caliph::CommandLine)
15
- command.commands[1].should be_an_instance_of(Caliph::CommandLine)
16
- command.command.should == "sudo -- gem install bundler"
12
+ expect(command).to be_an_instance_of(Caliph::WrappingChain)
13
+ expect(command.commands.size).to eq(2)
14
+ expect(command.commands[0]).to be_an_instance_of(Caliph::CommandLine)
15
+ expect(command.commands[1]).to be_an_instance_of(Caliph::CommandLine)
16
+ expect(command.command).to eq("sudo -- gem install bundler")
17
17
  end
18
18
  end
19
19
 
@@ -23,11 +23,11 @@ describe Caliph::CommandLineDSL do
23
23
  end
24
24
 
25
25
  it "should define commands" do
26
- command.should be_an_instance_of(Caliph::PipelineChain)
27
- command.should have(2).commands
28
- command.commands[0].should be_an_instance_of(Caliph::CommandLine)
29
- command.commands[1].should be_an_instance_of(Caliph::CommandLine)
30
- command.command.should == "cat /etc/passwd | grep root"
26
+ expect(command).to be_an_instance_of(Caliph::PipelineChain)
27
+ expect(command.commands.size).to eq(2)
28
+ expect(command.commands[0]).to be_an_instance_of(Caliph::CommandLine)
29
+ expect(command.commands[1]).to be_an_instance_of(Caliph::CommandLine)
30
+ expect(command.command).to eq("cat /etc/passwd | grep root")
31
31
  end
32
32
  end
33
33
 
@@ -38,11 +38,11 @@ describe Caliph::CommandLineDSL do
38
38
  end
39
39
 
40
40
  it "should define commands" do
41
- command.should be_an_instance_of(Caliph::PrereqChain)
42
- command.should have(2).commands
43
- command.commands[0].should be_an_instance_of(Caliph::CommandLine)
44
- command.commands[1].should be_an_instance_of(Caliph::CommandLine)
45
- command.command.should == "cd /tmp/trash && rm -rf *"
41
+ expect(command).to be_an_instance_of(Caliph::PrereqChain)
42
+ expect(command.commands.size).to eq(2)
43
+ expect(command.commands[0]).to be_an_instance_of(Caliph::CommandLine)
44
+ expect(command.commands[1]).to be_an_instance_of(Caliph::CommandLine)
45
+ expect(command.command).to eq("cd /tmp/trash && rm -rf *")
46
46
  end
47
47
  end
48
48
  end
data/spec/command-line.rb CHANGED
@@ -13,15 +13,15 @@ describe Caliph::CommandLine do
13
13
  end
14
14
 
15
15
  it "should have a name set" do
16
- commandline.name.should == "echo"
16
+ expect(commandline.name).to eq("echo")
17
17
  end
18
18
 
19
19
  it "should produce a command string" do
20
- commandline.command.should == "echo -n Some text"
20
+ expect(commandline.command).to eq("echo -n Some text")
21
21
  end
22
22
 
23
23
  it "should succeed" do
24
- commandline.succeeds?.should be_true
24
+ expect(commandline.succeeds?).to be_truthy
25
25
  end
26
26
 
27
27
  it "should not complain about success" do
@@ -44,11 +44,11 @@ describe Caliph::CommandLine, "setting environment variables" do
44
44
  end
45
45
 
46
46
  it "should succeed" do
47
- result.succeeded?.should be_true
47
+ expect(result.succeeded?).to be_truthy
48
48
  end
49
49
 
50
50
  it "should alter the command's environment variables" do
51
- result.stdout.should =~ /TEST_ENV.*indubitably/
51
+ expect(result.stdout).to match(/TEST_ENV.*indubitably/)
52
52
  end
53
53
 
54
54
  end
@@ -64,24 +64,24 @@ describe Caliph::CommandLine, 'redirecting' do
64
64
 
65
65
  it 'should allow redirect stdout' do
66
66
  commandline.redirect_stdout('some_file')
67
- result.should =~ /1>some_file$/
67
+ expect(result).to match(/1>some_file$/)
68
68
  end
69
69
 
70
70
  it 'should allow redirect stderr' do
71
71
  commandline.redirect_stderr('some_file')
72
- result.should =~ /2>some_file$/
72
+ expect(result).to match(/2>some_file$/)
73
73
  end
74
74
 
75
75
  it 'should allow chain redirects' do
76
76
  commandline.redirect_stdout('stdout_file').redirect_stderr('stderr_file')
77
- result.should =~ /\b1>stdout_file\b/
78
- result.should =~ /\b2>stderr_file\b/
77
+ expect(result).to match(/\b1>stdout_file\b/)
78
+ expect(result).to match(/\b2>stderr_file\b/)
79
79
  end
80
80
 
81
81
  it 'should redirect both' do
82
82
  commandline.redirect_both('output_file')
83
- result.should =~ /\b1>output_file\b/
84
- result.should =~ /\b2>output_file\b/
83
+ expect(result).to match(/\b1>output_file\b/)
84
+ expect(result).to match(/\b2>output_file\b/)
85
85
  end
86
86
  end
87
87
 
@@ -100,31 +100,29 @@ describe Caliph::PipelineChain do
100
100
  end
101
101
 
102
102
  it "should produce the right command" do
103
- commandline.command.should == 'env | cat'
103
+ expect(commandline.command).to eq('env | cat')
104
104
  end
105
105
 
106
106
  it "should produce a runnable command with format_string" do
107
- commandline.string_format.should == 'TEST_ENV=indubitably env | cat'
107
+ expect(commandline.string_format).to eq('TEST_ENV=indubitably env | cat')
108
108
  end
109
109
 
110
110
  it "should succeed" do
111
- result.succeeded?.should be_true
111
+ expect(result.succeeded?).to be_truthy
112
112
  end
113
113
 
114
114
  it "should alter the command's environment variables" do
115
- result.stdout.should =~ /TEST_ENV.*indubitably/
115
+ expect(result.stdout).to match(/TEST_ENV.*indubitably/)
116
116
  end
117
117
  end
118
118
 
119
-
120
-
121
119
  describe Caliph::CommandLine, "that fails" do
122
120
  let :commandline do
123
121
  Caliph::CommandLine.new("false")
124
122
  end
125
123
 
126
124
  it "should not succeed" do
127
- commandline.succeeds?.should == false
125
+ expect(commandline.succeeds?).to eq(false)
128
126
  end
129
127
 
130
128
  it "should raise error if succeed demanded" do
@@ -1,11 +1,17 @@
1
+ require "codeclimate-test-reporter"
2
+ CodeClimate::TestReporter.start
3
+
1
4
  require 'rspec'
2
5
  require 'rspec/core/formatters/base_formatter'
3
6
 
7
+ $:.unshift("./lib")
8
+
4
9
  require 'caliph'
5
- require 'cadre/rspec'
10
+ require 'cadre/rspec3'
6
11
 
7
12
  RSpec.configure do |config|
13
+ config.backtrace_inclusion_patterns = []
8
14
  config.run_all_when_everything_filtered = true
9
- config.add_formatter(Cadre::RSpec::NotifyOnCompleteFormatter)
10
- config.add_formatter(Cadre::RSpec::QuickfixFormatter)
15
+ config.add_formatter(Cadre::RSpec3::NotifyOnCompleteFormatter)
16
+ config.add_formatter(Cadre::RSpec3::QuickfixFormatter)
11
17
  end