crash-watch 1.1.12 → 1.2.0

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.
@@ -0,0 +1,178 @@
1
+ # encoding: binary
2
+ #
3
+ # Copyright (c) 2016 Phusion Holding B.V.
4
+ #
5
+ # Permission is hereby granted, free of charge, to any person obtaining
6
+ # a copy of this software and associated documentation files (the
7
+ # "Software"), to deal in the Software without restriction, including
8
+ # without limitation the rights to use, copy, modify, merge, publish,
9
+ # distribute, sublicense, and/or sell copies of the Software, and to
10
+ # permit persons to whom the Software is furnished to do so, subject to
11
+ # the following conditions:
12
+ #
13
+ # The above copyright notice and this permission notice shall be
14
+ # included in all copies or substantial portions of the Software.
15
+ #
16
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
23
+
24
+ require 'rbconfig'
25
+ require 'crash_watch/base'
26
+ require 'crash_watch/utils'
27
+
28
+ module CrashWatch
29
+ class LldbNotFound < Error
30
+ end
31
+
32
+ class LldbController
33
+ class ExitInfo
34
+ attr_reader :exit_code, :signal, :backtrace, :snapshot
35
+
36
+ def initialize(exit_code, signal, backtrace, snapshot)
37
+ @exit_code = exit_code
38
+ @signal = signal
39
+ @backtrace = backtrace
40
+ @snapshot = snapshot
41
+ end
42
+
43
+ def signaled?
44
+ !!@signal
45
+ end
46
+ end
47
+
48
+ END_OF_RESPONSE_MARKER = '--------END_OF_RESPONSE--------'
49
+
50
+ attr_accessor :debug
51
+
52
+ def initialize
53
+ @pid, @in, @out = Utils.popen_command(find_lldb, "-x")
54
+ execute("settings set prompt 'LLDB:'")
55
+ execute("settings set auto-confirm false")
56
+ end
57
+
58
+ def execute(command_string, timeout = nil)
59
+ raise "LLDB session is already closed" if !@pid
60
+ puts "lldb write #{command_string.inspect}" if @debug
61
+ marker = "\n#{END_OF_RESPONSE_MARKER}\n"
62
+ @in.puts(command_string)
63
+ @in.puts("script print #{marker.inspect}")
64
+ done = false
65
+ result = []
66
+ while !done
67
+ begin
68
+ if select([@out], nil, nil, timeout)
69
+ line = @out.readline.chomp
70
+ line.sub!(/^LLDB:/, '')
71
+ puts "lldb read #{line.inspect}" if @debug
72
+ if line == "#{END_OF_RESPONSE_MARKER}"
73
+ done = true
74
+ else
75
+ result << line
76
+ end
77
+ else
78
+ close!
79
+ done = true
80
+ result = nil
81
+ end
82
+ rescue EOFError
83
+ done = true
84
+ end
85
+ end
86
+
87
+ if result
88
+ # Remove echo of the command string
89
+ result.slice!(0, 2)
90
+ # Remove echo of the marker print command
91
+ result.pop
92
+ result.pop
93
+
94
+ result.join("\n") << "\n"
95
+ else
96
+ nil
97
+ end
98
+ end
99
+
100
+ def closed?
101
+ !@pid
102
+ end
103
+
104
+ def close
105
+ if !closed?
106
+ begin
107
+ execute("process detach", 5) if !closed?
108
+ execute("quit", 5) if !closed?
109
+ rescue Errno::EPIPE
110
+ end
111
+ if !closed?
112
+ @in.close
113
+ @out.close
114
+ Process.waitpid(@pid)
115
+ @pid = nil
116
+ end
117
+ end
118
+ end
119
+
120
+ def close!
121
+ if !closed?
122
+ @in.close
123
+ @out.close
124
+ Process.kill('KILL', @pid)
125
+ Process.waitpid(@pid)
126
+ @pid = nil
127
+ end
128
+ end
129
+
130
+ def attach(pid)
131
+ pid = pid.to_s.strip
132
+ raise ArgumentError if pid.empty?
133
+ result = execute("attach -p #{pid}")
134
+ result !~ /(unable to attach|cannot attach)/
135
+ end
136
+
137
+ def program_counter
138
+ execute("p/x $pc").gsub(/.* = /, '')
139
+ end
140
+
141
+ def current_thread
142
+ execute("thread info") =~ /^thread #(.+?): /
143
+ $1
144
+ end
145
+
146
+ def current_thread_backtrace
147
+ execute("bt").strip
148
+ end
149
+
150
+ def all_threads_backtraces
151
+ execute("bt all").strip
152
+ end
153
+
154
+ private
155
+ def find_lldb
156
+ result = nil
157
+ if ENV['LLDB'] && File.executable?(ENV['LLDB'])
158
+ result = ENV['LLDB']
159
+ else
160
+ ENV['PATH'].to_s.split(/:+/).each do |path|
161
+ filename = "#{path}/lldb"
162
+ if File.file?(filename) && File.executable?(filename)
163
+ result = filename
164
+ break
165
+ end
166
+ end
167
+ end
168
+
169
+ puts "Found lldb at: #{result}" if result
170
+
171
+ if result.nil?
172
+ raise LldbNotFound
173
+ else
174
+ result
175
+ end
176
+ end
177
+ end
178
+ end
@@ -1,15 +1,34 @@
1
+ # Copyright (c) 2010-2016 Phusion Holding B.V.
2
+ #
3
+ # Permission is hereby granted, free of charge, to any person obtaining
4
+ # a copy of this software and associated documentation files (the
5
+ # "Software"), to deal in the Software without restriction, including
6
+ # without limitation the rights to use, copy, modify, merge, publish,
7
+ # distribute, sublicense, and/or sell copies of the Software, and to
8
+ # permit persons to whom the Software is furnished to do so, subject to
9
+ # the following conditions:
10
+ #
11
+ # The above copyright notice and this permission notice shall be
12
+ # included in all copies or substantial portions of the Software.
13
+ #
14
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21
+
1
22
  CRASH_WATCH_FILES = [
2
- "README.markdown",
3
- "LICENSE.txt",
4
- "Rakefile",
5
- "crash-watch.gemspec",
6
- "bin/**/*",
7
- "lib/**/*",
8
- "test/**/*"
23
+ "README.markdown",
24
+ "LICENSE.txt",
25
+ "Rakefile",
26
+ "crash-watch.gemspec",
27
+ "bin/**/*",
28
+ "lib/**/*",
29
+ "test/**/*"
9
30
  ]
