crash-watch 1.1.12 → 1.2.0

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