Skip to content

Commit

Permalink
Use JNI rather than PowerShell to do Windows system calls
Browse files Browse the repository at this point in the history
  • Loading branch information
alexarchambault committed Apr 27, 2021
1 parent 77dce32 commit 590036b
Show file tree
Hide file tree
Showing 12 changed files with 147 additions and 36 deletions.
2 changes: 1 addition & 1 deletion .gitmodules
Expand Up @@ -3,7 +3,7 @@
url = https://github.com/coursier/test-metadata.git
[submodule "modules/directories"]
path = modules/directories
url = https://github.com/dirs-dev/directories-jvm.git
url = https://github.com/coursier/directories-jvm.git
[submodule "modules/tests/handmade-metadata"]
path = modules/tests/handmade-metadata
url = https://github.com/coursier/handmade-metadata.git
Expand Down
15 changes: 12 additions & 3 deletions build.sbt
Expand Up @@ -130,7 +130,10 @@ lazy val paths = project("paths")
.settings(
pureJava,
dontPublish,
addDirectoriesSources
addDirectoriesSources,
libraryDependencies ++= Seq(
Deps.jniUtils
)
)

lazy val cache = crossProject("cache")(JSPlatform, JVMPlatform)
Expand All @@ -143,6 +146,7 @@ lazy val cache = crossProject("cache")(JSPlatform, JVMPlatform)
addPathsSources,
Mima.previousArtifacts,
libraryDependencies ++= Seq(
Deps.jniUtils,
Deps.svm % Provided,
Deps.windowsAnsi
),
Expand Down Expand Up @@ -230,7 +234,8 @@ lazy val `bootstrap-launcher` = project("bootstrap-launcher")
utest,
libraryDependencies ++= Seq(
Deps.collectionCompat % Test,
Deps.java8Compat % Test
Deps.java8Compat % Test,
Deps.jniUtils
),
addPathsSources,
addWindowsAnsiPsSources,
Expand All @@ -244,6 +249,9 @@ lazy val `resources-bootstrap-launcher` = project("resources-bootstrap-launcher"
.settings(
pureJava,
dontPublish,
libraryDependencies ++= Seq(
Deps.jniUtils
),
unmanagedSourceDirectories.in(Compile) ++= unmanagedSourceDirectories.in(`bootstrap-launcher`, Compile).value,
mainClass.in(Compile) := Some("coursier.bootstrap.launcher.ResourcesLauncher"),
proguardedBootstrap("coursier.bootstrap.launcher.ResourcesLauncher", resourceBased = true)
Expand Down Expand Up @@ -324,7 +332,8 @@ lazy val env = project("env")
libs ++= Seq(
Deps.collectionCompat,
Deps.dataClass % Provided,
Deps.jimfs % Test
Deps.jimfs % Test,
Deps.jniUtils
),
utest
)
Expand Down
Expand Up @@ -16,17 +16,29 @@ private static void exit(String message) {
}

private static void maybeInitWindowsAnsi() throws InterruptedException, IOException {
if (!System.getProperty("coursier.bootstrap.windows-ansi", "").equalsIgnoreCase("false")) {
try {
// noop on Linux / macOS

boolean isWindows = System.getProperty("os.name")
.toLowerCase(java.util.Locale.ROOT)
.contains("windows");

if (!isWindows)
return;

if (System.getProperty("coursier.bootstrap.windows-ansi", "").equalsIgnoreCase("false"))
return;

boolean useJni = coursier.paths.Util.useJni();
try {
if (useJni)
coursier.jniutils.WindowsAnsiTerminal.enableAnsiOutput();
else
io.github.alexarchambault.windowsansi.WindowsAnsiPs.setup();
} catch (InterruptedException | IOException e) {
boolean doThrow = Boolean.getBoolean("coursier.bootstrap.windows-ansi.throw-exception");
if (doThrow || Boolean.getBoolean("coursier.bootstrap.windows-ansi.verbose"))
System.err.println("Error setting up Windows terminal for ANSI escape codes: " + e);
if (doThrow)
throw e;
}
} catch (InterruptedException | IOException e) {
boolean doThrow = Boolean.getBoolean("coursier.bootstrap.windows-ansi.throw-exception");
if (doThrow || Boolean.getBoolean("coursier.bootstrap.windows-ansi.verbose"))
System.err.println("Error setting up Windows terminal for ANSI escape codes: " + e);
if (doThrow)
throw e;
}
}

Expand Down
Expand Up @@ -65,10 +65,17 @@ object Terminal {
private lazy val isWindows = System.getProperty("os.name").toLowerCase(java.util.Locale.ROOT).contains("windows")

private def fromJLine(): Option[(Int, Int)] =
if (isWindows) {
val size = io.github.alexarchambault.windowsansi.WindowsAnsi.terminalSize()
Some((size.getWidth, size.getHeight))
} else
if (isWindows)
Some {
if (coursier.paths.Util.useJni()) {
val size = coursier.jniutils.WindowsAnsiTerminal.terminalSize()
(size.getWidth, size.getHeight)
} else {
val size = io.github.alexarchambault.windowsansi.WindowsAnsi.terminalSize()
(size.getWidth, size.getHeight)
}
}
else
None

def consoleDims(): (Int, Int) =
Expand Down
13 changes: 9 additions & 4 deletions modules/cli/src/main/scala/coursier/cli/Coursier.scala
Expand Up @@ -23,7 +23,6 @@ import coursier.cli.search.Search
import coursier.core.Version
import coursier.install.InstallDir
import coursier.launcher.internal.{FileUtil, Windows}
import io.github.alexarchambault.windowsansi.WindowsAnsi
import shapeless._

import scala.util.control.NonFatal
Expand All @@ -32,16 +31,22 @@ object Coursier extends CommandAppPreA(Parser[LauncherOptions], Help[LauncherOpt

val isGraalvmNativeImage = sys.props.contains("org.graalvm.nativeimage.imagecode")

if (System.console() != null && Windows.isWindows)
try WindowsAnsi.setup()
catch {
if (System.console() != null && Windows.isWindows) {
val useJni = coursier.paths.Util.useJni()
try {
if (useJni)
coursier.jniutils.WindowsAnsiTerminal.enableAnsiOutput()
else
io.github.alexarchambault.windowsansi.WindowsAnsi.setup()
} catch {
case NonFatal(e) =>
val doThrow = java.lang.Boolean.getBoolean("coursier.windows-ansi.throw-exception")
if (doThrow || java.lang.Boolean.getBoolean("coursier.windows-ansi.verbose"))
System.err.println(s"Error setting up Windows terminal for ANSI escape codes: $e")
if (doThrow)
throw e
}
}

CacheUrl.setupProxyAuth()

Expand Down
Expand Up @@ -18,7 +18,7 @@ final case class EnvParams(
// TODO Allow to customize some parameters of WindowsEnvVarUpdater / ProfileUpdater?
def envVarUpdater: Either[WindowsEnvVarUpdater, ProfileUpdater] =
if (Windows.isWindows)
Left(WindowsEnvVarUpdater())
Left(WindowsEnvVarUpdater().withUseJni(Some(coursier.paths.Util.useJni())))
else
Right(
ProfileUpdater()
Expand Down
2 changes: 1 addition & 1 deletion modules/directories
35 changes: 25 additions & 10 deletions modules/env/src/main/scala/coursier/env/WindowsEnvVarUpdater.scala
Expand Up @@ -4,26 +4,41 @@ import dataclass.data

@data class WindowsEnvVarUpdater(
powershellRunner: PowershellRunner = PowershellRunner(),
target: String = "User"
target: String = "User",
useJni: Option[Boolean] = None
) extends EnvVarUpdater {

private lazy val useJni0 = useJni.getOrElse {
// FIXME Should be coursier.paths.Util.useJni(), but it's not available from here.
!System.getProperty("coursier.jni", "").equalsIgnoreCase("false")
}

// https://stackoverflow.com/questions/9546324/adding-directory-to-path-environment-variable-in-windows/29109007#29109007
// https://docs.microsoft.com/fr-fr/dotnet/api/system.environment.getenvironmentvariable?view=netframework-4.8#System_Environment_GetEnvironmentVariable_System_String_System_EnvironmentVariableTarget_
// https://docs.microsoft.com/fr-fr/dotnet/api/system.environment.setenvironmentvariable?view=netframework-4.8#System_Environment_SetEnvironmentVariable_System_String_System_String_System_EnvironmentVariableTarget_

private def getEnvironmentVariable(name: String): Option[String] = {
val output = powershellRunner.runScript(WindowsEnvVarUpdater.getEnvVarScript(name)).stripSuffix(System.lineSeparator())
if (output == "null") // if ever the actual value is "null", we'll miss it
None
else
Some(output)
}
private def getEnvironmentVariable(name: String): Option[String] =
if (useJni0)
Option(coursier.jniutils.WindowsEnvironmentVariables.get(name))
else {
val output = powershellRunner.runScript(WindowsEnvVarUpdater.getEnvVarScript(name)).stripSuffix(System.lineSeparator())
if (output == "null") // if ever the actual value is "null", we'll miss it
None
else
Some(output)
}

private def setEnvironmentVariable(name: String, value: String): Unit =
powershellRunner.runScript(WindowsEnvVarUpdater.setEnvVarScript(name, value))
if (useJni0)
coursier.jniutils.WindowsEnvironmentVariables.set(name, value)
else
powershellRunner.runScript(WindowsEnvVarUpdater.setEnvVarScript(name, value))

private def clearEnvironmentVariable(name: String): Unit =
powershellRunner.runScript(WindowsEnvVarUpdater.clearEnvVarScript(name))
if (useJni0)
coursier.jniutils.WindowsEnvironmentVariables.delete(name)
else
powershellRunner.runScript(WindowsEnvVarUpdater.clearEnvVarScript(name))

def applyUpdate(update: EnvironmentUpdate): Boolean = {

Expand Down
14 changes: 13 additions & 1 deletion modules/paths/src/main/java/coursier/paths/CoursierPaths.java
Expand Up @@ -4,6 +4,7 @@
import java.io.IOException;
import java.nio.file.Files;

import dev.dirs.GetWinDirs;
import dev.dirs.ProjectDirectories;

/**
Expand Down Expand Up @@ -87,7 +88,18 @@ private static ProjectDirectories coursierDirectories() throws IOException {
if (coursierDirectories0 == null)
synchronized (coursierDirectoriesLock) {
if (coursierDirectories0 == null) {
coursierDirectories0 = ProjectDirectories.from(null, null, "Coursier");
GetWinDirs getWinDirs;
if (coursier.paths.Util.useJni())
getWinDirs = guids -> {
String[] dirs = new String[guids.length];
for (int i = 0; i < guids.length; i++) {
dirs[i] = coursier.jniutils.WindowsKnownFolders.knownFolderPath("{" + guids[i] + "}");
}
return dirs;
};
else
getWinDirs = GetWinDirs.powerShellBased;
coursierDirectories0 = ProjectDirectories.from(null, null, "Coursier", getWinDirs);
}
}

Expand Down
44 changes: 44 additions & 0 deletions modules/paths/src/main/java/coursier/paths/Util.java
Expand Up @@ -5,6 +5,7 @@
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.LinkedHashMap;
import java.util.Locale;
import java.util.Map;
import java.util.Properties;
import java.util.regex.Matcher;
Expand Down Expand Up @@ -150,4 +151,47 @@ public static boolean useAnsiOutput() {
}
return useAnsiOutput0;
}

private static Boolean useJni0 = null;
public static boolean useJni() {
if (useJni0 != null)
return useJni0;

boolean isWindows = System.getProperty("os.name")
.toLowerCase(Locale.ROOT)
.contains("windows");
if (!isWindows) {
useJni0 = false;
return useJni0;
}

String prop = System.getenv("COURSIER_JNI");
if (prop == null || prop.isEmpty())
prop = System.getProperty("coursier.jni", "");

boolean force = prop.equalsIgnoreCase("force");
if (force) {
useJni0 = true;
return useJni0;
}

boolean disabled = prop.equalsIgnoreCase("false");
if (disabled) {
useJni0 = false;
return useJni0;
}

// Try to get a dummy user env var from registry. If it fails, assume the JNI stuff is broken,
// and fallback on PowerShell scripts.
try {
coursier.jniutils.WindowsEnvironmentVariables.get("PATH");
useJni0 = true;
} catch (Throwable t) {
if (System.getProperty("coursier.jni.check.throw", "").equalsIgnoreCase("true"))
throw new RuntimeException(t);
useJni0 = false;
}

return useJni0;
}
}
1 change: 1 addition & 0 deletions project/Deps.scala
Expand Up @@ -28,6 +28,7 @@ object Deps {
def http4sDsl = "org.http4s" %% "http4s-dsl" % versions.http4s
def java8Compat = "org.scala-lang.modules" %% "scala-java8-compat" % "0.9.1"
def jimfs = "com.google.jimfs" % "jimfs" % "1.1"
def jniUtils = "io.get-coursier.jniutils" % "windows-jni-utils" % "0.2.0"
def jol = "org.openjdk.jol" % "jol-core" % "0.14"
def jsoniterCore = "com.github.plokhotnyuk.jsoniter-scala" %% "jsoniter-scala-core" % versions.jsoniterScala
def jsoniterMacros = "com.github.plokhotnyuk.jsoniter-scala" %% "jsoniter-scala-macros" % versions.jsoniterScala
Expand Down
8 changes: 7 additions & 1 deletion project/Settings.scala
@@ -1,5 +1,5 @@

import java.io.ByteArrayOutputStream
import java.io.{ByteArrayOutputStream, File}
import java.nio.charset.StandardCharsets
import java.nio.file.Files
import java.util.{Arrays, Locale}
Expand Down Expand Up @@ -547,6 +547,12 @@ object Settings {
proguardBinaryDeps.in(Proguard) ++= rtJarOpt.toSeq, // seems needed with sbt 1.4.0
proguardedJar := proguardedJarTask.value,
proguardVersion.in(Proguard) := Deps.proguardVersion,
proguardOptions.in(Proguard) := {
val current = proguardOptions.in(Proguard).value
val idx = current.indexWhere(_.contains(File.separator + "windows-jni-utils"))
assert(idx >= 0, s"options: $current")
current.take(idx) ++ Seq(current(idx).replace("-libraryjars", "-injars")) ++ current.drop(idx + 1)
},
proguardOptions.in(Proguard) ++= Seq(
"-dontnote",
"-dontwarn",
Expand Down

0 comments on commit 590036b

Please sign in to comment.