10
31
 
11
32
  CRASH_WATCH_EXCLUDE_FILES = [
12
- "Rakefile",
13
- "debian.template/**/*",
14
- "rpm/**/*"
33
+ "Rakefile"
15
34
  ]
@@ -0,0 +1,94 @@
1
+ # encoding: binary
2
+ #
3
+ # Copyright (c) 2016 Phusion Holding B.V.
4
+ #
5
+ # Permission is hereby granted, free of charge, to any person obtaining
6
+ # a copy of this software and associated documentation files (the
7
+ # "Software"), to deal in the Software without restriction, including
8
+ # without limitation the rights to use, copy, modify, merge, publish,
9
+ # distribute, sublicense, and/or sell copies of the Software, and to
10
+ # permit persons to whom the Software is furnished to do so, subject to
11
+ # the following conditions:
12
+ #
13
+ # The above copyright notice and this permission notice shall be
14
+ # included in all copies or substantial portions of the Software.
15
+ #
16
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
23
+
24
+ module CrashWatch
25
+ module Utils
26
+ extend Utils
27
+
28
+ def self.included(klass)
29
+ # When included into another class, make sure that Utils
30
+ # methods are made private.
31
+ public_instance_methods(false).each do |method_name|
32
+ klass.send(:private, method_name)
33
+ end
34
+ end
35
+
36
+ def gdb_installed?
37
+ command_installed?('gdb')
38
+ end
39
+
40
+ def lldb_installed?
41
+ command_installed?('lldb')
42
+ end
43
+
44
+ def command_installed?(command)
45
+ path = ENV['PATH'].to_s
46
+ path.split(File::PATH_SEPARATOR).each do |dir|
47
+ next if dir.empty?
48
+ filename = "#{dir}/#{command}"
49
+ if File.file?(filename) && File.executable?(filename)
50
+ return true
51
+ end
52
+ end
53
+ false
54
+ end
55
+
56
+ def popen_command(*command)
57
+ a, b = IO.pipe
58
+ c, d = IO.pipe
59
+ if Process.respond_to?(:spawn)
60
+ args = command.dup
61
+ args << {
62
+ STDIN => a,
63
+ STDOUT => d,
64
+ STDERR => d,
65
+ :close_others => true
66
+ }
67
+ pid = Process.spawn(*args)
68
+ else
69
+ pid = fork do
70
+ STDIN.reopen(a)
71
+ STDOUT.reopen(d)
72
+ STDERR.reopen(d)
73
+ b.close
74
+ c.close
75
+ exec(*command)
76
+ end
77
+ end
78
+ a.close
79
+ d.close
80
+ b.binmode
81
+ c.binmode
82
+ [pid, b, c]
83
+ end
84
+
85
+ def find_signal_name(signo)
86
+ Signal.list.each_pair do |name, number|
87
+ if number == signo
88
+ return name
89
+ end
90
+ end
91
+ nil
92
+ end
93
+ end
94
+ end
@@ -1,3 +1,24 @@
1
+ # Copyright (c) 2010-2016 Phusion Holding B.V.
2
+ #
3
+ # Permission is hereby granted, free of charge, to any person obtaining
4
+ # a copy of this software and associated documentation files (the
5
+ # "Software"), to deal in the Software without restriction, including
6
+ # without limitation the rights to use, copy, modify, merge, publish,
7
+ # distribute, sublicense, and/or sell copies of the Software, and to
8
+ # permit persons to whom the Software is furnished to do so, subject to
9
+ # the following conditions:
10
+ #
11
+ # The above copyright notice and this permission notice shall be
12
+ # included in all copies or substantial portions of the Software.
13
+ #
14
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21
+
1
22
  module CrashWatch
