diff options
author | Jan Christopher Vogt <oss.nsp@cvogt.org> | 2016-10-14 00:52:10 -0400 |
---|---|---|
committer | GitHub <noreply@github.com> | 2016-10-14 00:52:10 -0400 |
commit | 358a189db9842705d5c885a4c315635cd36fdc14 (patch) | |
tree | 7533ac2d08c686278256117a14ad64419d708274 | |
parent | 86a4f0970c0e55ac072648691df8c159de50e969 (diff) | |
parent | dc01feea46cef8c075d41b1bcbb11f54d3f7fad1 (diff) | |
download | cbt-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.md | 2 | ||||
-rw-r--r-- | examples/build-info-example/BuildInfo.scala | 8 | ||||
-rw-r--r-- | examples/build-info-example/Main.scala | 9 | ||||
-rw-r--r-- | examples/build-info-example/Readme.md | 3 | ||||
-rw-r--r-- | examples/build-info-example/build/build.scala | 27 | ||||
-rw-r--r-- | nailgun_launcher/NailgunLauncher.java | 26 | ||||
-rw-r--r-- | nailgun_launcher/Stage0Lib.java | 10 | ||||
-rw-r--r-- | nailgun_launcher/ThreadLocalOutputStream.java | 30 | ||||
-rw-r--r-- | nailgun_launcher/TrapSecurityManager.java | 52 | ||||
-rw-r--r-- | nailgun_launcher/TrappedExitCode.java | 8 | ||||
-rw-r--r-- | stage1/Stage1Lib.scala | 87 | ||||
-rw-r--r-- | stage2/GitDependency.scala | 48 | ||||
-rw-r--r-- | test/library-test/build/build.scala | 8 | ||||
-rw-r--r-- | test/simple-fixed-cbt/Main.scala | 6 | ||||
-rw-r--r-- | test/simple-fixed-cbt/build/build.scala | 14 | ||||
-rw-r--r-- | test/test.scala | 14 |
16 files changed, 274 insertions, 78 deletions
@@ -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) |