From 10489768440d66a231f7eed950fc9f4b21912502 Mon Sep 17 00:00:00 2001 From: Mark Harrah Date: Wed, 26 Jun 2013 10:07:32 -0400 Subject: [PATCH] jline/jansi fixes for windows. Fixes #763, fixes #562. The startup script should set sbt.cygwin=true if running from cygwin. This will set the terminal type properly for JLine if not already set. If sbt.cygwin=false or unset and os.name includes "windows", JAnsi is downloaded by the launcher and installed on standard out/err. The value for jline.terminal is transformed from explicit jline.X to the basic types "windows", "unix", or "none". Now that sbt uses JLine 2.0, these types are understood by both sbt's JLine and Scala's. Older Scala versions shaded the classes but not the terminal property so both couldn't be configured with a class name at the same time. --- launch/src/main/scala/xsbt/boot/Boot.scala | 16 ---------- .../scala/xsbt/boot/BootConfiguration.scala | 1 + launch/src/main/scala/xsbt/boot/JAnsi.scala | 24 +++++++++++++++ launch/src/main/scala/xsbt/boot/Launch.scala | 30 +++++++++++-------- launch/src/main/scala/xsbt/boot/Pre.scala | 2 ++ project/Sbt.scala | 4 +-- project/Util.scala | 3 +- .../src/main/scala/sbt/LineReader.scala | 18 +++++++++++ 8 files changed, 66 insertions(+), 32 deletions(-) create mode 100644 launch/src/main/scala/xsbt/boot/JAnsi.scala diff --git a/launch/src/main/scala/xsbt/boot/Boot.scala b/launch/src/main/scala/xsbt/boot/Boot.scala index ed60e0027..afc70c2d7 100644 --- a/launch/src/main/scala/xsbt/boot/Boot.scala +++ b/launch/src/main/scala/xsbt/boot/Boot.scala @@ -17,7 +17,6 @@ object Boot System.clearProperty("scala.home") // avoid errors from mixing Scala versions in the same JVM System.setProperty("jline.shutdownhook", "false") CheckProxy() - initJansi() run(args) } } @@ -47,19 +46,4 @@ object Boot } private def exit(code: Int): Nothing = System.exit(code).asInstanceOf[Nothing] - - private def initJansi() { - try { - val c = Class.forName("org.fusesource.jansi.AnsiConsole") - c.getMethod("systemInstall").invoke(null) - } catch { - case ignore: ClassNotFoundException => - /* The below code intentionally traps everything. It technically shouldn't trap the - * non-StackOverflowError VirtualMachineErrors and AWTError would be weird, but this is PermGen - * mitigation code that should not render sbt completely unusable if jansi initialization fails. - * [From Mark Harrah, https://github.com/sbt/sbt/pull/633#issuecomment-11957578]. - */ - case ex: Throwable => println("Jansi found on class path but initialization failed: " + ex) - } - } } diff --git a/launch/src/main/scala/xsbt/boot/BootConfiguration.scala b/launch/src/main/scala/xsbt/boot/BootConfiguration.scala index e64af227c..5170b61fa 100644 --- a/launch/src/main/scala/xsbt/boot/BootConfiguration.scala +++ b/launch/src/main/scala/xsbt/boot/BootConfiguration.scala @@ -20,6 +20,7 @@ private object BootConfiguration val LibraryModuleName = "scala-library" val JUnitName = "junit" + val JAnsiVersion = "1.11" val SbtOrg = "org.scala-sbt" diff --git a/launch/src/main/scala/xsbt/boot/JAnsi.scala b/launch/src/main/scala/xsbt/boot/JAnsi.scala new file mode 100644 index 000000000..e9c6a5ff0 --- /dev/null +++ b/launch/src/main/scala/xsbt/boot/JAnsi.scala @@ -0,0 +1,24 @@ +package xsbt.boot + + import Pre._ + +object JAnsi +{ + def uninstall(loader: ClassLoader): Unit = callJAnsi("systemUninstall", loader) + def install(loader: ClassLoader): Unit = callJAnsi("systemInstall", loader) + + private[this] def callJAnsi(methodName: String, loader: ClassLoader): Unit = if(isWindows && !isCygwin) callJAnsiMethod(methodName, loader) + private[this] def callJAnsiMethod(methodName: String, loader: ClassLoader): Unit = + try { + val c = Class.forName("org.fusesource.jansi.AnsiConsole", true, loader) + c.getMethod(methodName).invoke(null) + } catch { + case ignore: ClassNotFoundException => + /* The below code intentionally traps everything. It technically shouldn't trap the + * non-StackOverflowError VirtualMachineErrors and AWTError would be weird, but this is PermGen + * mitigation code that should not render sbt completely unusable if jansi initialization fails. + * [From Mark Harrah, https://github.com/sbt/sbt/pull/633#issuecomment-11957578]. + */ + case ex: Throwable => println("Jansi found on class path but initialization failed: " + ex) + } +} \ No newline at end of file diff --git a/launch/src/main/scala/xsbt/boot/Launch.scala b/launch/src/main/scala/xsbt/boot/Launch.scala index 6144646d1..0359960e0 100644 --- a/launch/src/main/scala/xsbt/boot/Launch.scala +++ b/launch/src/main/scala/xsbt/boot/Launch.scala @@ -4,7 +4,7 @@ package xsbt.boot import Pre._ -import BootConfiguration.{CompilerModuleName, LibraryModuleName} +import BootConfiguration.{CompilerModuleName, JAnsiVersion, LibraryModuleName} import java.io.File import java.net.{URL, URLClassLoader} import java.util.concurrent.Callable @@ -51,9 +51,14 @@ object Launch val appProvider: xsbti.AppProvider = launcher.app(app, orNull(scalaVersion)) // takes ~40 ms when no update is required val appConfig: xsbti.AppConfiguration = new AppConfiguration(toArray(arguments), workingDirectory, appProvider) - val main = appProvider.newMain() - try { withContextLoader(appProvider.loader)(main.run(appConfig)) } - catch { case e: xsbti.FullReload => if(e.clean) delete(launcher.bootDirectory); throw e } + JAnsi.install(launcher.topLoader) + try { + val main = appProvider.newMain() + try { withContextLoader(appProvider.loader)(main.run(appConfig)) } + catch { case e: xsbti.FullReload => if(e.clean) delete(launcher.bootDirectory); throw e } + } finally { + JAnsi.uninstall(launcher.topLoader) + } } final def launch(run: RunConfiguration => xsbti.MainResult)(config: RunConfiguration): Option[Int] = { @@ -88,7 +93,7 @@ class Launch private[xsbt](val bootDirectory: File, val lockBoot: Boolean, val i getAppProvider(id, scalaVersion, false) val bootLoader = new BootFilteredLoader(getClass.getClassLoader) - val topLoader = jnaLoader(bootLoader) + val topLoader = if(isWindows && !isCygwin) jansiLoader(bootLoader) else bootLoader val updateLockFile = if(lockBoot) Some(new File(bootDirectory, "sbt.boot.lock")) else None @@ -99,19 +104,20 @@ class Launch private[xsbt](val bootDirectory: File, val lockBoot: Boolean, val i def isOverrideRepositories: Boolean = ivyOptions.isOverrideRepositories def checksums = checksumsList.toArray[String] - def jnaLoader(parent: ClassLoader): ClassLoader = + // JAnsi needs to be shared between Scala and the application so there aren't two competing versions + def jansiLoader(parent: ClassLoader): ClassLoader = { - val id = AppID("net.java.dev.jna", "jna", "3.2.3", "", toArray(Nil), xsbti.CrossValue.Disabled, array()) + val id = AppID("org.fusesource.jansi", "jansi", JAnsiVersion, "", toArray(Nil), xsbti.CrossValue.Disabled, array()) val configuration = makeConfiguration(ScalaOrg, None) - val jnaHome = appDirectory(new File(bootDirectory, baseDirectoryName(ScalaOrg, None)), id) - val module = appModule(id, None, false, "jna") + val jansiHome = appDirectory(new File(bootDirectory, baseDirectoryName(ScalaOrg, None)), id) + val module = appModule(id, None, false, "jansi") def makeLoader(): ClassLoader = { - val urls = toURLs(wrapNull(jnaHome.listFiles(JarFilter))) + val urls = toURLs(wrapNull(jansiHome.listFiles(JarFilter))) val loader = new URLClassLoader(urls, bootLoader) - checkLoader(loader, module, "com.sun.jna.Function" :: Nil, loader) + checkLoader(loader, module, "org.fusesource.jansi.internal.WindowsSupport" :: Nil, loader) } val existingLoader = - if(jnaHome.exists) + if(jansiHome.exists) try Some(makeLoader()) catch { case e: Exception => None } else None diff --git a/launch/src/main/scala/xsbt/boot/Pre.scala b/launch/src/main/scala/xsbt/boot/Pre.scala index 011f6d2f6..73440d745 100644 --- a/launch/src/main/scala/xsbt/boot/Pre.scala +++ b/launch/src/main/scala/xsbt/boot/Pre.scala @@ -79,4 +79,6 @@ object Pre } if(f.exists) f.delete() } + final val isWindows: Boolean = System.getProperty("os.name").toLowerCase.contains("windows") + final val isCygwin: Boolean = isWindows && java.lang.Boolean.getBoolean("sbt.cygwin") } diff --git a/project/Sbt.scala b/project/Sbt.scala index bc0d96948..0e359c164 100644 --- a/project/Sbt.scala +++ b/project/Sbt.scala @@ -62,9 +62,9 @@ object Sbt extends Build // Utilities related to reflection, managing Scala versions, and custom class loaders lazy val classpathSub = testedBaseProject(utilPath / "classpath", "Classpath") dependsOn(launchInterfaceSub, interfaceSub, ioSub) settings(scalaCompiler) // Command line-related utilities. - lazy val completeSub = testedBaseProject(utilPath / "complete", "Completion") dependsOn(collectionSub, controlSub, ioSub) settings(jline : _*) + lazy val completeSub = testedBaseProject(utilPath / "complete", "Completion") dependsOn(collectionSub, controlSub, ioSub) settings(jline) // logging - lazy val logSub = testedBaseProject(utilPath / "log", "Logging") dependsOn(interfaceSub, processSub) settings(jline : _*) + lazy val logSub = testedBaseProject(utilPath / "log", "Logging") dependsOn(interfaceSub, processSub) settings(jline) // Relation lazy val relationSub = testedBaseProject(utilPath / "relation", "Relation") dependsOn(interfaceSub, processSub) // class file reader and analyzer diff --git a/project/Util.scala b/project/Util.scala index 230deff20..71c85f385 100644 --- a/project/Util.scala +++ b/project/Util.scala @@ -156,8 +156,7 @@ object Common { def lib(m: ModuleID) = libraryDependencies += m lazy val jlineDep = "jline" % "jline" % "2.11" - lazy val jansiDep = "org.fusesource.jansi" % "jansi" % "1.9" // jline pom doesn't explicitly declare it? - lazy val jline = Seq(lib(jlineDep), lib(jansiDep)) + lazy val jline = lib(jlineDep) lazy val ivy = lib("org.apache.ivy" % "ivy" % "2.3.0-rc1") lazy val httpclient = lib("commons-httpclient" % "commons-httpclient" % "3.1") lazy val jsch = lib("com.jcraft" % "jsch" % "0.1.46" intransitive() ) diff --git a/util/complete/src/main/scala/sbt/LineReader.scala b/util/complete/src/main/scala/sbt/LineReader.scala index 6d81a2391..e3d3df0f0 100644 --- a/util/complete/src/main/scala/sbt/LineReader.scala +++ b/util/complete/src/main/scala/sbt/LineReader.scala @@ -64,6 +64,24 @@ abstract class JLine extends LineReader } private object JLine { + private[this] val TerminalProperty = "jline.terminal" + + fixTerminalProperty() + + // translate explicit class names to type in order to support + // older Scala, since it shaded classes but not the system property + private[sbt] def fixTerminalProperty() { + val newValue = System.getProperty(TerminalProperty) match { + case "jline.UnixTerminal" => "unix" + case null if System.getProperty("sbt.cygwin") != null => "unix" + case "jline.WindowsTerminal" => "windows" + case "jline.AnsiWindowsTerminal" => "windows" + case "jline.UnsupportedTerminal" => "none" + case x => x + } + if(newValue != null) System.setProperty(TerminalProperty, newValue) + } + // When calling this, ensure that enableEcho has been or will be called. // TerminalFactory.get will initialize the terminal to disable echo. private def terminal = jline.TerminalFactory.get