diff --git a/src/debugger_requests.jl b/src/debugger_requests.jl index f271162..4444433 100644 --- a/src/debugger_requests.jl +++ b/src/debugger_requests.jl @@ -1,6 +1,17 @@ # Request handlers function initialize_request(debug_session::DebugSession, params::InitializeRequestArguments) + # The REPL-hosted debugger runs many sessions in one process (VSCodeServer's + # `start_debug_backend`), and JuliaInterpreter's breakpoint registry is process-global. + # The client only sends `setBreakpoints` for files that still have breakpoints, so a file + # whose breakpoints were all deleted between sessions is never mentioned again and its + # stale breakpoints would keep firing. Start every session from a clean slate; the client + # re-sends the full, current set during the configuration phase that follows. + # (Function breakpoints get the same treatment in `DebugEngines.set_function_breakpoints!`.) + for bp in copy(JuliaInterpreter.breakpoints()) + bp isa JuliaInterpreter.BreakpointFileLocation && JuliaInterpreter.remove(bp) + end + return Capabilities( true, # supportsConfigurationDoneRequest::Union{Missing,Bool} true, # supportsFunctionBreakpoints::Union{Missing,Bool} @@ -167,12 +178,16 @@ function set_break_points_request(debug_session::DebugSession, params::SetBreakp @debug "setbreakpoints_request" filename = params.source.path + # `JuliaInterpreter.breakpoint` normalizes the path it is handed and stores that, so the + # removal below has to compare against the normalized form or it silently matches nothing + # and leaves the old breakpoints in place. + normalized_filename = normpath(filename) # JuliaInterpreter.remove mutates the vector returned by # breakpoints(), so we make a copy to not mess up iteration for bp in copy(JuliaInterpreter.breakpoints()) if bp isa JuliaInterpreter.BreakpointFileLocation - if bp.path == filename + if bp.path == normalized_filename @debug "Removing breakpoint at $(bp.path):$(bp.line)" JuliaInterpreter.remove(bp) end diff --git a/test/test_debugsession.jl b/test/test_debugsession.jl index 4fed2c9..9bedf87 100644 --- a/test/test_debugsession.jl +++ b/test/test_debugsession.jl @@ -111,3 +111,49 @@ end @test DefaultTerminationTarget.ran == true @test count(==("terminated"), events) == 1 end + +@testitem "initialize clears file breakpoints left over from an earlier session" begin + import JuliaInterpreter + + # The REPL-hosted debugger serves many sessions from one process, and the client only + # sends `setBreakpoints` for files that still have breakpoints. A file whose breakpoints + # were all deleted between sessions is therefore never mentioned again, so unless the + # adapter clears the process-global registry itself the deleted breakpoints keep firing + # for the rest of the REPL's life (julialang.org discourse #138981). + JuliaInterpreter.remove() + + target = joinpath(@__DIR__, "stale_breakpoint_target.jl") + JuliaInterpreter.breakpoint(target, 2) + @test any(bp -> bp isa JuliaInterpreter.BreakpointFileLocation, JuliaInterpreter.breakpoints()) + + # A new session's first request, with no `setBreakpoints` following it. + session = DebugAdapter.DebugSession(IOBuffer()) + DebugAdapter.initialize_request(session, DebugAdapter.InitializeRequestArguments(adapterID="julia")) + + @test !any(bp -> bp isa JuliaInterpreter.BreakpointFileLocation, JuliaInterpreter.breakpoints()) +end + +@testitem "setBreakpoints removes previous breakpoints for a non-normalized source path" begin + import JuliaInterpreter + + # `JuliaInterpreter.breakpoint` stores the normalized path, so the removal pass has to + # compare against the normalized form of `source.path` or it matches nothing and leaves + # the old breakpoints behind. + JuliaInterpreter.remove() + + target = joinpath(@__DIR__, "sub", "..", "breakpoint_target.jl") + source = DebugAdapter.Source(path=target) + + DebugAdapter.set_break_points_request( + DebugAdapter.DebugSession(IOBuffer()), + DebugAdapter.SetBreakpointsArguments(source=source, breakpoints=[DebugAdapter.SourceBreakpoint(line=2)]) + ) + @test count(bp -> bp isa JuliaInterpreter.BreakpointFileLocation, JuliaInterpreter.breakpoints()) == 1 + + # The client now reports that the file has no breakpoints left. + DebugAdapter.set_break_points_request( + DebugAdapter.DebugSession(IOBuffer()), + DebugAdapter.SetBreakpointsArguments(source=source, breakpoints=DebugAdapter.SourceBreakpoint[]) + ) + @test count(bp -> bp isa JuliaInterpreter.BreakpointFileLocation, JuliaInterpreter.breakpoints()) == 0 +end