Skip to content

Commit 20f70ea

Browse files
committed
Fix multi-process breakpoint sync issues found in review
- Fix version counter drift: read file version before writing to prevent processes from missing each other's updates - Add MethodBreakpoint sync support (to_sync_data + reconciliation) - Fix CatchBreakpoint sync to preserve command and path attributes - Add syncable? predicate to avoid unnecessary hash allocation - Add type validation in create_bp_from_spec for defense-in-depth - Use Dir.tmpdir instead of hardcoded /tmp for portability - Set explicit 0600 permissions on temp state file writes - Broaden error handling to SystemCallError in read/write state - Add error handling to ensure_wk_lock! for disk-full/read-only - Publish breakpoint changes on DAP disconnect
1 parent cdd7e8e commit 20f70ea

File tree

3 files changed

+55
-10
lines changed

3 files changed

+55
-10
lines changed

lib/debug/breakpoint.rb

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,10 @@ def to_sync_data
2525
nil
2626
end
2727

28+
def syncable?
29+
false
30+
end
31+
2832
def safe_eval b, expr
2933
b.eval(expr)
3034
rescue Exception => e
@@ -233,6 +237,10 @@ def to_sync_data
233237
'hook_call' => @hook_call, 'command' => @command }
234238
end
235239

240+
def syncable?
241+
true
242+
end
243+
236244
def duplicable?
237245
@oneshot
238246
end
@@ -315,7 +323,12 @@ class CatchBreakpoint < Breakpoint
315323
attr_reader :last_exc
316324

317325
def to_sync_data
318-
{ 'type' => 'catch', 'pat' => @pat, 'cond' => @cond }
326+
{ 'type' => 'catch', 'pat' => @pat, 'cond' => @cond,
327+
'command' => @command, 'path' => @path }
328+
end
329+
330+
def syncable?
331+
true
319332
end
320333

321334
def initialize pat, cond: nil, command: nil, path: nil
@@ -443,6 +456,16 @@ def to_s
443456
class MethodBreakpoint < Breakpoint
444457
attr_reader :sig_method_name, :method, :klass
445458

459+
def to_sync_data
460+
{ 'type' => 'method', 'klass' => @sig_klass_name,
461+
'op' => @sig_op, 'method' => @sig_method_name,
462+
'cond' => @cond, 'command' => @command }
463+
end
464+
465+
def syncable?
466+
true
467+
end
468+
446469
def initialize b, klass_name, op, method_name, cond: nil, command: nil, path: nil
447470
@sig_klass_name = klass_name
448471
@sig_op = op

lib/debug/server_dap.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -403,6 +403,7 @@ def process_request req
403403
terminate = args.fetch("terminateDebuggee", false)
404404

405405
SESSION.clear_all_breakpoints
406+
SESSION.bp_sync_publish
406407
send_response req
407408

408409
if SESSION.in_subsession?

lib/debug/session.rb

Lines changed: 30 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
require 'json' if ENV['RUBY_DEBUG_TEST_UI'] == 'terminal'
4444
require 'pp'
4545
require 'set'
46+
require 'tmpdir'
4647

4748
class RubyVM::InstructionSequence
4849
def traceable_lines_norec lines
@@ -133,27 +134,36 @@ def reconcile_breakpoints(specs)
133134

134135
def bp_key_from_spec(spec)
135136
case spec['type']
136-
when 'line' then [spec['path'], spec['line']]
137-
when 'catch' then [:catch, spec['pat']]
137+
when 'line' then [spec['path'], spec['line']]
138+
when 'catch' then [:catch, spec['pat']]
139+
when 'method' then "#{spec['klass']}#{spec['op']}#{spec['method']}"
138140
end
139141
end
140142

141143
def create_bp_from_spec(spec)
142144
bp = case spec['type']
143145
when 'line'
146+
return unless spec['path'].is_a?(String) && spec['line'].is_a?(Integer)
144147
LineBreakpoint.new(spec['path'], spec['line'],
145148
cond: spec['cond'], oneshot: spec['oneshot'],
146149
hook_call: spec['hook_call'] != false,
147150
command: spec['command'])
148151
when 'catch'
149-
CatchBreakpoint.new(spec['pat'], cond: spec['cond'])
152+
return unless spec['pat'].is_a?(String)
153+
CatchBreakpoint.new(spec['pat'],
154+
cond: spec['cond'], command: spec['command'],
155+
path: spec['path'])
156+
when 'method'
157+
return unless spec['klass'].is_a?(String) && spec['op'].is_a?(String) && spec['method'].is_a?(String)
158+
MethodBreakpoint.new(TOPLEVEL_BINDING, spec['klass'], spec['op'], spec['method'],
159+
cond: spec['cond'], command: spec['command'])
150160
end
151161

152162
add_bp(bp) if bp
153163
end
154164

155165
def syncable_bp?(bp)
156-
bp.to_sync_data != nil
166+
bp.syncable?
157167
end
158168
end
159169

@@ -2141,7 +2151,7 @@ def read_breakpoint_state; nil; end
21412151
def wk_lock
21422152
return if multi? # MultiProcessGroup handles its own locking
21432153
ensure_wk_lock!
2144-
@wk_lock_file.flock(File::LOCK_EX)
2154+
@wk_lock_file&.flock(File::LOCK_EX)
21452155
end
21462156

21472157
def unlock_wk_lock
@@ -2151,8 +2161,10 @@ def unlock_wk_lock
21512161

21522162
private def ensure_wk_lock!
21532163
return if @wk_lock_file
2154-
path = File.join('/tmp', "ruby-debug-#{Process.uid}-pgrp-#{Process.getpgrp}.lock")
2164+
path = File.join(Dir.tmpdir, "ruby-debug-#{Process.uid}-pgrp-#{Process.getpgrp}.lock")
21552165
@wk_lock_file = File.open(path, File::WRONLY | File::CREAT, 0600)
2166+
rescue SystemCallError => e
2167+
DEBUGGER__.warn "Failed to create well-known lock file: #{e.message}"
21562168
end
21572169

21582170
def multi_process!
@@ -2249,11 +2261,20 @@ def unlock
22492261
end
22502262

22512263
def write_breakpoint_state(specs)
2252-
@bp_sync_version += 1
2264+
# Read current file version to avoid drift between processes
2265+
current_v = begin
2266+
d = JSON.parse(File.read(@state_tempfile.path))
2267+
d['v']
2268+
rescue
2269+
0
2270+
end
2271+
@bp_sync_version = [current_v, @bp_sync_version].max + 1
22532272
data = JSON.generate({ 'v' => @bp_sync_version, 'bps' => specs })
22542273
tmp = "#{@state_tempfile.path}.#{Process.pid}.tmp"
2255-
File.write(tmp, data)
2274+
File.write(tmp, data, perm: 0600)
22562275
File.rename(tmp, @state_tempfile.path)
2276+
rescue SystemCallError => e
2277+
DEBUGGER__.warn "Failed to write breakpoint state: #{e.message}"
22572278
end
22582279

22592280
def read_breakpoint_state
@@ -2263,7 +2284,7 @@ def read_breakpoint_state
22632284
return nil if remote_v <= @bp_sync_version
22642285
@bp_sync_version = remote_v
22652286
data['bps']
2266-
rescue JSON::ParserError, Errno::ENOENT
2287+
rescue JSON::ParserError, SystemCallError
22672288
nil
22682289
end
22692290

0 commit comments

Comments
 (0)