Skip to content

Commit 12212bf

Browse files
committed
Fix re-using process for debugging
Each time we disconnect the debugger, the child process opens a new port that we must read to connect a new debugger
1 parent 5f985ad commit 12212bf

File tree

2 files changed

+92
-69
lines changed

2 files changed

+92
-69
lines changed

compiler/test/dotty/tools/debug/DebugTests.scala

Lines changed: 40 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,15 @@ import dotty.tools.io.JFile
77
import dotty.tools.vulpix.*
88
import org.junit.Test
99

10+
import java.util.concurrent.TimeoutException
1011
import scala.concurrent.duration.*
1112
import scala.util.control.NonFatal
1213

1314
class DebugTests:
1415
import DebugTests.*
1516
@Test def debug: Unit =
1617
implicit val testGroup: TestGroup = TestGroup("debug")
17-
// compileFile("tests/debug/eval-static-fields.scala", TestConfiguration.defaultOptions).checkDebug()
18+
// compileFile("tests/debug/eval-private-members-in-parent.scala", TestConfiguration.defaultOptions).checkDebug()
1819
compileFilesInDir("tests/debug", TestConfiguration.defaultOptions).checkDebug()
1920

2021
object DebugTests extends ParallelTesting:
@@ -49,21 +50,24 @@ object DebugTests extends ParallelTesting:
4950
val debugSteps = DebugStepAssert.parseCheckFile(checkFile)
5051
val expressionEvaluator =
5152
ExpressionEvaluator(testSource.sourceFiles, testSource.flags, testSource.runClassPath, testSource.outDir)
52-
try
53-
val status = debugMain(testSource.runClassPath): debuggee =>
54-
val debugger = Debugger(debuggee.jdiPort, expressionEvaluator, maxDuration/* , verbose = true */)
55-
// configure the breakpoints before starting the debuggee
56-
val breakpoints = debugSteps.map(_.step).collect { case b: DebugStep.Break => b }
57-
for b <- breakpoints do debugger.configureBreakpoint(b.className, b.line)
58-
try
59-
debuggee.launch()
60-
playDebugSteps(debugger, debugSteps/* , verbose = true */)
61-
finally
62-
// stop debugger to let debuggee terminate its execution
63-
debugger.dispose()
64-
reportDebuggeeStatus(testSource, status)
53+
try debugMain(testSource.runClassPath): debuggee =>
54+
val jdiPort = debuggee.readJdiPort()
55+
val debugger = Debugger(jdiPort, expressionEvaluator, maxDuration/* , verbose = true */)
56+
// configure the breakpoints before starting the debuggee
57+
val breakpoints = debugSteps.map(_.step).collect { case b: DebugStep.Break => b }.distinct
58+
for b <- breakpoints do debugger.configureBreakpoint(b.className, b.line)
59+
try
60+
debuggee.launch()
61+
playDebugSteps(debugger, debugSteps/* , verbose = true */)
62+
val status = debuggee.exit()
63+
reportDebuggeeStatus(testSource, status)
64+
finally
65+
// closing the debugger must be done at the very end so that the
66+
// 'Listening for transport dt_socket at address: <port>' message is ready to be read
67+
// by the next DebugTest
68+
debugger.dispose()
6569
catch case DebugStepException(message, location) =>
66-
echo(s"\nDebug step failed: $location\n" + message)
70+
echo(s"\n[error] Debug step failed: $location\n" + message)
6771
failTestSource(testSource)
6872
end verifyDebug
6973

@@ -77,33 +81,41 @@ object DebugTests extends ParallelTesting:
7781
*/
7882
var thread: ThreadReference = null
7983
def location = thread.frame(0).location
84+
def continueIfPaused(): Unit =
85+
if thread != null then
86+
debugger.continue(thread)
87+
thread = null
8088

8189
for case step <- steps do
8290
import DebugStep.*
83-
step match
91+
try step match
8492
case DebugStepAssert(Break(className, line), assert) =>
85-
// continue if paused
86-
if thread != null then
87-
debugger.continue(thread)
88-
thread = null
93+
continueIfPaused()
8994
thread = debugger.break()
90-
if verbose then println(s"break ${location.declaringType.name} ${location.lineNumber}")
95+
if verbose then
96+
println(s"break $location ${location.method.name}")
9197
assert(location)
9298
case DebugStepAssert(Next, assert) =>
9399
thread = debugger.next(thread)
94-
if verbose then println(s"next ${location.lineNumber}")
100+
if verbose then println(s"next $location ${location.method.name}")
95101
assert(location)
96102
case DebugStepAssert(Step, assert) =>
97103
thread = debugger.step(thread)
98-
if verbose then println(s"step ${location.lineNumber}")
104+
if verbose then println(s"step $location ${location.method.name}")
99105
assert(location)
100106
case DebugStepAssert(Eval(expr), assert) =>
101-
val result =
102-
try debugger.evaluate(expr, thread)
103-
catch case NonFatal(cause) =>
104-
throw new Exception(s"Evaluation of $expr failed", cause)
105-
if verbose then println(s"eval $expr $result")
107+
if verbose then println(s"eval $expr")
108+
val result = debugger.evaluate(expr, thread)
109+
if verbose then println(result.fold("error " + _, "result " + _))
106110
assert(result)
111+
catch
112+
case _: TimeoutException => throw new DebugStepException("Timeout", step.location)
113+
case e: DebugStepException => throw e
114+
case NonFatal(e) =>
115+
throw new Exception(s"Debug step failed unexpectedly: ${step.location}", e)
116+
end for
117+
// let the debuggee finish its execution
118+
continueIfPaused()
107119
end playDebugSteps
108120

