caliph 0.1.1 → 0.1.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -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