Skip to content

Commit b3c056d

Browse files
authored
Merge pull request #17 from square/lazarus/isolated-eval
isolate eval call so it can't muck with local variables
2 parents 9e0bde4 + 244d54e commit b3c056d

10 files changed

Lines changed: 176 additions & 116 deletions

File tree

.github/workflows/ruby.yml

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ permissions:
1818

1919
jobs:
2020
test:
21-
2221
runs-on: ubuntu-latest
2322
strategy:
2423
matrix:
@@ -27,10 +26,7 @@ jobs:
2726
steps:
2827
- uses: actions/checkout@v4
2928
- name: Set up Ruby
30-
# To automatically get bug fixes and new Ruby versions for ruby/setup-ruby,
31-
# change this to (see https://github.com/ruby/setup-ruby#versioning):
32-
# uses: ruby/setup-ruby@v1
33-
uses: ruby/setup-ruby@55283cc23133118229fd3f97f9336ee23a179fcf # v1.146.0
29+
uses: ruby/setup-ruby@v1
3430
with:
3531
ruby-version: ${{ matrix.ruby-version }}
3632
bundler-cache: true # runs 'bundle install' and caches installed gems automatically

.rubocop.yml

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,25 @@
11
AllCops:
2-
TargetRubyVersion: 2.4
2+
TargetRubyVersion: 3.1
33
DisplayCopNames: true
4+
NewCops: enable
45

5-
Style/FrozenStringLiteralComment:
6-
Enabled: true
6+
plugins:
7+
- rubocop-rake
8+
- rubocop-rspec
79

10+
Layout/LineLength:
11+
Enabled: false
812
Metrics:
913
Enabled: false
10-
14+
RSpec/DescribedClass:
15+
Enabled: false
16+
RSpec/ExampleLength:
17+
Enabled: false
18+
RSpec/MultipleExpectations:
19+
Enabled: false
1120
Style/Documentation:
1221
Enabled: false
13-
22+
Style/FrozenStringLiteralComment:
23+
Enabled: true
1424
Style/StringLiterals:
1525
EnforcedStyle: double_quotes

.travis.yml

Lines changed: 0 additions & 11 deletions
This file was deleted.

CHANGES.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,14 @@
11
### 0.1.9 2025-04-03
22