2
- VERSION_STRING = '1.1.12'
23
+ VERSION_STRING = '1.2.0'
3
24
  end
@@ -0,0 +1,34 @@
1
+ require 'shellwords'
2
+
3
+ shared_examples_for 'a CrashWatch controller' do
4
+ def run_script_and_wait(code, snapshot_callback = nil, &block)
5
+ @process = IO.popen(%Q{exec ruby -e #{Shellwords.escape code}}, 'w')
6
+ @gdb.attach(@process.pid)
7
+ thread = Thread.new do
8
+ sleep 0.1
9
+ if block
10
+ block.call
11
+ end
12
+ @process.write("\n")
13
+ end
14
+ exit_info = @gdb.wait_until_exit(&snapshot_callback)
15
+ thread.join
16
+ exit_info
17
+ end
18
+
19
+ describe "#attach" do
20
+ before :each do
21
+ @process = IO.popen("exec ruby -e 'sleep 9999'", "w")
22
+ end
23
+
24
+ it "returns true if attaching worked" do
25
+ expect(@gdb.attach(@process.pid)).to be_truthy
26
+ end
27
+
28
+ it "returns false if the PID doesn't exist" do
29
+ Process.kill('KILL', @process.pid)
30
+ sleep 0.25
31
+ expect(@gdb.attach(@process.pid)).to be_falsey
32
+ end
33
+ end
34
+ end
@@ -1,116 +1,89 @@
1
1
  source_root = File.expand_path(File.dirname(__FILE__) + "/..")
2
- $LOAD_PATH.unshift("#{source_root}/lib")
3
- Thread.abort_on_exception = true
4
-
2
+ require "#{source_root}/test/spec_helper"
3
+ require "#{source_root}/test/controller_sharedspec"
5
4
  require 'crash_watch/gdb_controller'
5
+ require 'crash_watch/utils'
6
+
7
+ if CrashWatch::Utils.gdb_installed?
8
+ describe CrashWatch::GdbController do
9
+ before :each do
10
+ @gdb = CrashWatch::GdbController.new
11
+ end
12
+
13
+ after :each do
14
+ @gdb.close
15
+ if @process
16
+ Process.kill('KILL', @process.pid)
17
+ @process.close
18
+ end
19
+ end
20
+
21
+ include_examples 'a CrashWatch controller'
22
+
23
+ describe "#execute" do
24
+ it "executes the desired command and returns its output" do
25
+ expect(@gdb.execute("echo hello world")).to eq("hello world\n")
26
+ end
27
+ end
28
+
29
+ describe "#wait_until_exit" do
30
+ it "returns the expected information if the process exited normally" do
31
+ exit_info = run_script_and_wait('STDIN.readline')
32
+ expect(exit_info.exit_code).to eq(0)
33
+ expect(exit_info).not_to be_signaled
34
+ end
35
+
36
+ it "returns the expected information if the process exited with a non-zero exit code" do
37
+ exit_info = run_script_and_wait('STDIN.readline; exit 3')
38
+ expect(exit_info.exit_code).to eq(3)
39
+ expect(exit_info).not_to be_signaled
40
+ expect(exit_info.backtrace).not_to be_nil
41
+ expect(exit_info.backtrace).not_to be_empty
42
+ end
43
+
44
+ it "returns the expected information if the process exited because of a signal" do
45
+ exit_info = run_script_and_wait(
46
+ 'STDIN.readline;' +
47
+ 'require "rubygems";' +
48
+ 'require "ffi";' +
49
+ 'module MyLib;' +
50
+ 'extend FFI::Library;' +
51
+ 'ffi_lib "c";' +
52
+ 'attach_function :abort, [], :void;' +
53
+ 'end;' +
54
+ 'MyLib.abort')
55
+ expect(exit_info).to be_signaled
56
+ expect(exit_info.backtrace).to match(/abort/)
57
+ end
58
+
59
+ it "ignores non-fatal signals" do
60
+ exit_info = run_script_and_wait('trap("INT") { }; STDIN.readline; exit 2') do
61
+ Process.kill('INT', @process.pid)
62
+ end
63
+ expect(exit_info.exit_code).to eq(2)
64
+ expect(exit_info).not_to be_signaled
65
+ expect(exit_info.backtrace).not_to be_nil
66
+ expect(exit_info.backtrace).not_to be_empty
67
+ end
6
68
 
7
- describe CrashWatch::GdbController do
8
- before :each do
9
- @gdb = CrashWatch::GdbController.new
10
- end
11
-
12
- after :each do
13
- @gdb.close
14
- if @process
15
- Process.kill('KILL', @process.pid)
16
- @process.close
17
- end
18
- end
19
-
20
- def run_script_and_wait(code, snapshot_callback = nil, &block)
21
- @process = IO.popen(%Q{ruby -e '#{code}'}, 'w')
22
- @gdb.attach(@process.pid)
23
- thread = Thread.new do
24
- sleep 0.1
25
- if block
26
- block.call
27
- end
28
- @process.write("\n")
29
- end
30
- exit_info = @gdb.wait_until_exit(&snapshot_callback)
31
- thread.join
32
- return exit_info
33
- end
34
-
35
- describe "#execute" do
36
- it "executes the desired command and returns its output" do
37
- @gdb.execute("echo hello world").should == "hello world\n"
38
- end
39
- end
40
-
41
- describe "#attach" do
42
- before :each do
43
- @process = IO.popen("sleep 9999", "w")
44
- end
45
-
46
- it "returns true if attaching worked" do
47
- @gdb.attach(@process.pid).should be_true
48
- end
49
-
50
- it "returns false if the PID doesn't exist" do
51
- Process.kill('KILL', @process.pid)
52
- sleep 0.25
53
- @gdb.attach(@process.pid).should be_false
54
- end
55
- end
56
-
57
- describe "#wait_until_exit" do
58
- it "returns the expected information if the process exited normally" do
59
- exit_info = run_script_and_wait('STDIN.readline')
60
- exit_info.exit_code.should == 0
61
- exit_info.should_not be_signaled
62
- end
63
-
64
- it "returns the expected information if the process exited with a non-zero exit code" do
65
- exit_info = run_script_and_wait('STDIN.readline; exit 3')
66
- exit_info.exit_code.should == 3
67
- exit_info.should_not be_signaled
68
- exit_info.backtrace.should_not be_nil
69
- exit_info.backtrace.should_not be_empty
70
- end
71
-
72
- it "returns the expected information if the process exited because of a signal" do
73
- exit_info = run_script_and_wait(
74
- 'STDIN.readline;' +
75
- 'require "rubygems";' +
76
- 'require "ffi";' +
77
- 'module MyLib;' +
78
- 'extend FFI::Library;' +
79
- 'ffi_lib "c";' +
80
- 'attach_function :abort, [], :void;' +
81
- 'end;' +
82
- 'MyLib.abort')
83
- exit_info.should be_signaled
84
- exit_info.backtrace.should =~ /abort/
85
- end
86
-
87
- it "ignores non-fatal signals" do
88
- exit_info = run_script_and_wait('trap("INT") { }; STDIN.readline; exit 2') do
89
- Process.kill('INT', @process.pid)
90
- end
91
- exit_info.exit_code.should == 2
92
- exit_info.should_not be_signaled
93
- exit_info.backtrace.should_not be_nil
94
- exit_info.backtrace.should_not be_empty
95
- end
96
-
97
- it "returns information of the signal that aborted the process, not information of ignored signals" do
98
- exit_info = run_script_and_wait(
99
- 'trap("INT") { };' +
100
- 'STDIN.readline;' +
101
- 'require "rubygems";' +
102
- 'require "ffi";' +
103
- 'module MyLib;' +
104
- 'extend FFI::Library;' +
105
- 'ffi_lib "c";' +
106
- 'attach_function :abort, [], :void;' +
107
- 'end;' +
108
- 'MyLib.abort'
109
- ) do
110
- Process.kill('INT', @process.pid)
111
- end
112
- exit_info.should be_signaled
113
- exit_info.backtrace.should =~ /abort/
114
- end
115
- end
116
- end
69
+ it "returns information of the signal that aborted the process, not information of ignored signals" do
70
+ exit_info = run_script_and_wait(
71
+ 'trap("INT") { };' +
72
+ 'STDIN.readline;' +
73
+ 'require "rubygems";' +
74
+ 'require "ffi";' +
75
+ 'module MyLib;' +
76
+ 'extend FFI::Library;' +
77
+ 'ffi_lib "c";' +
78
+ 'attach_function :abort, [], :void;' +
79
+ 'end;' +
80
+ 'MyLib.abort'
81
+ ) do
82
+ Process.kill('INT', @process.pid)
83
+ end
84
+ expect(exit_info).to be_signaled
85
+ expect(exit_info.backtrace).to match(/abort/)
86
+ end
87
+ end
88
+ end
89
+ end