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.
This commit is contained in:
Mark Harrah 2013-06-26 10:07:32 -04:00
parent b880c5bc16
commit 1048976844
8 changed files with 66 additions and 32 deletions

View File

@ -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)
}
}
}

View File

@ -20,6 +20,7 @@ private object BootConfiguration
val LibraryModuleName = "scala-library"
val JUnitName = "junit"
val JAnsiVersion = "1.11"
val SbtOrg = "org.scala-sbt"

View File

@ -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)
}
}

View File

@ -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

View File

@ -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")
}

View File

@ -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

View File

@ -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() )

View File

@ -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