Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 1 addition & 5 deletions .github/workflows/ruby.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ permissions:

jobs:
test:

runs-on: ubuntu-latest
strategy:
matrix:
Expand All @@ -27,10 +26,7 @@ jobs:
steps:
- uses: actions/checkout@v4
- name: Set up Ruby
# To automatically get bug fixes and new Ruby versions for ruby/setup-ruby,
# change this to (see https://github.com/ruby/setup-ruby#versioning):
# uses: ruby/setup-ruby@v1
uses: ruby/setup-ruby@55283cc23133118229fd3f97f9336ee23a179fcf # v1.146.0
uses: ruby/setup-ruby@v1
with:
ruby-version: ${{ matrix.ruby-version }}
bundler-cache: true # runs 'bundle install' and caches installed gems automatically
Expand Down
20 changes: 15 additions & 5 deletions .rubocop.yml
Original file line number Diff line number Diff line change
@@ -1,15 +1,25 @@
AllCops:
TargetRubyVersion: 2.4
TargetRubyVersion: 3.1
DisplayCopNames: true
NewCops: enable

Style/FrozenStringLiteralComment:
Enabled: true
plugins:
- rubocop-rake
- rubocop-rspec

Layout/LineLength:
Enabled: false
Metrics:
Enabled: false

RSpec/DescribedClass:
Enabled: false
RSpec/ExampleLength:
Enabled: false
RSpec/MultipleExpectations:
Enabled: false
Style/Documentation:
Enabled: false

Style/FrozenStringLiteralComment:
Enabled: true
Style/StringLiterals:
EnforcedStyle: double_quotes
11 changes: 0 additions & 11 deletions .travis.yml

This file was deleted.

8 changes: 7 additions & 1 deletion CHANGES.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
### 0.1.9 2025-04-03

