diff options
author | Paul Phillips <paulp@improving.org> | 2010-12-06 20:46:20 +0000 |
---|---|---|
committer | Paul Phillips <paulp@improving.org> | 2010-12-06 20:46:20 +0000 |
commit | 1113f7ddca4f1814396e0de9f586c300b135f157 (patch) | |
tree | 077b70af447784f580fdcfcdceb505117661500b | |
parent | 8922c4ed092a2fb4c3f6002f7c49d304f299cd02 (diff) | |
download | scala-1113f7ddca4f1814396e0de9f586c300b135f157.tar.gz scala-1113f7ddca4f1814396e0de9f586c300b135f157.tar.bz2 scala-1113f7ddca4f1814396e0de9f586c300b135f157.zip |
The ability to interrupt repl execution without...
The ability to interrupt repl execution without terminating the JVM has
snowballed a bit, but it's so handy that it's worth it. This commit
further encapsulates individual lines of execution such that they
can be managed after the signal comes in, and if necessary, forcibly
terminated. I'm straying into territory where I could use a really
Thread-knowledgeable reviewer: any volunteers? In the meantime, no
review.
5 files changed, 170 insertions, 38 deletions
diff --git a/src/compiler/scala/tools/nsc/Interpreter.scala b/src/compiler/scala/tools/nsc/Interpreter.scala index 57e3f904be..99aa1bcad6 100644 --- a/src/compiler/scala/tools/nsc/Interpreter.scala +++ b/src/compiler/scala/tools/nsc/Interpreter.scala @@ -6,16 +6,13 @@ package scala.tools.nsc import Predef.{ println => _, _ } -import java.io.{ File, PrintWriter, StringWriter, Writer } -import File.pathSeparator -import java.lang.{ reflect, Class, ClassLoader => JavaClassLoader } -import java.net.{ MalformedURLException, URL } -import reflect.InvocationTargetException -import java.util.concurrent.Future - +import java.io.{ PrintWriter } +import java.io.File.pathSeparator +import java.lang.reflect +import java.net.URL import util.{ Set => _, _ } import interpreter._ -import io.{ PlainFile, VirtualDirectory, spawn, callable, newDaemonThreadExecutor } +import io.VirtualDirectory import reporters.{ ConsoleReporter, Reporter } import symtab.{ Flags, Names } import scala.tools.nsc.{ InterpreterResults => IR } @@ -27,7 +24,7 @@ import Exceptional.unwrap import scala.collection.{ mutable, immutable } import scala.collection.mutable.{ ListBuffer, ArrayBuffer } import scala.PartialFunction.{ cond, condOpt } -import scala.util.control.Exception.{ Catcher, catching, catchingPromiscuously, ultimately, unwrapping } +import scala.util.control.Exception.{ ultimately } import scala.reflect.NameTransformer import Interpreter._ @@ -186,10 +183,8 @@ class Interpreter(val settings: Settings, out: PrintWriter) { } } - /** An executor service which creates daemon threads. */ - private lazy val lineExecutor = newDaemonThreadExecutor() - private var _currentExecution: Future[String] = null - def currentExecution = _currentExecution + protected def createLineManager(): Line.Manager = new Line.Manager + lazy val lineManager = createLineManager() /** interpreter settings */ lazy val isettings = new InterpreterSettings(this) @@ -580,7 +575,10 @@ class Interpreter(val settings: Settings, out: PrintWriter) { if (trees.size == 1) trees.head match { case _:Assign => // we don't want to include assignments case _:TermTree | _:Ident | _:Select => // ... but do want these as valdefs. - return requestFromLine("val %s =\n%s".format(varName, line), synthetic) + requestFromLine("val %s =\n%s".format(varName, line), synthetic) match { + case Right(req) => return Right(req withOriginalLine line) + case x => return x + } case _ => } @@ -829,6 +827,10 @@ class Interpreter(val settings: Settings, out: PrintWriter) { /** One line of code submitted by the user for interpretation */ private class Request(val line: String, val lineName: String, val trees: List[Tree]) { + private var _originalLine: String = null + def withOriginalLine(s: String): this.type = { _originalLine = s ; this } + def originalLine = if (_originalLine == null) line else _originalLine + /** name to use for the object that will compute "line" */ def objectName = lineName + INTERPRETER_WRAPPER_SUFFIX @@ -1023,29 +1025,32 @@ class Interpreter(val settings: Settings, out: PrintWriter) { /** load and run the code using reflection */ def loadAndRun: (String, Boolean) = { - try { - val resultValMethod = loadedResultObject getMethod "scala_repl_result" - _currentExecution = lineExecutor submit callable(resultValMethod.invoke(loadedResultObject).toString) - while (!currentExecution.isDone) - Thread.`yield` + import interpreter.Line._ - if (currentExecution.isCancelled) ("Execution interrupted by signal.\n", false) - else (currentExecution.get(), true) - } - catch { - case t: Throwable if bindLastException => - /** We turn off the binding to accomodate ticket #2817 */ - withoutBindingLastException { - val message = - if (opt.richExes) bindExceptionally(unwrap(t)) - else bindUnexceptionally(unwrap(t)) - - (message, false) - } + def handleException(t: Throwable) = { + /** We turn off the binding to accomodate ticket #2817 */ + withoutBindingLastException { + val message = + if (opt.richExes) bindExceptionally(unwrap(t)) + else bindUnexceptionally(unwrap(t)) + + (message, false) + } } - finally { - _currentExecution = null + + try { + val resultValMethod = loadedResultObject getMethod "scala_repl_result" + val execution = lineManager.set(originalLine)(resultValMethod invoke loadedResultObject) + + execution.await() + execution.state match { + case Done => ("" + execution.get(), true) + case Threw => if (bindLastException) handleException(execution.caught()) else throw execution.caught() + case Cancelled => ("Execution interrupted by signal.\n", false) + case Running => ("Execution still running! Seems impossible.", false) + } } + finally lineManager.clear() } override def toString = "Request(line=%s, %s trees)".format(line, trees.size) diff --git a/src/compiler/scala/tools/nsc/InterpreterLoop.scala b/src/compiler/scala/tools/nsc/InterpreterLoop.scala index 27e1be208d..82b32fe219 100644 --- a/src/compiler/scala/tools/nsc/InterpreterLoop.scala +++ b/src/compiler/scala/tools/nsc/InterpreterLoop.scala @@ -118,18 +118,19 @@ class InterpreterLoop(in0: Option[BufferedReader], protected val out: PrintWrite } ignoring(classOf[Exception]) { SignalManager("INT") = { - val exec = interpreter.currentExecution - if (exec != null) exec.cancel(true) + if (interpreter.lineManager.running) + interpreter.lineManager.cancel() else if (in.currentLine != "") { + // non-empty buffer, so make them hit ctrl-C a second time SignalManager("INT") = onExit() - io.timer(5)(installSigIntHandler()) // restore original + io.timer(5)(installSigIntHandler()) // and restore original handler if they don't } else onExit() } } } - /** Close the interpreter and set the var to <code>null</code>. */ + /** Close the interpreter and set the var to null. */ def closeInterpreter() { if (interpreter ne null) { interpreter.close @@ -144,6 +145,18 @@ class InterpreterLoop(in0: Option[BufferedReader], protected val out: PrintWrite settings.classpath append addedClasspath interpreter = new Interpreter(settings, out) { + override protected def createLineManager() = new Line.Manager { + override def onRunaway(line: Line[_]): Unit = { + val template = """ + |// She's gone rogue, captain! Have to take her out! + |// Calling Thread.stop on runaway %s with offending code: + |// scala> %s""".stripMargin + + println(template.format(line.thread, line.code)) + line.thread.stop() + in.redrawLine() + } + } override protected def parentClassLoader = settings.explicitParentLoader.getOrElse( classOf[InterpreterLoop].getClassLoader ) } diff --git a/src/compiler/scala/tools/nsc/interpreter/InteractiveReader.scala b/src/compiler/scala/tools/nsc/interpreter/InteractiveReader.scala index 014fd62a0c..8ea4d74369 100644 --- a/src/compiler/scala/tools/nsc/interpreter/InteractiveReader.scala +++ b/src/compiler/scala/tools/nsc/interpreter/InteractiveReader.scala @@ -17,6 +17,7 @@ trait InteractiveReader { protected def readOneLine(prompt: String): String val interactive: Boolean def init(): Unit = () + def redrawLine(): Unit = () def currentLine = "" // the current buffer contents, if available def readLine(prompt: String): String = { diff --git a/src/compiler/scala/tools/nsc/interpreter/JLineReader.scala b/src/compiler/scala/tools/nsc/interpreter/JLineReader.scala index 1da96de0d9..0d71822172 100644 --- a/src/compiler/scala/tools/nsc/interpreter/JLineReader.scala +++ b/src/compiler/scala/tools/nsc/interpreter/JLineReader.scala @@ -16,6 +16,11 @@ class JLineReader(interpreter: Interpreter) extends InteractiveReader { override lazy val history = Some(History(consoleReader)) override lazy val completion = Option(interpreter) map (x => new Completion(x)) override def init() = consoleReader.getTerminal().initializeTerminal() + override def redrawLine() = { + consoleReader.flushConsole() + consoleReader.drawLine() + consoleReader.flushConsole() + } val consoleReader = { val r = new jline.ConsoleReader() diff --git a/src/compiler/scala/tools/nsc/interpreter/Line.scala b/src/compiler/scala/tools/nsc/interpreter/Line.scala new file mode 100644 index 0000000000..2dc23aa38a --- /dev/null +++ b/src/compiler/scala/tools/nsc/interpreter/Line.scala @@ -0,0 +1,108 @@ +/* NSC -- new Scala compiler + * Copyright 2005-2010 LAMP/EPFL + * @author Paul Phillips + */ + +package scala.tools.nsc +package interpreter + +import java.util.concurrent.locks.ReentrantLock +import scala.tools.nsc.util.Exceptional +import Exceptional.unwrap +import Line._ + +/** Encapsulation of a single line in the repl. The concurrency + * infrastructure arose to deal with signals so SIGINT could be + * trapped without losing the repl session, but it will be useful + * in ways beyond that. Each line obtains a thread and the repl + * waits on a condition indicating that either the line has + * completed or failed. + */ +class Line[+T](val code: String, body: => T) { + private var _state: State = Running + private var _result: Any = null + private var _caught: Throwable = null + private val lock = new ReentrantLock() + private val finished = lock.newCondition() + + private def withLock[T](body: => T) = { + lock.lock() + try body + finally lock.unlock() + } + private def setState(state: State) = withLock { + _state = state + finished.signal() + } + // private because it should be called by the manager. + private def cancel() = if (running) setState(Cancelled) + + // This is where the line thread is created and started. + private val _thread = io.daemonize(true) { + try { + _result = body + setState(Done) + } + catch { + case x => + _caught = x + setState(Threw) + } + } + + def state = _state + def thread = _thread + def alive = thread.isAlive + def runaway = !success && alive + def success = _state == Done + def running = _state == Running + + def caught() = { await() ; _caught } + def get() = { await() ; _result } + def await() = withLock { while (running) finished.await() } +} + +object Line { + // seconds to let a runaway thread live before calling stop() + private val HUNTER_KILLER_DELAY = 5 + + // A line opens in state Running, and will eventually + // transition to Threw (an exception was caught), Cancelled + // (the line was explicitly cancelled, presumably by SIGINT) + // or Done (success). + sealed abstract class State + case object Running extends State + case object Threw extends State + case object Cancelled extends State + case object Done extends State + + class Manager { + /** Override to add behavior for runaway lines. This method will + * be called if a line thread is still running five seconds after + * it has been cancelled. + */ + def onRunaway(line: Line[_]): Unit = () + + private var _current: Option[Line[_]] = None + def current = _current + + def clear() = { + _current foreach (_.cancel()) + _current = None + } + def set[T](code: String)(body: => T) = { + val line = new Line(code, body) + _current = Some(line) + line + } + def running = _current.isDefined + def cancel() = { + current foreach { line => + line.thread.interrupt() + line.cancel() + if (line.runaway) + io.timer(HUNTER_KILLER_DELAY) { if (line.alive) onRunaway(line) } + } + } + } +} |