aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJan Christopher Vogt <oss.nsp@cvogt.org>2016-10-14 00:52:10 -0400
committerGitHub <noreply@github.com>2016-10-14 00:52:10 -0400
commit358a189db9842705d5c885a4c315635cd36fdc14 (patch)
tree7533ac2d08c686278256117a14ad64419d708274
parent86a4f0970c0e55ac072648691df8c159de50e969 (diff)
parentdc01feea46cef8c075d41b1bcbb11f54d3f7fad1 (diff)
downloadcbt-358a189db9842705d5c885a4c315635cd36fdc14.tar.gz
cbt-358a189db9842705d5c885a4c315635cd36fdc14.tar.bz2
cbt-358a189db9842705d5c885a4c315635cd36fdc14.zip
Merge pull request #276 from cvogt/fix-cross-classloader-security-manager
Fix exit code trapping and out/err redirection with and without nailgun
-rw-r--r--README.md2
-rw-r--r--examples/build-info-example/BuildInfo.scala8
-rw-r--r--examples/build-info-example/Main.scala9
-rw-r--r--examples/build-info-example/Readme.md3
-rw-r--r--examples/build-info-example/build/build.scala27
-rw-r--r--nailgun_launcher/NailgunLauncher.java26
-rw-r--r--nailgun_launcher/Stage0Lib.java10
-rw-r--r--nailgun_launcher/ThreadLocalOutputStream.java30
-rw-r--r--nailgun_launcher/TrapSecurityManager.java52
-rw-r--r--nailgun_launcher/TrappedExitCode.java8
-rw-r--r--stage1/Stage1Lib.scala87
-rw-r--r--stage2/GitDependency.scala48
-rw-r--r--test/library-test/build/build.scala8
-rw-r--r--test/simple-fixed-cbt/Main.scala6
-rw-r--r--test/simple-fixed-cbt/build/build.scala14
-rw-r--r--test/test.scala14
16 files changed, 274 insertions, 78 deletions
diff --git a/README.md b/README.md
index ba75c93..f1d0fc6 100644
--- a/README.md
+++ b/README.md
@@ -148,7 +148,7 @@ CBT can help you with that. Execute:
$ cbt tools createBuild
```
-Now there should be a file `build/build.scala` with a sample `Build class.
+Now there should be a file `build/build.scala` with a sample `Build` class.
Btw., a build file can have it's own build and so on recursively like in SBT.
When you create a file `build/build/build.scala` and change `Build` class in there
diff --git a/examples/build-info-example/BuildInfo.scala b/examples/build-info-example/BuildInfo.scala
new file mode 100644
index 0000000..ac0e680
--- /dev/null
+++ b/examples/build-info-example/BuildInfo.scala
@@ -0,0 +1,8 @@
+// generated file
+import java.io._
+object BuildInfo{
+def artifactId = "build-info-example"
+def groupId = "cbt.examples"
+def version = "0.1"
+def scalaVersion = "2.11.8"
+}
diff --git a/examples/build-info-example/Main.scala b/examples/build-info-example/Main.scala
new file mode 100644
index 0000000..cb4ad75
--- /dev/null
+++ b/examples/build-info-example/Main.scala
@@ -0,0 +1,9 @@
+object Main{
+ def main(args: Array[String]): Unit = {
+ import BuildInfo._
+ println("scalaVersion: "+scalaVersion)
+ println("groupId: "+groupId)
+ println("artifactId: "+artifactId)
+ println("version: "+version)
+ }
+} \ No newline at end of file
diff --git a/examples/build-info-example/Readme.md b/examples/build-info-example/Readme.md
new file mode 100644
index 0000000..acbb84e
--- /dev/null
+++ b/examples/build-info-example/Readme.md
@@ -0,0 +1,3 @@
+This is an example how to propagate build-time information
+such as version or scalaVersion to runtime.
+The advantage of the approach taken here is simplicity.
diff --git a/examples/build-info-example/build/build.scala b/examples/build-info-example/build/build.scala
new file mode 100644
index 0000000..f6fc7a4
--- /dev/null
+++ b/examples/build-info-example/build/build.scala
@@ -0,0 +1,27 @@
+import cbt._
+import java.nio.file.Files._
+
+class Build(val context: Context) extends PackageJars{
+ def name = "build-info-example"
+ def groupId = "cbt.examples"
+ def defaultVersion = "0.1"
+ override def defaultScalaVersion = "2.11.8"
+ override def compile = {
+ val file = (projectDirectory ++ "/BuildInfo.scala").toPath
+ val contents = s"""// generated file
+import java.io._
+object BuildInfo{
+def artifactId = "$artifactId"
+def groupId = "$groupId"
+def version = "$version"
+def scalaVersion = "$scalaVersion"
+}
+"""
+ if( exists(file) && contents != new String(readAllBytes(file)) )
+ write(
+ (projectDirectory ++ "/BuildInfo.scala").toPath,
+ contents.getBytes
+ )
+ super.compile
+ }
+}
diff --git a/nailgun_launcher/NailgunLauncher.java b/nailgun_launcher/NailgunLauncher.java
index 5a70312..b1daf2a 100644
--- a/nailgun_launcher/NailgunLauncher.java
+++ b/nailgun_launcher/NailgunLauncher.java
@@ -23,13 +23,6 @@ public class NailgunLauncher{
public final static SecurityManager initialSecurityManager
= System.getSecurityManager();
- public final static ThreadLocal<Boolean> trapExitCode =
- new ThreadLocal<Boolean>() {
- @Override protected Boolean initialValue() {
- return false;
- }
- };
-
public static String TARGET = System.getenv("TARGET");
private static String NAILGUN = "nailgun_launcher/";
private static String STAGE1 = "stage1/";
@@ -66,7 +59,24 @@ public class NailgunLauncher{
installProxySettings();
String[] diff = args[0].split("\\.");
long start = _start - (Long.parseLong(diff[0]) * 1000L) - Long.parseLong(diff[1]);
-
+
+ // if nailgun didn't install it's threadLocal stdout/err replacements, install CBT's.
+ // this hack allows to later swap out System.out/err while still affecting things like
+ // scala.Console, which captured them at startup
+ try{
+ System.out.getClass().getDeclaredField("streams"); // nailgun ThreadLocalPrintStream
+ assert(System.out.getClass().getName() == "com.martiansoftware.nailgun.ThreadLocalPrintStream");
+ } catch( NoSuchFieldException e ){
+ System.setOut( new PrintStream(new ThreadLocalOutputStream(System.out)) );
+ }
+ try{
+ System.err.getClass().getDeclaredField("streams"); // nailgun ThreadLocalPrintStream
+ assert(System.out.getClass().getName() == "com.martiansoftware.nailgun.ThreadLocalPrintStream");
+ } catch( NoSuchFieldException e ){
+ System.setErr( new PrintStream(new ThreadLocalOutputStream(System.err)) );
+ }
+ // ---------------------
+
_assert(System.getenv("CBT_HOME") != null, "environment variable CBT_HOME not defined");
String CBT_HOME = System.getenv("CBT_HOME");
String cache = CBT_HOME + "/cache/";
diff --git a/nailgun_launcher/Stage0Lib.java b/nailgun_launcher/Stage0Lib.java
index 11a9aab..452bdae 100644
--- a/nailgun_launcher/Stage0Lib.java
+++ b/nailgun_launcher/Stage0Lib.java
@@ -20,21 +20,21 @@ public class Stage0Lib{
}
public static int runMain(String cls, String[] args, ClassLoader cl) throws Throwable{
- Boolean trapExitCodeBefore = NailgunLauncher.trapExitCode.get();
+ Boolean trapExitCodeBefore = TrapSecurityManager.trapExitCode().get();
try{
- NailgunLauncher.trapExitCode.set(true);
+ TrapSecurityManager.trapExitCode().set(true);
cl.loadClass(cls)
.getMethod("main", String[].class)
.invoke( null, (Object) args);
return 0;
}catch( InvocationTargetException exception ){
Throwable cause = exception.getCause();
- if(cause instanceof TrappedExitCode){
- return ((TrappedExitCode) cause).exitCode;
+ if(TrapSecurityManager.isTrappedExit(cause)){
+ return TrapSecurityManager.exitCode(cause);
}
throw exception;
} finally {
- NailgunLauncher.trapExitCode.set(trapExitCodeBefore);
+ TrapSecurityManager.trapExitCode().set(trapExitCodeBefore);
}
}
diff --git a/nailgun_launcher/ThreadLocalOutputStream.java b/nailgun_launcher/ThreadLocalOutputStream.java
new file mode 100644
index 0000000..c12b775
--- /dev/null
+++ b/nailgun_launcher/ThreadLocalOutputStream.java
@@ -0,0 +1,30 @@
+package cbt;
+import java.io.*;
+
+public class ThreadLocalOutputStream extends OutputStream{
+ final public ThreadLocal<OutputStream> threadLocal;
+ final private OutputStream initialValue;
+
+ public ThreadLocalOutputStream( OutputStream initialValue ){
+ this.initialValue = initialValue;
+ threadLocal = new ThreadLocal<OutputStream>() {
+ @Override protected OutputStream initialValue() {
+ return ThreadLocalOutputStream.this.initialValue;
+ }
+ };
+ }
+
+ public OutputStream get(){
+ return threadLocal.get();
+ }
+
+ public void set( OutputStream outputStream ){
+ threadLocal.set( outputStream );
+ }
+
+ public void write( int b ) throws IOException{
+ // after implementing this I realized NailgunLauncher uses the same hack,
+ // so probably this is not a problem performance
+ get().write(b);
+ }
+}
diff --git a/nailgun_launcher/TrapSecurityManager.java b/nailgun_launcher/TrapSecurityManager.java
index fada878..1626787 100644
--- a/nailgun_launcher/TrapSecurityManager.java
+++ b/nailgun_launcher/TrapSecurityManager.java
@@ -9,6 +9,33 @@ would be Nailgun's if running on Nailgun. If we do not delegate to Nailgun, it s
could in some cases kill the server process
*/
public class TrapSecurityManager extends ProxySecurityManager{
+ public static ThreadLocal<Boolean> trapExitCode(){
+ // storing the flag in the installed security manager
+ // instead of e.g. a static member is necessary because
+ // we run multiple versions of CBT with multiple TrapSecurityManager classes
+ // but we need to affect the installed one
+ SecurityManager sm = System.getSecurityManager();
+ if(sm instanceof TrapSecurityManager){
+ return ((TrapSecurityManager) sm)._trapExitCode;
+ } else {
+ try{
+ @SuppressWarnings("unchecked")
+ ThreadLocal<Boolean> res =
+ (ThreadLocal<Boolean>) sm.getClass().getMethod("trapExitCode").invoke(null);
+ return res;
+ } catch(Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+ }
+
+ private final ThreadLocal<Boolean> _trapExitCode =
+ new ThreadLocal<Boolean>() {
+ @Override protected Boolean initialValue() {
+ return false;
+ }
+ };
+
public TrapSecurityManager(){
super(NailgunLauncher.initialSecurityManager);
}
@@ -19,21 +46,38 @@ public class TrapSecurityManager extends ProxySecurityManager{
Calling .super leads to ClassNotFound exteption for a lambda.
Calling to the previous SecurityManager leads to a stack overflow
*/
- if(!NailgunLauncher.trapExitCode.get()){
+ if(!TrapSecurityManager.trapExitCode().get()){
super.checkPermission(permission);
}
}
public void checkPermission( Permission permission, Object context ){
/* Does this methods need to be overidden? */
- if(!NailgunLauncher.trapExitCode.get()){
+ if(!TrapSecurityManager.trapExitCode().get()){
super.checkPermission(permission, context);
}
}
+
+ private static final String prefix = "[TrappedExit] ";
+
@Override
public void checkExit( int status ){
- if(NailgunLauncher.trapExitCode.get()){
- throw new TrappedExitCode(status);
+ if(TrapSecurityManager.trapExitCode().get()){
+ // using a RuntimeException and a prefix here instead of a custom
+ // exception type because this is thrown by the installed TrapSecurityManager
+ // but other versions of cbt need to be able to catch it, that do not have access
+ // to that version of the TrapSecurityManager class
+ throw new RuntimeException(prefix+status);
}
super.checkExit(status);
}
+
+ public static boolean isTrappedExit( Throwable t ){
+ return t instanceof RuntimeException && t.getMessage().startsWith(prefix);
+ }
+
+ public static int exitCode( Throwable t ){
+ assert(isTrappedExit(t));
+ return Integer.parseInt( t.getMessage().substring(prefix.length()) );
+ }
+
}
diff --git a/nailgun_launcher/TrappedExitCode.java b/nailgun_launcher/TrappedExitCode.java
deleted file mode 100644
index 154db27..0000000
--- a/nailgun_launcher/TrappedExitCode.java
+++ /dev/null
@@ -1,8 +0,0 @@
-package cbt;
-import java.security.*;
-public class TrappedExitCode extends SecurityException{
- public int exitCode;
- public TrappedExitCode(int exitCode){
- this.exitCode = exitCode;
- }
-}
diff --git a/stage1/Stage1Lib.scala b/stage1/Stage1Lib.scala
index 7620fd8..273b9af 100644
--- a/stage1/Stage1Lib.scala
+++ b/stage1/Stage1Lib.scala
@@ -23,7 +23,7 @@ object CatchTrappedExitCode{
def unapply(e: Throwable): Option[ExitCode] = {
Option(e) flatMap {
case i: InvocationTargetException => unapply(i.getTargetException)
- case e: TrappedExitCode => Some( ExitCode(e.exitCode) )
+ case e if TrapSecurityManager.isTrappedExit(e) => Some( ExitCode(TrapSecurityManager.exitCode(e)) )
case _ => None
}
}
@@ -256,9 +256,9 @@ class Stage1Lib( val logger: Logger ) extends BaseLib{
val singleArgs = scalacOptions.map( "-S" ++ _ )
val code =
- try{
+ redirectOutToErr{
System.err.println("Compiling to " ++ compileTarget.toString)
- redirectOutToErr{
+ try{
lib.runMain(
_class,
dualArgs ++ singleArgs ++ Seq(
@@ -266,27 +266,27 @@ class Stage1Lib( val logger: Logger ) extends BaseLib{
) ++ files.map(_.toString),
zinc.classLoader(classLoaderCache)
)
+ } catch {
+ case e: Exception =>
+ System.err.println(red("The Scala compiler crashed. Try running it by hand:"))
+ System.out.println(s"""
+ java -cp \\
+ ${zinc.classpath.strings.mkString(":\\\n")} \\
+ \\
+ ${_class} \\
+ \\
+ ${dualArgs.grouped(2).map(_.mkString(" ")).mkString(" \\\n")} \\
+ \\
+ ${singleArgs.mkString(" \\\n")} \\
+ \\
+ -cp \\
+ ${classpath.strings.mkString(":\\\n")} \\
+ \\
+ ${files.sorted.mkString(" \\\n")}
+ """
+ )
+ ExitCode.Failure
}
- } catch {
- case e: Exception =>
- System.err.println(red("The Scala compiler crashed. Try running it by hand:"))
- System.out.println(s"""
-java -cp \\
-${zinc.classpath.strings.mkString(":\\\n")} \\
-\\
-${_class} \\
-\\
-${dualArgs.grouped(2).map(_.mkString(" ")).mkString(" \\\n")} \\
-\\
-${singleArgs.mkString(" \\\n")} \\
-\\
--cp \\
-${classpath.strings.mkString(":\\\n")} \\
-\\
-${files.sorted.mkString(" \\\n")}
-"""
- )
- ExitCode.Failure
}
if(code == ExitCode.Success){
@@ -302,26 +302,49 @@ ${files.sorted.mkString(" \\\n")}
}
}
def redirectOutToErr[T](code: => T): T = {
- val oldOut = System.out
- try{
- System.setOut(System.err)
- code
- } finally{
- System.setOut(oldOut)
+ val ( out, err ) = try{
+ // trying nailgun's System.our/err wrapper
+ val field = System.out.getClass.getDeclaredField("streams")
+ assert(System.out.getClass.getName == "com.martiansoftware.nailgun.ThreadLocalPrintStream")
+ assert(System.err.getClass.getName == "com.martiansoftware.nailgun.ThreadLocalPrintStream")
+ field.setAccessible(true)
+ val out = field.get(System.out).asInstanceOf[ThreadLocal[PrintStream]]
+ val err = field.get(System.err).asInstanceOf[ThreadLocal[PrintStream]]
+ ( out, err )
+ } catch {
+ case e: NoSuchFieldException =>
+ // trying cbt's System.our/err wrapper
+ val field = classOf[FilterOutputStream].getDeclaredField("out")
+ field.setAccessible(true)
+ val outStream = field.get(System.out)
+ val errStream = field.get(System.err)
+ assert(outStream.getClass.getName == "cbt.ThreadLocalOutputStream")
+ assert(errStream.getClass.getName == "cbt.ThreadLocalOutputStream")
+ val field2 = outStream.getClass.getDeclaredField("threadLocal")
+ field2.setAccessible(true)
+ val out = field2.get(outStream).asInstanceOf[ThreadLocal[PrintStream]]
+ val err = field2.get(errStream).asInstanceOf[ThreadLocal[PrintStream]]
+ ( out, err )
}
+
+ val oldOut: PrintStream = out.get
+ out.set( err.get: PrintStream )
+ val res = code
+ out.set( oldOut )
+ res
}
def trapExitCode( code: => ExitCode ): ExitCode = {
- val trapExitCodeBefore = NailgunLauncher.trapExitCode.get
+ val trapExitCodeBefore = TrapSecurityManager.trapExitCode().get
try{
- NailgunLauncher.trapExitCode.set(true)
+ TrapSecurityManager.trapExitCode().set(true)
code
} catch {
case CatchTrappedExitCode(exitCode) =>
logger.stage1(s"caught exit code $exitCode")
exitCode
} finally {
- NailgunLauncher.trapExitCode.set(trapExitCodeBefore)
+ TrapSecurityManager.trapExitCode().set(trapExitCodeBefore)
}
}
diff --git a/stage2/GitDependency.scala b/stage2/GitDependency.scala
index 8faabc5..650fd09 100644
--- a/stage2/GitDependency.scala
+++ b/stage2/GitDependency.scala
@@ -3,6 +3,7 @@ import java.io._
import java.nio.file.Files.readAllBytes
import java.net._
import org.eclipse.jgit.api._
+import org.eclipse.jgit.internal.storage.file._
import org.eclipse.jgit.transport.UsernamePasswordCredentialsProvider
import org.eclipse.jgit.lib.Ref
@@ -22,35 +23,46 @@ case class GitDependency(
private val credentialsFile = context.projectDirectory ++ "/git.login"
private object checkoutCache extends Cache[File]
+
+ private def authenticate(_git: CloneCommand) =
+ if(!credentialsFile.exists){
+ _git
+ } else {
+ val (user, password) = {
+ // TODO: implement safer method than reading credentials from plain text file
+ val c = new String(readAllBytes(credentialsFile.toPath)).split("\n").head.trim.split(":")
+ (c(0), c.drop(1).mkString(":"))
+ }
+ _git.setCredentialsProvider( new UsernamePasswordCredentialsProvider(user, password) )
+ }
+
def checkout: File = checkoutCache{
val checkoutDirectory = context.cache ++ s"/git/$domain/$path/$ref"
- if(checkoutDirectory.exists){
+ val _git = if(checkoutDirectory.exists){
logger.git(s"Found existing checkout of $url#$ref in $checkoutDirectory")
+ val _git = new Git(new FileRepository(checkoutDirectory ++ "/.git"))
+ val actualRef = _git.getRepository.getBranch
+ if(actualRef != ref){
+ logger.git(s"actual ref '$actualRef' does not match expected ref '$ref' - fetching and checking out")
+ _git.fetch().call()
+ _git.checkout().setName(ref).call
+ }
+ _git
} else {
logger.git(s"Cloning $url into $checkoutDirectory")
- val git = {
- val _git = Git
+ val _git = authenticate(
+ Git
.cloneRepository()
.setURI(url)
.setDirectory(checkoutDirectory)
-
- if(!credentialsFile.exists){
- _git
- } else {
- val (user, password) = {
- // TODO: implement safer method than reading credentials from plain text file
- val c = new String(readAllBytes(credentialsFile.toPath)).split("\n").head.trim.split(":")
- (c(0), c.drop(1).mkString(":"))
- }
- _git.setCredentialsProvider( new UsernamePasswordCredentialsProvider(user, password) )
- }
- }.call()
+ ).call()
logger.git(s"Checking out ref $ref")
- git.checkout()
- .setName(ref)
- .call()
+ _git.checkout().setName(ref).call()
+ _git
}
+ val actualRef = _git.getRepository.getBranch
+ assert( actualRef == ref, s"actual ref '$actualRef' does not match expected ref '$ref'")
checkoutDirectory
}
private object dependencyCache extends Cache[DependencyImplementation]
diff --git a/test/library-test/build/build.scala b/test/library-test/build/build.scala
index a6e61af..d07e58e 100644
--- a/test/library-test/build/build.scala
+++ b/test/library-test/build/build.scala
@@ -1,7 +1,11 @@
import cbt._
-// cbt:https://github.com/cvogt/cbt.git#1f4f6097d3ca682d6fd20a7cc6dd277832350827
-class Build(val context: Context) extends BaseBuild{
+// cbt:https://github.com/cvogt/cbt.git#bf4ea112fe668fb7e2e95a2baca4989b16384783
+class Build(val context: Context) extends BaseBuild with PackageJars{
+ def groupId = "cbt.test"
+ def defaultVersion = "0.1"
+ def name = "library-test"
+
override def dependencies =
super.dependencies ++ // don't forget super.dependencies here for scala-library, etc.
Seq(
diff --git a/test/simple-fixed-cbt/Main.scala b/test/simple-fixed-cbt/Main.scala
new file mode 100644
index 0000000..75f9349
--- /dev/null
+++ b/test/simple-fixed-cbt/Main.scala
@@ -0,0 +1,6 @@
+import lib_test.Foo
+import org.eclipse.jgit.lib.Ref
+import com.spotify.missinglink.ArtifactLoader
+object Main extends App{
+ println(Foo.bar)
+}
diff --git a/test/simple-fixed-cbt/build/build.scala b/test/simple-fixed-cbt/build/build.scala
new file mode 100644
index 0000000..1d0640d
--- /dev/null
+++ b/test/simple-fixed-cbt/build/build.scala
@@ -0,0 +1,14 @@
+import cbt._
+
+// cbt:https://github.com/cvogt/cbt.git#bf4ea112fe668fb7e2e95a2baca4989b16384783
+class Build(val context: cbt.Context) extends PackageJars{
+ override def dependencies = super.dependencies ++ Seq(
+ DirectoryDependency( context.cbtHome ++ "/test/library-test" )
+ ) ++ Resolver( mavenCentral ).bind(
+ MavenDependency("org.eclipse.jgit", "org.eclipse.jgit", "4.2.0.201601211800-r"),
+ MavenDependency("com.spotify", "missinglink-core", "0.1.1")
+ )
+ def groupId: String = "cbt.test"
+ def defaultVersion: String = "0.1"
+ def name: String = "simple-fixed-cbt"
+} \ No newline at end of file
diff --git a/test/test.scala b/test/test.scala
index 8845528..5b4a4af 100644
--- a/test/test.scala
+++ b/test/test.scala
@@ -71,6 +71,7 @@ object Main{
val res = runCbt(path, Seq(name))
val debugToken = name ++ " " ++ path ++ " "
assertSuccess(res,debugToken)
+ res
// assert(res.err == "", res.err) // FIXME: enable this
}
@@ -211,12 +212,25 @@ object Main{
compile("../examples/uber-jar-example")
{
+ val res = task("docJar","simple-fixed-cbt")
+ assert( res.out endsWith "simple-fixed-cbt_2.11-0.1-javadoc.jar", res.out )
+ assert( res.err contains "model contains", res.err )
+ assert( res.err endsWith "documentable templates", res.err )
+ }
+
+ {
val res = runCbt("simple", Seq("printArgs","1","2","3"))
assert(res.exit0)
assert(res.out == "1 2 3", res.out)
}
{
+ val res = runCbt("../examples/build-info-example", Seq("run"))
+ assert(res.exit0)
+ assert(res.out contains "version: 0.1", res.out)
+ }
+
+ {
val res = runCbt("forgot-extend", Seq("run"))
assert(!res.exit0)
assert(res.err contains "Build cannot be cast to cbt.BuildInterface", res.err)