- [#17](https://github.com/square/debug_socket/pull/17)
Refactor error handling. Previously all errors (including `eval` errors) were caught in the same `rescue Exception`.
Now, we only `rescue Exception` for `eval` errors. For `DebugSocket` errors, we only `rescue StandardError` and we
allow 20 consecutive errors before `DebugSocket` gives up and dies permanently.
(@nerdrew)

- [#16](https://github.com/square/debug_socket/pull/16)
Allow external auditing of debug sessions.
(dogor@)
(@doctown)

### 0.1.8 2022-10-10

Expand Down
10 changes: 8 additions & 2 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,13 @@ source "https://rubygems.org"
# Specify your gem's dependencies in debug_socket.gemspec
gemspec

gem "pry"
gem "base64"
gem "ostruct"
gem "pry-byebug"
gem "rake"
gem "rspec", "~> 3.8"
gem "rubocop", "0.59.1"
gem "rubocop", "1.73.1"
gem "rubocop-rake"
gem "rubocop-rspec"
gem "ruby-lsp", require: false
gem "syntax_tree", require: false
3 changes: 3 additions & 0 deletions debug_socket.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,7 @@ Gem::Specification.new do |spec|
spec.bindir = "exe"
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
spec.require_paths = ["lib"]
spec.metadata["rubygems_mfa_required"] = "true"

spec.required_ruby_version = ">= 3.1"
end
4 changes: 3 additions & 1 deletion exe/debug-socket
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,10 @@ end
socket_path = ARGV[0]
command = ARGV[1] || "backtrace"

warn "\nSending `#{command}` to the following socket: #{socket_path}"\
warn(
"\nSending `#{command}` to the following socket: #{socket_path}" \
"----------------------------------------------------------\n\n"
)

UNIXSocket.open(socket_path) do |socket|
socket.write(command)
Expand Down
74 changes: 46 additions & 28 deletions lib/debug_socket.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,30 @@
require "time"

module DebugSocket
module Commands
# When running `eval`, we don't want the input to overwrite local variables etc. `eval` runs in the current scope,
# so we have an empty scope here that runs in a module that only has other shortcut commands the client might want
# to run.
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@doctown comment as requested.

def self.isolated_eval(input)
eval(input) # rubocop:disable Security/Eval
# We rescue Exception here because the input could have SyntaxErrors etc.
rescue Exception => e # rubocop:disable Lint/RescueException
DebugSocket.logger&.error { "debug-socket-error=#{e.inspect} input=#{input.inspect} path=#{@path} backtrace=#{e.backtrace.inspect}" }
"#{e.class.name}: #{e.message}\n#{e.backtrace.join("\n")}"
end

# Print the backtrace for every Thread
def self.backtrace
pid = Process.pid
"#{Time.now.utc.iso8601} #{$PROGRAM_NAME}\n" + Thread.list.map do |thread|
output = "#{Time.now.utc.iso8601} pid=#{pid} thread.object_id=#{thread.object_id} thread.status=#{thread.status}"
backtrace = thread.backtrace || []
output << "\n#{backtrace.join("\n")}" if backtrace
output
end.join("\n\n")
end
end

@thread = nil
@pid = Process.pid

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

require "logger"
@logger = Logger.new(STDERR)
@logger = Logger.new($stderr)
end

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

server = UNIXServer.new(@path)
@thread = Thread.new do
errors = 0
loop do
begin
socket = server.accept
input = socket.read
logger&.warn("debug-socket-command=#{input.inspect}")

self.perform_audit(input, &block) if block

socket.puts(eval(input)) # rubocop:disable Security/Eval

rescue Exception => e # rubocop:disable Lint/RescueException
logger&.error { "debug-socket-error=#{e.inspect} backtrace=#{e.backtrace.inspect}" }
ensure
socket&.close
end
socket = server.accept
input = socket.read
logger&.warn("debug-socket-command=#{input.inspect}")

perform_audit(input, &block) if block
socket.puts(Commands.isolated_eval(input))

errors = 0
rescue StandardError => e
errors += 1
logger&.error { "debug-socket-error=#{e.inspect} errors=#{errors} path=#{@path} backtrace=#{e.backtrace.inspect}" }
raise e if errors > 20

sleep(1)
ensure
socket&.close
end
rescue Exception => e # rubocop:disable Lint/RescueException
logger&.error { "debug-socket-error=#{e.inspect} DebugSocket is broken now path=#{@path} backtrace=#{e.backtrace.inspect}" }
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should the message inform the user to restart the connection or try another socket?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is no other socket. This message means the debug socket server is broken and the process would need to be restarted. This is a fatal (for the debug socket at least) error.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know if we have ever seen this in practice. BUT I noticed it when testing. When I typo-ed a line, there were a LOT of logs spewing the error because we had a rescue Exception inside a loop. I've now changed it to have a rescue Exception on the eval above (we don't want user input to disrupt anything). But we shouldn't be rescuing SyntaxErrors in this file itself.

Now, we only allow 20 errors that aren't cause by the eval itself, e.g. a socket error can happen if the client writes to the socket and then closes really quickly before it has time to respond. I think that is ok. I don't know how to distinguish "there's something wrong with the socket and we shouldn't infinite loop spewing errors" vs "the client did something wrong and we should rescue the error and keep going". I think it's better to fail VERY conservatively for a debug tool. I never want DebugSocket to interrupt or negatively impact a running process (of course you can do that yourself by sending exit as your command, but don't do that).

end

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

def self.backtrace
pid = Process.pid
"#{Time.now.utc.iso8601} #{$PROGRAM_NAME}\n" + Thread.list.map do |thread|
output =
+"#{Time.now.utc.iso8601} pid=#{pid} thread.object_id=#{thread.object_id} thread.status=#{thread.status}"
backtrace = thread.backtrace || []
output << "\n#{backtrace.join("\n")}" if backtrace
output
end.join("\n\n")
end

# Allow debug socket input commands to be audited by an external callback
private_class_method def self.perform_audit(input)
yield input
rescue Exception => e
logger&.error "debug-socket-error=callback unsuccessful: #{e.inspect} for #{input.inspect} socket_path=#{@path}"
rescue Exception => e # rubocop:disable Lint/RescueException
logger&.error "debug-socket-error=callback unsuccessful: #{e.inspect} for #{input.inspect} path=#{@path} backtrace=#{e.backtrace.inspect}"
end
end
Loading