3+
- [#17](https://github.com/square/debug_socket/pull/17)
4+
Refactor error handling. Previously all errors (including `eval` errors) were caught in the same `rescue Exception`.
5+
Now, we only `rescue Exception` for `eval` errors. For `DebugSocket` errors, we only `rescue StandardError` and we
6+
allow 20 consecutive errors before `DebugSocket` gives up and dies permanently.
7+
(@nerdrew)
8+
39
- [#16](https://github.com/square/debug_socket/pull/16)
410
Allow external auditing of debug sessions.
5-
(dogor@)
11+
(@doctown)
612

713
### 0.1.8 2022-10-10
814

Gemfile

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,13 @@ source "https://rubygems.org"
55
# Specify your gem's dependencies in debug_socket.gemspec
66
gemspec
77

8-
gem "pry"
8+
gem "base64"
9+
gem "ostruct"
10+
gem "pry-byebug"
911
gem "rake"
1012
gem "rspec", "~> 3.8"
11-
gem "rubocop", "0.59.1"
13+
gem "rubocop", "1.73.1"
14+
gem "rubocop-rake"
15+
gem "rubocop-rspec"
16+
gem "ruby-lsp", require: false
17+
gem "syntax_tree", require: false

debug_socket.gemspec

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,4 +17,7 @@ Gem::Specification.new do |spec|
1717
spec.bindir = "exe"
1818
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
1919
spec.require_paths = ["lib"]
20+
spec.metadata["rubygems_mfa_required"] = "true"
21+
22+
spec.required_ruby_version = ">= 3.1"
2023
end

exe/debug-socket

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,10 @@ end
1212
socket_path = ARGV[0]
1313
command = ARGV[1] || "backtrace"
1414

15-
warn "\nSending `#{command}` to the following socket: #{socket_path}"\
15+
warn(
16+
"\nSending `#{command}` to the following socket: #{socket_path}" \
1617
"----------------------------------------------------------\n\n"
18+
)
1719

1820
UNIXSocket.open(socket_path) do |socket|
1921
socket.write(command)

lib/debug_socket.rb

Lines changed: 46 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,30 @@
55
require "time"
66

77
module DebugSocket
8+
module Commands
9+
# When running `eval`, we don't want the input to overwrite local variables etc. `eval` runs in the current scope,
10+
# so we have an empty scope here that runs in a module that only has other shortcut commands the client might want
11+
# to run.
12+
def self.isolated_eval(input)
13+
eval(input) # rubocop:disable Security/Eval
14+
# We rescue Exception here because the input could have SyntaxErrors etc.
15+
rescue Exception => e # rubocop:disable Lint/RescueException
16+
DebugSocket.logger&.error { "debug-socket-error=#{e.inspect} input=#{input.inspect} path=#{@path} backtrace=#{e.backtrace.inspect}" }
17+
"#{e.class.name}: #{e.message}\n#{e.backtrace.join("\n")}"
18+
end
19+
20+
# Print the backtrace for every Thread
21+
def self.backtrace
22+
pid = Process.pid
23+
"#{Time.now.utc.iso8601} #{$PROGRAM_NAME}\n" + Thread.list.map do |thread|
24+
output = "#{Time.now.utc.iso8601} pid=#{pid} thread.object_id=#{thread.object_id} thread.status=#{thread.status}"
25+
backtrace = thread.backtrace || []
26+
output << "\n#{backtrace.join("\n")}" if backtrace
27+
output
28+
end.join("\n\n")
29+
end
30+
end
31+
832
@thread = nil
933
@pid = Process.pid
1034

@@ -16,7 +40,7 @@ def self.logger
1640
return @logger if defined?(@logger)
1741

1842
require "logger"
19-
@logger = Logger.new(STDERR)
43+
@logger = Logger.new($stderr)
2044
end
2145

2246
def self.start(path, &block)
@@ -32,22 +56,27 @@ def self.start(path, &block)
3256

3357
server = UNIXServer.new(@path)
3458
@thread = Thread.new do
59+
errors = 0
3560
loop do
36-
begin
37-
socket = server.accept
38-
input = socket.read
39-
logger&.warn("debug-socket-command=#{input.inspect}")
40-
41-
self.perform_audit(input, &block) if block
42-
43-
socket.puts(eval(input)) # rubocop:disable Security/Eval
44-
45-
rescue Exception => e # rubocop:disable Lint/RescueException
46-
logger&.error { "debug-socket-error=#{e.inspect} backtrace=#{e.backtrace.inspect}" }
47-
ensure
48-
socket&.close
49-
end
61+
socket = server.accept
62+
input = socket.read
63+
logger&.warn("debug-socket-command=#{input.inspect}")
64+
65+
perform_audit(input, &block) if block
66+
socket.puts(Commands.isolated_eval(input))
67+
68+
errors = 0
69+
rescue StandardError => e
70+
errors += 1
71+
logger&.error { "debug-socket-error=#{e.inspect} errors=#{errors} path=#{@path} backtrace=#{e.backtrace.inspect}" }
72+
raise e if errors > 20
73+
74+
sleep(1)
75+
ensure
76+
socket&.close
5077
end
78+
rescue Exception => e # rubocop:disable Lint/RescueException
79+
logger&.error { "debug-socket-error=#{e.inspect} DebugSocket is broken now path=#{@path} backtrace=#{e.backtrace.inspect}" }
5180
end
5281

5382
logger&.unknown { "Debug socket listening on #{@path}" }
@@ -65,21 +94,10 @@ def self.stop
6594
@path = nil
6695
end
6796

68-
def self.backtrace
69-
pid = Process.pid
70-
"#{Time.now.utc.iso8601} #{$PROGRAM_NAME}\n" + Thread.list.map do |thread|
71-
output =
72-
+"#{Time.now.utc.iso8601} pid=#{pid} thread.object_id=#{thread.object_id} thread.status=#{thread.status}"
73-
backtrace = thread.backtrace || []
74-
output << "\n#{backtrace.join("\n")}" if backtrace
75-
output
76-
end.join("\n\n")
77-
end
78-
7997
# Allow debug socket input commands to be audited by an external callback
8098
private_class_method def self.perform_audit(input)
8199
yield input
82-
rescue Exception => e
83-
logger&.error "debug-socket-error=callback unsuccessful: #{e.inspect} for #{input.inspect} socket_path=#{@path}"
100+
rescue Exception => e # rubocop:disable Lint/RescueException
101+
logger&.error "debug-socket-error=callback unsuccessful: #{e.inspect} for #{input.inspect} path=#{@path} backtrace=#{e.backtrace.inspect}"
84102
end
85103
end

0 commit comments

Comments
 (0)