109121
private def reportDebuggeeStatus(testSource: TestSource, status: Status): Unit =

compiler/test/dotty/tools/vulpix/RunnerOrchestration.scala

Lines changed: 52 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -59,16 +59,19 @@ trait RunnerOrchestration {
5959
def runMain(classPath: String, toolArgs: ToolArgs)(implicit summaryReport: SummaryReporting): Status =
6060
monitor.runMain(classPath)
6161

62+
/** Each method of Debuggee can be called only once, in the order of definition.*/
6263
trait Debuggee:
63-
/** the jdi port to connect the debugger */
64-
def jdiPort: Int
64+
/** read the jdi port to connect the debugger */
65+
def readJdiPort(): Int
6566
/** start the main method in the background */
6667
def launch(): Unit
68+
/** wait until the end of the main method */
69+
def exit(): Status
6770

6871
/** Provide a Debuggee for debugging the Test class's main method
69-
* @param f the debugging flow: set breakpoints, launch main class, pause, step, evaluate etc
72+
* @param f the debugging flow: set breakpoints, launch main class, pause, step, evaluate, exit etc
7073
*/
71-
def debugMain(classPath: String)(f: Debuggee => Unit)(implicit summaryReport: SummaryReporting): Status =
74+
def debugMain(classPath: String)(f: Debuggee => Unit)(implicit summaryReport: SummaryReporting): Unit =
7275
assert(debugMode, "debugMode is disabled")
7376
monitor.debugMain(classPath)(f)
7477

@@ -92,14 +95,31 @@ trait RunnerOrchestration {
9295
def runMain(classPath: String)(implicit summaryReport: SummaryReporting): Status =
9396
withRunner(_.runMain(classPath))
9497

95-
def debugMain(classPath: String)(f: Debuggee => Unit)(implicit summaryReport: SummaryReporting): Status =
98+
def debugMain(classPath: String)(f: Debuggee => Unit)(implicit summaryReport: SummaryReporting): Unit =
9699
withRunner(_.debugMain(classPath)(f))
97100

98-
// A JVM process and its JDI port for debugging, if debugMode is enabled.
99-
private class RunnerProcess(p: Process, val jdiPort: Option[Int]):
100-
val stdout = new BufferedReader(new InputStreamReader(p.getInputStream(), StandardCharsets.UTF_8))
101-
val stdin = new PrintStream(p.getOutputStream(), /* autoFlush = */ true)
101+
private class RunnerProcess(p: Process):
102+
private val stdout = new BufferedReader(new InputStreamReader(p.getInputStream(), StandardCharsets.UTF_8))
103+
private val stdin = new PrintStream(p.getOutputStream(), /* autoFlush = */ true)
104+
105+
def readLine(): String =
106+
stdout.readLine() match
107+
case s"Listening for transport dt_socket at address: $port" =>
108+
throw new IOException(
109+
s"Unexpected transport dt_socket message." +
110+
" The port is going to be lost and no debugger will be able to connect."
111+
)
112+
case line => line
113+
114+
def printLine(line: String): Unit = stdin.println(line)
115+
116+
def getJdiPort(): Int =
117+
stdout.readLine() match
118+
case s"Listening for transport dt_socket at address: $port" => port.toInt
119+
case line => throw new IOException(s"Failed getting JDI port of child JVM: got $line")
120+
102121
export p.{exitValue, isAlive, destroy}
122+
end RunnerProcess
103123

104124
private class Runner(private var process: RunnerProcess):
105125

@@ -111,7 +131,7 @@ trait RunnerOrchestration {
111131
*/
112132
def isAlive: Boolean =
113133
try { process.exitValue(); false }
114-
catch { case _: IllegalThreadStateException => true }
134+
catch case _: IllegalThreadStateException => true
115135

116136
/** Destroys the underlying process and kills IO streams */
117137
def kill(): Unit =
@@ -123,51 +143,50 @@ trait RunnerOrchestration {
123143
assert(process ne null, "Runner was killed and then reused without setting a new process")
124144
awaitStatusOrRespawn(startMain(classPath))
125145

126-
def debugMain(classPath: String)(f: Debuggee => Unit): Status =
146+
def debugMain(classPath: String)(f: Debuggee => Unit): Unit =
127147
assert(process ne null, "Runner was killed and then reused without setting a new process")
128-
assert(process.jdiPort.isDefined, "Runner has not been started in debug mode")
129148

130-
var mainFuture: Future[Status] = null
131149
val debuggee = new Debuggee:
132-
def jdiPort: Int = process.jdiPort.get
133-
def launch(): Unit =
134-
mainFuture = startMain(classPath)
150+
var mainFuture: Future[Status] = null
151+
def readJdiPort(): Int = process.getJdiPort()
152+
def launch(): Unit = mainFuture = startMain(classPath)
153+
def exit(): Status =
154+
awaitStatusOrRespawn(mainFuture)
135155

136156
try f(debuggee)
137-
catch case debugFailure: Throwable =>
138-
if mainFuture != null then awaitStatusOrRespawn(mainFuture)
139-
throw debugFailure
140-
141-
assert(mainFuture ne null, "main method not started by debugger")
142-
awaitStatusOrRespawn(mainFuture)
157+
catch case e: Throwable =>
158+
// if debugging failed it is safer to respawn a new process
159+
respawn()
160+
throw e
143161
end debugMain
144162

145163
private def startMain(classPath: String): Future[Status] =
146164
// pass classpath to running process
147-
process.stdin.println(classPath)
165+
process.printLine(classPath)
148166

149167
// Create a future reading the object:
150168
Future:
151169
val sb = new StringBuilder
152170

153-
var childOutput: String = process.stdout.readLine()
171+
var childOutput: String = process.readLine()
154172

155173
// Discard all messages until the test starts
156174
while (childOutput != ChildJVMMain.MessageStart && childOutput != null)
157-
childOutput = process.stdout.readLine()
158-
childOutput = process.stdout.readLine()
175+
childOutput = process.readLine()
176+
childOutput = process.readLine()
159177

160178
while childOutput != ChildJVMMain.MessageEnd && childOutput != null do
161179
sb.append(childOutput).append(System.lineSeparator)
162-
childOutput = process.stdout.readLine()
180+
childOutput = process.readLine()
163181

164-
if (process.isAlive() && childOutput != null) Success(sb.toString)
182+
if process.isAlive() && childOutput != null then Success(sb.toString)
165183
else Failure(sb.toString)
166184
end startMain
167185

168186
// wait status of the main class execution, respawn if failure or timeout
169187
private def awaitStatusOrRespawn(future: Future[Status]): Status =
170-
val status = try Await.result(future, maxDuration)
188+
val status =
189+
try Await.result(future, maxDuration)
171190
catch case _: TimeoutException => Timeout
172191
// handle failures
173192
status match
@@ -178,13 +197,13 @@ trait RunnerOrchestration {
178197
// Makes the encapsulating RunnerMonitor spawn a new runner
179198
private def respawn(): Unit =
180199
process.destroy()
181-
process = createProcess
200+
process = createProcess()
182201
end Runner
183202

184203
/** Create a process which has the classpath of the `ChildJVMMain` and the
185204
* scala library.
186205
*/
187-
private def createProcess: RunnerProcess = {
206+
private def createProcess(): RunnerProcess =
188207
val url = classOf[ChildJVMMain].getProtectionDomain.getCodeSource.getLocation
189208
val cp = Paths.get(url.toURI).toString + JFile.pathSeparator + Properties.scalaLibrary
190209
val javaBin = Paths.get(sys.props("java.home"), "bin", "java").toString
@@ -196,15 +215,7 @@ trait RunnerOrchestration {
196215
.redirectInput(ProcessBuilder.Redirect.PIPE)
197216
.redirectOutput(ProcessBuilder.Redirect.PIPE)
198217
.start()
199-
200-
val jdiPort = Option.when(debugMode):
201-
val reader = new BufferedReader(new InputStreamReader(process.getInputStream, StandardCharsets.UTF_8))
202-
reader.readLine() match
203-
case s"Listening for transport dt_socket at address: $port" => port.toInt
204-
case line => throw new IOException(s"Failed getting JDI port of child JVM: got $line")
205-
206-
RunnerProcess(process, jdiPort)
207-
}
218+
RunnerProcess(process)
208219

209220
private val freeRunners = mutable.Queue.empty[Runner]
210221
private val busyRunners = mutable.Set.empty[Runner]
@@ -213,7 +224,7 @@ trait RunnerOrchestration {
213224
while (freeRunners.isEmpty && busyRunners.size >= numberOfSlaves) wait()
214225

215226
val runner =
216-
if (freeRunners.isEmpty) new Runner(createProcess)
227+
if (freeRunners.isEmpty) new Runner(createProcess())
217228
else freeRunners.dequeue()
218229
busyRunners += runner
219230

0 commit comments

Comments
 (0)