From 363a1456f671323b35dcacf2c8b8eb39180b8a53 Mon Sep 17 00:00:00 2001 From: Paul Phillips Date: Wed, 17 Nov 2010 01:00:57 +0000 Subject: Two annoying REPL things made less annoying: * ctrl-C will no longer kill the repl unless you hit it again * ctrl-Z will no longer make the repl useless because of jline In the service of the first I wrote signal handling code, which we can put to use in other ways as well. No review. --- .../tools/nsc/interpreter/InteractiveReader.scala | 12 ++- .../scala/tools/nsc/interpreter/JLineReader.scala | 12 ++- src/compiler/scala/tools/nsc/io/package.scala | 10 +++ src/compiler/scala/tools/util/SignalManager.scala | 97 ++++++++++++++++++++++ 4 files changed, 126 insertions(+), 5 deletions(-) create mode 100644 src/compiler/scala/tools/util/SignalManager.scala diff --git a/src/compiler/scala/tools/nsc/interpreter/InteractiveReader.scala b/src/compiler/scala/tools/nsc/interpreter/InteractiveReader.scala index 74b3c6ee48..7e4abd1d76 100644 --- a/src/compiler/scala/tools/nsc/interpreter/InteractiveReader.scala +++ b/src/compiler/scala/tools/nsc/interpreter/InteractiveReader.scala @@ -5,19 +5,24 @@ package scala.tools.nsc package interpreter + +import java.io.IOException +import java.nio.channels.ClosedByInterruptException import scala.util.control.Exception._ +import InteractiveReader._ /** Reads lines from an input stream */ trait InteractiveReader { - import InteractiveReader._ - import java.io.IOException protected def readOneLine(prompt: String): String val interactive: Boolean + def init(): Unit = () def readLine(prompt: String): String = { def handler: Catcher[String] = { - case e: IOException if restartSystemCall(e) => readLine(prompt) + case e: ClosedByInterruptException => error("Reader closed by interrupt.") + // Terminal has to be re-initialized after SIGSTP or up arrow etc. stop working. + case e: IOException if restartSystemCall(e) => init() ; readLine(prompt) } catching(handler) { readOneLine(prompt) } } @@ -34,7 +39,6 @@ trait InteractiveReader { Properties.isMac && (e.getMessage == msgEINTR) } - object InteractiveReader { val msgEINTR = "Interrupted system call" def createDefault(): InteractiveReader = createDefault(null) diff --git a/src/compiler/scala/tools/nsc/interpreter/JLineReader.scala b/src/compiler/scala/tools/nsc/interpreter/JLineReader.scala index ab11a53d43..2fe436fd2f 100644 --- a/src/compiler/scala/tools/nsc/interpreter/JLineReader.scala +++ b/src/compiler/scala/tools/nsc/interpreter/JLineReader.scala @@ -8,13 +8,23 @@ package interpreter import java.io.File import jline.{ ConsoleReader, ArgumentCompletor, History => JHistory } +import scala.tools.util.SignalManager /** Reads from the console using JLine */ class JLineReader(interpreter: Interpreter) extends InteractiveReader { def this() = this(null) - override lazy val history = Some(History(consoleReader)) + override lazy val history = Some(History(consoleReader)) override lazy val completion = Option(interpreter) map (x => new Completion(x)) + override def init() = consoleReader.getTerminal().initializeTerminal() + + /** Requires two interrupt signals within three seconds + * of one another to initiate exit. + */ + SignalManager.requireInterval(3, SignalManager.INT) { + case true => Console.println("\nPress ctrl-C again to exit.") + case false => System.exit(1) + } val consoleReader = { val r = new jline.ConsoleReader() diff --git a/src/compiler/scala/tools/nsc/io/package.scala b/src/compiler/scala/tools/nsc/io/package.scala index 2016fb5518..1d484a6c0e 100644 --- a/src/compiler/scala/tools/nsc/io/package.scala +++ b/src/compiler/scala/tools/nsc/io/package.scala @@ -6,6 +6,7 @@ package scala.tools.nsc import java.util.concurrent.{ Future, Callable, Executors } +import java.util.{ Timer, TimerTask } package object io { def runnable(body: => Unit): Runnable = new Runnable { override def run() = body } @@ -24,4 +25,13 @@ package object io { thread.start thread } + + // Set a timer to execute the given code. + def timer(seconds: Int)(body: => Unit): Timer = { + val alarm = new Timer(true) // daemon + val tt = new TimerTask { def run() = body } + + alarm.schedule(tt, seconds * 1000) + alarm + } } \ No newline at end of file diff --git a/src/compiler/scala/tools/util/SignalManager.scala b/src/compiler/scala/tools/util/SignalManager.scala new file mode 100644 index 0000000000..a6513ecb50 --- /dev/null +++ b/src/compiler/scala/tools/util/SignalManager.scala @@ -0,0 +1,97 @@ +/* NSC -- new Scala compiler + * Copyright 2005-2010 LAMP/EPFL + * @author Paul Phillips + */ + +package scala.tools +package util + +import sun.misc.{ Signal, SignalHandler } +import SignalHandler._ +import nsc.io.timer + +/** Unofficial signal handling code. According to sun it's unsupported, + * but it's too useful not to take advantage of. Degrade gracefully. + */ +class SignalManager { + def apply(name: String): SignalWrapper = + try { new SignalWrapper(new Signal(name)) } + catch { case x: IllegalArgumentException => new SignalError(x.getMessage) } + + class ChainedHandler(prev: SignalHandler, current: SignalHandler) extends SignalHandler { + def handle(sig: Signal): Unit = { + current handle sig + if (prev != SIG_DFL && prev != SIG_IGN) + prev handle sig + } + } + class SignalWrapper(val signal: Signal) { + def name = signal.getName + def add(body: => Unit) = { + val handler = new SignalHandler { def handle(sig: Signal) = body } + val prev = Signal.handle(signal, handler) + + new ChainedHandler(prev, handler) + } + override def toString = "SIG" + name + } + class SignalError(message: String) extends SignalWrapper(null) { + override def toString = message + } +} + +object SignalManager extends SignalManager { + private implicit def mkSignalWrapper(name: String): SignalWrapper = this(name) + + def HUP: SignalWrapper = "HUP" + def INT: SignalWrapper = "INT" + def QUIT: SignalWrapper = "QUIT" + def ILL: SignalWrapper = "ILL" + def TRAP: SignalWrapper = "TRAP" + def ABRT: SignalWrapper = "ABRT" + def EMT: SignalWrapper = "EMT" + def FPE: SignalWrapper = "FPE" + def KILL: SignalWrapper = "KILL" + def BUS: SignalWrapper = "BUS" + def SEGV: SignalWrapper = "SEGV" + def SYS: SignalWrapper = "SYS" + def PIPE: SignalWrapper = "PIPE" + def ALRM: SignalWrapper = "ALRM" + def TERM: SignalWrapper = "TERM" + def URG: SignalWrapper = "URG" + def STOP: SignalWrapper = "STOP" + def TSTP: SignalWrapper = "TSTP" + def CONT: SignalWrapper = "CONT" + def CHLD: SignalWrapper = "CHLD" + def TTIN: SignalWrapper = "TTIN" + def TTOU: SignalWrapper = "TTOU" + def IO: SignalWrapper = "IO" + def XCPU: SignalWrapper = "XCPU" + def XFSZ: SignalWrapper = "XFSZ" + def VTALRM: SignalWrapper = "VTALRM" + def PROF: SignalWrapper = "PROF" + def WINCH: SignalWrapper = "WINCH" + def INFO: SignalWrapper = "INFO" + def USR1: SignalWrapper = "USR1" + def USR2: SignalWrapper = "USR2" + + /** Given a number of seconds, a signal, and a function: sets up a handler which upon + * receiving the signal once, calls the function with argument true, and if the + * signal is received again within the allowed time, calls it with argument false. + * (Otherwise it calls it with true and starts the timer over again.) + */ + def requireInterval(seconds: Int, wrapper: SignalWrapper)(fn: Boolean => Unit) = { + var received = false + wrapper add { + if (received) fn(false) + else { + received = true + fn(true) + timer(seconds)(received = false) + } + } + } +} + + + -- cgit v1.2.3