diff --git a/src/main/scala/sbt/ClasspathUtilities.scala b/src/main/scala/sbt/ClasspathUtilities.scala index ed2ff8d5d..878798002 100644 --- a/src/main/scala/sbt/ClasspathUtilities.scala +++ b/src/main/scala/sbt/ClasspathUtilities.scala @@ -52,7 +52,7 @@ private[sbt] object ClasspathUtilities { url.getProtocol match { - case "file" => new File(url.toURI) :: Nil + case "file" => FileUtilities.toFile(url) :: Nil case "jar" => val path = url.getPath val end = path.indexOf('!') diff --git a/src/main/scala/sbt/DefaultProject.scala b/src/main/scala/sbt/DefaultProject.scala index fefe966e1..7552a1fa1 100644 --- a/src/main/scala/sbt/DefaultProject.scala +++ b/src/main/scala/sbt/DefaultProject.scala @@ -300,8 +300,8 @@ abstract class BasicScalaProject extends ScalaProject with BasicDependencyProjec override def deliverScalaDependencies: Iterable[ModuleID] = { val snapshot = mainDependencies.snapshot - mapScalaModule(snapshot.scalaLibrary, ManageDependencies.ScalaLibraryID) ++ - mapScalaModule(snapshot.scalaCompiler, ManageDependencies.ScalaCompilerID) + mapScalaModule(snapshot.scalaLibrary, ScalaArtifacts.LibraryID) ++ + mapScalaModule(snapshot.scalaCompiler, ScalaArtifacts.CompilerID) } override def watchPaths = mainSources +++ testSources +++ mainResources +++ testResources } @@ -413,7 +413,7 @@ object BasicScalaProject private def mapScalaModule(in: Iterable[_], id: String) = { ScalaVersion.current.toList.flatMap { scalaVersion => - in.map(jar => ModuleID(ManageDependencies.ScalaOrganization, id, scalaVersion)) + in.map(jar => ModuleID(ScalaArtifacts.Organization, id, scalaVersion)) } } } @@ -467,8 +467,8 @@ final class LibraryDependencies(project: Project, conditional: CompileConditiona } private object LibraryDependencies { - private def ScalaLibraryPrefix = ManageDependencies.ScalaLibraryID - private def ScalaCompilerPrefix = ManageDependencies.ScalaCompilerID + private def ScalaLibraryPrefix = ScalaArtifacts.LibraryID + private def ScalaCompilerPrefix = ScalaArtifacts.CompilerID private def ScalaJarPrefixes = List(ScalaCompilerPrefix, ScalaLibraryPrefix) private def isScalaJar(file: File) = ClasspathUtilities.isArchive(file) && ScalaJarPrefixes.exists(isNamed(file)) private def isScalaLibraryJar(file: File) = isNamed(file)(ScalaLibraryPrefix) diff --git a/src/main/scala/sbt/FileUtilities.scala b/src/main/scala/sbt/FileUtilities.scala index a13cefad5..69c70bb99 100644 --- a/src/main/scala/sbt/FileUtilities.scala +++ b/src/main/scala/sbt/FileUtilities.scala @@ -7,7 +7,7 @@ import java.io.{Closeable, File, FileInputStream, FileOutputStream, InputStream, import java.io.{ByteArrayOutputStream, InputStreamReader, OutputStreamWriter} import java.io.{BufferedReader, BufferedWriter, FileReader, FileWriter, Reader, Writer} import java.util.zip.{GZIPInputStream, GZIPOutputStream} -import java.net.URL +import java.net.{URL, URISyntaxException} import java.nio.charset.{Charset, CharsetDecoder, CharsetEncoder} import java.nio.channels.FileChannel import java.util.jar.{Attributes, JarEntry, JarFile, JarInputStream, JarOutputStream, Manifest} @@ -787,11 +787,15 @@ object FileUtilities writer.write(line) writer.write(Newline) } + + def toFile(url: URL) = + try { new File(url.toURI) } + catch { case _: URISyntaxException => new File(url.getPath) } /** The directory in which temporary files are placed.*/ val temporaryDirectory = new File(System.getProperty("java.io.tmpdir")) def classLocation(cl: Class[_]): URL = cl.getProtectionDomain.getCodeSource.getLocation - def classLocationFile(cl: Class[_]): File = new File(classLocation(cl).toURI) + def classLocationFile(cl: Class[_]): File = toFile(classLocation(cl)) def classLocation[T](implicit mf: scala.reflect.Manifest[T]): URL = classLocation(mf.erasure) def classLocationFile[T](implicit mf: scala.reflect.Manifest[T]): File = classLocationFile(mf.erasure) diff --git a/src/main/scala/sbt/LineReader.scala b/src/main/scala/sbt/LineReader.scala index 1332f6aac..614fd117e 100644 --- a/src/main/scala/sbt/LineReader.scala +++ b/src/main/scala/sbt/LineReader.scala @@ -14,21 +14,39 @@ import jline.ConsoleReader abstract class JLine extends LineReader { protected[this] val reader: ConsoleReader - def readLine(prompt: String) = + def readLine(prompt: String) = JLine.withJLine { unsynchronizedReadLine(prompt) } + private[this] def unsynchronizedReadLine(prompt: String) = reader.readLine(prompt) match { case null => None case x => Some(x.trim) } } +private object JLine +{ + def terminal = jline.Terminal.getTerminal + def createReader() = + terminal.synchronized + { + val cr = new ConsoleReader + terminal.enableEcho() + cr.setBellEnabled(false) + cr + } + def withJLine[T](action: => T): T = + { + val t = terminal + t.synchronized + { + t.disableEcho() + try { action } + finally { t.enableEcho() } + } + } +} object SimpleReader extends JLine { - protected[this] val reader = - { - val cr = new ConsoleReader - cr.setBellEnabled(false) - cr - } + protected[this] val reader = JLine.createReader() } class JLineReader(historyPath: Option[Path], completors: Completors, log: Logger) extends JLine { diff --git a/src/main/scala/sbt/Logger.scala b/src/main/scala/sbt/Logger.scala index b637a4499..e319a04e9 100644 --- a/src/main/scala/sbt/Logger.scala +++ b/src/main/scala/sbt/Logger.scala @@ -248,13 +248,12 @@ final class BufferedLogger(delegate: Logger) extends Logger object ConsoleLogger { - private def os = System.getProperty("os.name") - private def isWindows = os.toLowerCase.indexOf("windows") >= 0 - private def formatExplicitlyDisabled = java.lang.Boolean.getBoolean("sbt.log.noformat") - private val formatEnabled = !(formatExplicitlyDisabled || isWindows) + private[this] def formatExplicitlyDisabled = java.lang.Boolean.getBoolean("sbt.log.noformat") + private[this] def ansiSupported = jline.Terminal.getTerminal.isANSISupported + private val formatEnabled = ansiSupported && !formatExplicitlyDisabled } -/** A logger that logs to the console. On non-windows systems, the level labels are +/** A logger that logs to the console. On supported systems, the level labels are * colored. * * This logger is not thread-safe.*/ diff --git a/src/main/scala/sbt/ManageDependencies.scala b/src/main/scala/sbt/ManageDependencies.scala index ba761be89..c6f0f2461 100644 --- a/src/main/scala/sbt/ManageDependencies.scala +++ b/src/main/scala/sbt/ManageDependencies.scala @@ -37,14 +37,17 @@ final class IvyPaths(val projectDirectory: Path, val managedLibDirectory: Path, final class IvyFlags(val validate: Boolean, val addScalaTools: Boolean, val errorIfNoConfiguration: Boolean) extends NotNull final class IvyConfiguration(val paths: IvyPaths, val manager: Manager, val flags: IvyFlags, val ivyScala: Option[IvyScala], val log: Logger) extends NotNull final class UpdateConfiguration(val outputPattern: String, val synchronize: Boolean, val quiet: Boolean) extends NotNull +object ScalaArtifacts +{ + val Organization = "org.scala-lang" + val LibraryID = "scala-library" + val CompilerID = "scala-compiler" +} object ManageDependencies { val DefaultIvyConfigFilename = "ivysettings.xml" val DefaultIvyFilename = "ivy.xml" val DefaultMavenFilename = "pom.xml" - val ScalaOrganization = "org.scala-lang" - val ScalaLibraryID = "scala-library" - val ScalaCompilerID = "scala-compiler" private def defaultIvyFile(project: Path) = project / DefaultIvyFilename private def defaultIvyConfiguration(project: Path) = project / DefaultIvyConfigFilename @@ -280,7 +283,7 @@ object ManageDependencies Control.lazyFold(module.getDependencies.toList) { dep => val id = dep.getDependencyRevisionId - if(id.getOrganisation == ScalaOrganization && id.getRevision != scalaVersion && dep.getModuleConfigurations.exists(configSet.contains)) + if(id.getOrganisation == ScalaArtifacts.Organization && id.getRevision != scalaVersion && dep.getModuleConfigurations.exists(configSet.contains)) Some("Different Scala version specified in dependency ("+ id.getRevision + ") than in project (" + scalaVersion + ").") else None @@ -307,9 +310,9 @@ object ManageDependencies } } def excludeScalaJar(name: String) - { module.addExcludeRule(excludeRule(ScalaOrganization, name, configurationNames)) } - excludeScalaJar(ScalaLibraryID) - excludeScalaJar(ScalaCompilerID) + { module.addExcludeRule(excludeRule(ScalaArtifacts.Organization, name, configurationNames)) } + excludeScalaJar(ScalaArtifacts.LibraryID) + excludeScalaJar(ScalaArtifacts.CompilerID) } private def configureCache(settings: IvySettings) { diff --git a/src/main/scala/sbt/Process.scala b/src/main/scala/sbt/Process.scala index 092eda35c..ac6e106b4 100644 --- a/src/main/scala/sbt/Process.scala +++ b/src/main/scala/sbt/Process.scala @@ -57,12 +57,25 @@ trait ProcessBuilder extends NotNull /** Starts the process represented by this builder, blocks until it exits, and returns the exit code. Standard output and error are * sent to the given Logger.*/ def !(log: Logger): Int + /** Starts the process represented by this builder, blocks until it exits, and returns the exit code. Standard output and error are + * sent to the console. The newly started process reads from standard input of the current process if `connectInput` is true.*/ + def !< : Int + /** Starts the process represented by this builder, blocks until it exits, and returns the exit code. Standard output and error are + * sent to the given Logger. The newly started process reads from standard input of the current process if `connectInput` is true.*/ + def !<(log: Logger) : Int /** Starts the process represented by this builder. Standard output and error are sent to the console.*/ def run(): Process /** Starts the process represented by this builder. Standard output and error are sent to the given Logger.*/ def run(log: Logger): Process /** Starts the process represented by this builder. I/O is handled by the given ProcessIO instance.*/ def run(io: ProcessIO): Process + /** Starts the process represented by this builder. Standard output and error are sent to the console. + * The newly started process reads from standard input of the current process if `connectInput` is true.*/ + def run(connectInput: Boolean): Process + /** Starts the process represented by this builder, blocks until it exits, and returns the exit code. Standard output and error are + * sent to the given Logger. + * The newly started process reads from standard input of the current process if `connectInput` is true.*/ + def run(log: Logger, connectInput: Boolean): Process /** Constructs a command that runs this command first and then `other` if this command succeeds.*/ def #&& (other: ProcessBuilder): ProcessBuilder diff --git a/src/main/scala/sbt/Project.scala b/src/main/scala/sbt/Project.scala index 69fa9f5e1..ac4527d45 100644 --- a/src/main/scala/sbt/Project.scala +++ b/src/main/scala/sbt/Project.scala @@ -376,7 +376,7 @@ object Project if(builderProjectPath.asFile.isDirectory) { val pluginProjectPath = info.builderPath / PluginProjectDirectoryName - val additionalPaths = additional match { case u: URLClassLoader => u.getURLs.map(url => Path.fromFile(new File(url.toURI))); case _ => Nil } + val additionalPaths = additional match { case u: URLClassLoader => u.getURLs.map(url => Path.fromFile(FileUtilities.toFile(url))); case _ => Nil } val builderProject = new BuilderProject(ProjectInfo(builderProjectPath.asFile, Nil, None), pluginProjectPath, additionalPaths, buildLog) builderProject.compile.run.toLeft(()).right.flatMap { ignore => builderProject.projectDefinition.right.map { diff --git a/src/main/scala/sbt/Resources.scala b/src/main/scala/sbt/Resources.scala index b92cd5e73..01b319a89 100644 --- a/src/main/scala/sbt/Resources.scala +++ b/src/main/scala/sbt/Resources.scala @@ -16,7 +16,7 @@ object Resources throw new Exception("Resource base directory '" + basePath + "' not on classpath.") else { - val file = new File(resource.toURI) + val file = toFile(resource) if(file.exists) new Resources(file) else diff --git a/src/main/scala/sbt/Run.scala b/src/main/scala/sbt/Run.scala index f4dbb412e..5662e5d87 100644 --- a/src/main/scala/sbt/Run.scala +++ b/src/main/scala/sbt/Run.scala @@ -55,8 +55,10 @@ object Run extends ScalaRun log.info("") Control.trapUnit("Error during session: ", log) { - val loop = new InterpreterLoop - executeTrapExit(loop.main(settings), log) + JLine.withJLine { + val loop = new InterpreterLoop + executeTrapExit(loop.main(settings), log) + } } } } @@ -128,8 +130,10 @@ object Run extends ScalaRun log.info("") Control.trapUnit("Error during session: ", log) { - val loop = new ProjectInterpreterLoop(compilerSettings, project) - executeTrapExit(loop.main(interpreterSettings), log) + JLine.withJLine { + val loop = new ProjectInterpreterLoop(compilerSettings, project) + executeTrapExit(loop.main(interpreterSettings), log) + } } }} } diff --git a/src/main/scala/sbt/ScalaProject.scala b/src/main/scala/sbt/ScalaProject.scala index 7572c628c..c104b3c37 100644 --- a/src/main/scala/sbt/ScalaProject.scala +++ b/src/main/scala/sbt/ScalaProject.scala @@ -58,7 +58,7 @@ trait SimpleScalaProject extends Project { val command = buildCommand log.debug("Executing command " + command) - val exitValue = command ! log + val exitValue = command.run(log).exitValue() // don't buffer output if(exitValue == 0) None else diff --git a/src/main/scala/sbt/impl/ProcessImpl.scala b/src/main/scala/sbt/impl/ProcessImpl.scala index eb1707671..fc97a25d7 100644 --- a/src/main/scala/sbt/impl/ProcessImpl.scala +++ b/src/main/scala/sbt/impl/ProcessImpl.scala @@ -4,7 +4,7 @@ package sbt import java.lang.{Process => JProcess, ProcessBuilder => JProcessBuilder} -import java.io.{BufferedReader, Closeable, InputStream, InputStreamReader, IOException, OutputStream} +import java.io.{BufferedReader, Closeable, InputStream, InputStreamReader, IOException, OutputStream, PrintStream} import java.io.{PipedInputStream, PipedOutputStream} import java.io.{File, FileInputStream, FileOutputStream} import java.net.URL @@ -25,7 +25,7 @@ private object Spawn private object BasicIO { - def apply(log: Logger) = new ProcessIO(ignoreOut, processFully(log, Level.Info), processFully(log, Level.Error)) + def apply(log: Logger, withIn: Boolean) = new ProcessIO(input(withIn), processFully(log, Level.Info), processFully(log, Level.Error)) def ignoreOut = (i: OutputStream) => () val BufferSize = 8192 @@ -34,9 +34,13 @@ private object BasicIO def processFully(processLine: String => Unit)(i: InputStream) { val reader = new BufferedReader(new InputStreamReader(i)) + processLinesFully(processLine)(reader.readLine) + } + def processLinesFully(processLine: String => Unit)(readLine: () => String) + { def readFully() { - val line = reader.readLine() + val line = readLine() if(line != null) { processLine(line) @@ -45,7 +49,16 @@ private object BasicIO } readFully() } - def standard: ProcessIO = new ProcessIO(ignoreOut, processFully(System.out.println), processFully(System.err.println)) + def connectToIn(o: OutputStream) + { + val printer = new PrintStream(o) + try { processFully {x => printer.println(x); printer.flush() }(System.in) } + catch { case _: InterruptedException => () } + finally { printer.close() } + } + def input(connect: Boolean): OutputStream => Unit = if(connect) connectToIn else ignoreOut + def standard(connectInput: Boolean): ProcessIO = standard(input(connectInput)) + def standard(in: OutputStream => Unit): ProcessIO = new ProcessIO(in, processFully(System.out.println), processFully(System.err.println)) def transferFully(in: InputStream, out: OutputStream) { @@ -81,15 +94,20 @@ private abstract class AbstractProcessBuilder extends ProcessBuilder def #> (f: File): ProcessBuilder = new PipedProcessBuilder(this, new FileOutput(f, false), false) def #>> (f: File): ProcessBuilder = new PipedProcessBuilder(this, new FileOutput(f, true), true) - def run(): Process = run(BasicIO.standard) - def run(log: Logger): Process = run(BasicIO(log)) + def run(): Process = run(false) + def run(connectInput: Boolean): Process = run(BasicIO.standard(connectInput)) + def run(log: Logger): Process = run(log, false) + def run(log: Logger, connectInput: Boolean): Process = run(BasicIO(log, connectInput)) - def ! = run().exitValue() - def !(log: Logger) = + def ! = run(false).exitValue() + def !< = run(true).exitValue() + def !(log: Logger) = runBuffered(log, false) + def !<(log: Logger) = runBuffered(log, true) + private[this] def runBuffered(log: Logger, connectInput: Boolean) = { val log2 = new BufferedLogger(log) log2.startRecording() - try { run(log2).exitValue() } + try { run(log2, connectInput).exitValue() } finally { log2.playAll(); log2.clearAll() } } def !(io: ProcessIO) = run(io).exitValue() @@ -320,19 +338,23 @@ private[sbt] class SimpleProcessBuilder(p: JProcessBuilder) extends AbstractProc Spawn(processError(process.getErrorStream)) :: Nil else Nil - new SimpleProcess(process, inThread :: outThread :: errorThread) + new SimpleProcess(process, inThread, outThread :: errorThread) } override def toString = p.command.toString override def canPipeTo = true } -/** A thin wrapper around a java.lang.Process. `ioThreads` are the Threads created to do I/O. -* The implementation of `exitValue` waits until these threads die before returning. */ -private class SimpleProcess(p: JProcess, ioThreads: Iterable[Thread]) extends Process +/** A thin wrapper around a java.lang.Process. `outputThreads` are the Threads created to read from the +* output and error streams of the process. `inputThread` is the Thread created to write to the input stream of +* the process. +* The implementation of `exitValue` interrupts `inputThread` and then waits until all I/O threads die before +* returning. */ +private class SimpleProcess(p: JProcess, inputThread: Thread, outputThreads: List[Thread]) extends Process { override def exitValue() = { p.waitFor() // wait for the process to terminate - ioThreads.foreach(_.join()) // this ensures that all output is complete before returning (waitFor does not ensure this) + inputThread.interrupt() // we interrupt the input thread to notify it that it can terminate + (inputThread :: outputThreads).foreach(_.join()) // this ensures that all output is complete before returning (waitFor does not ensure this) p.exitValue() } override def destroy() = p.destroy() diff --git a/src/sbt-test/compiler-project/run-test/src/main/scala/Foo.scala b/src/sbt-test/compiler-project/run-test/src/main/scala/Foo.scala index 871a9d9b7..4f0c03709 100644 --- a/src/sbt-test/compiler-project/run-test/src/main/scala/Foo.scala +++ b/src/sbt-test/compiler-project/run-test/src/main/scala/Foo.scala @@ -6,7 +6,7 @@ import scala.tools.nsc.{Interpreter, Settings} class Foo { val settings = new Settings() - settings.classpath.value = new java.io.File(classOf[Holder].getProtectionDomain.getCodeSource.getLocation.toURI).getAbsolutePath + settings.classpath.value = FileUtilities.toFile(classOf[Holder].getProtectionDomain.getCodeSource.getLocation).getAbsolutePath val inter = new Interpreter(settings) def eval(code: String): Any = { @@ -24,4 +24,4 @@ object Test val foo = new Foo foo.eval("3") } -} \ No newline at end of file +} diff --git a/src/test/scala/sbt/ProcessSpecification.scala b/src/test/scala/sbt/ProcessSpecification.scala index 61d530555..5dcbcf67a 100644 --- a/src/test/scala/sbt/ProcessSpecification.scala +++ b/src/test/scala/sbt/ProcessSpecification.scala @@ -86,6 +86,6 @@ object ProcessSpecification extends Properties("Process I/O") "java -cp " + thisClasspath + " " + command } private def getSource[T](implicit mf: scala.reflect.Manifest[T]): String = - (new File(mf.erasure.getProtectionDomain.getCodeSource.getLocation.toURI)).getAbsolutePath + (FileUtilities.toFile(mf.erasure.getProtectionDomain.getCodeSource.getLocation)).getAbsolutePath } private trait SourceTag \ No newline at end of file