aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJan Christopher Vogt <oss.nsp@cvogt.org>2017-06-15 23:15:37 -0400
committerGitHub <noreply@github.com>2017-06-15 23:15:37 -0400
commit494c302e738529ff2113d25910398d7b2b7a196c (patch)
treed67c88d10906647c7fd56229e6e56121a9107744
parent618711302b7ea29de651c1f771eb3160e236b339 (diff)
parent09051773461b98d374d1b46dd0a2caa57768ab30 (diff)
downloadcbt-494c302e738529ff2113d25910398d7b2b7a196c.tar.gz
cbt-494c302e738529ff2113d25910398d7b2b7a196c.tar.bz2
cbt-494c302e738529ff2113d25910398d7b2b7a196c.zip
Merge pull request #523 from cvogt/restart
sbt-revolver like restart feature
-rw-r--r--README.md5
-rw-r--r--build/build.scala2
-rwxr-xr-xcbt29
-rw-r--r--examples/restart/Readme.md10
-rw-r--r--examples/restart/build/build.scala4
-rw-r--r--examples/restart/src/Main.scala6
-rw-r--r--libraries/process/build/build.scala10
-rw-r--r--libraries/process/build/build/build.scala5
-rw-r--r--libraries/process/process.scala175
-rw-r--r--libraries/process/test/test.scala7
-rw-r--r--stage1/Stage1.scala1
-rw-r--r--stage1/Stage1Lib.scala108
-rw-r--r--stage1/resolver.scala26
-rw-r--r--stage2/BasicBuild.scala41
-rw-r--r--stage2/Lib.scala5
-rw-r--r--stage2/libraries.scala1
16 files changed, 320 insertions, 115 deletions
diff --git a/README.md b/README.md
index 94aa2ab..90dd845 100644
--- a/README.md
+++ b/README.md
@@ -226,6 +226,11 @@ To also clear the screen on each run use:
$ cbt loop clear run
```
+To call and restart the main method on file change (like sbt-revolver)
+```
+$ cbt direct loop restart
+```
+
### Adding tests
The simplest way to add tests is putting a few assertions into the previously
diff --git a/build/build.scala b/build/build.scala
index 74bf6da..729e6ed 100644
--- a/build/build.scala
+++ b/build/build.scala
@@ -13,7 +13,7 @@ class Build(val context: Context) extends Shared with Scalariform with PublishLo
super.dependencies ++ Resolver(mavenCentral).bind(
MavenDependency("org.eclipse.jgit", "org.eclipse.jgit", "4.2.0.201601211800-r"),
ScalaDependency("org.scala-lang.modules","scala-xml",constants.scalaXmlVersion)
- ) :+ libraries.cbt.reflect :+ libraries.cbt.eval
+ ) :+ libraries.cbt.reflect :+ libraries.cbt.eval :+ libraries.cbt.process
}
override def sources = Seq(
diff --git a/cbt b/cbt
index bc369de..e2014ec 100755
--- a/cbt
+++ b/cbt
@@ -290,6 +290,7 @@ stage1 () {
USER_PRESSED_CTRL_C=130
CBT_LOOP_FILE="$CWD/target/.cbt-loop.tmp"
+CBT_KILL_FILE="$CWD/target/.cbt-kill.tmp"
if [ $loop -eq 0 ]; then
which fswatch >/dev/null 2>/dev/null
export fswatch_installed=$?
@@ -305,20 +306,42 @@ while true; do
if [ -f "$CBT_LOOP_FILE" ]; then
rm "$CBT_LOOP_FILE"
fi
+ if [ -f "$CBT_KILL_FILE" ]; then
+ rm "$CBT_KILL_FILE"
+ fi
stage1 "$@"
if [ ! $loop -eq 0 ] || [ $exitCode -eq $USER_PRESSED_CTRL_C ]; then
log "not looping, exiting" "$@"
break
else
+ for file in "${NAILGUN_SOURCES[@]}"; do
+ echo "$file" >> "$CBT_LOOP_FILE"
+ done
files=
if [ -f "$CBT_LOOP_FILE" ]; then
- files=($(cat "$CBT_LOOP_FILE"))
+ files=($(sort "$CBT_LOOP_FILE"))
+ fi
+ pids=
+ if [ -f "$CBT_KILL_FILE" ]; then
+ pids=($(cat "$CBT_KILL_FILE")) # FIXME: should we uniq here?
#rm "$CBT_LOOP_FILE"
fi
echo ""
echo "Watching for file changes... (ctrl+c short press for loop, long press for abort)"
- #echo fswatch --one-event "${NAILGUN_SOURCES[@]}" "${files[@]}"
- fswatch --one-event "${NAILGUN_SOURCES[@]}" "${files[@]}"
+ for file in "${files[@]}"; do
+ if [ $file == "" ]; then
+ echo "warning: empty file found in loop file list" 1>&2
+ fi
+ done
+ fswatch --one-event "${files[@]}"
+ for pid in "${pids[@]}"; do
+ if [ $pid == "" ]; then
+ echo "warning: empty pid found in pid kill list" 1>&2
+ else
+ log "killing process $pid"
+ kill -KILL $pid
+ fi
+ done
fi
done
diff --git a/examples/restart/Readme.md b/examples/restart/Readme.md
new file mode 100644
index 0000000..e4bf1b5
--- /dev/null
+++ b/examples/restart/Readme.md
@@ -0,0 +1,10 @@
+This example's main method simply prints the current process id.
+This can be used to experiment with cbt's restart feature
+(the equivalent to sbt-revolver).
+
+```
+cbt direct loop restart
+```
+
+starts the main method in a separate process and kills it, when
+a change in project, build file or cbt is detected.
diff --git a/examples/restart/build/build.scala b/examples/restart/build/build.scala
new file mode 100644
index 0000000..a18f951
--- /dev/null
+++ b/examples/restart/build/build.scala
@@ -0,0 +1,4 @@
+import cbt._
+class Build(val context: Context) extends BaseBuild{
+ override def dependencies = super.dependencies :+ libraries.cbt.process
+} \ No newline at end of file
diff --git a/examples/restart/src/Main.scala b/examples/restart/src/Main.scala
new file mode 100644
index 0000000..1c03832
--- /dev/null
+++ b/examples/restart/src/Main.scala
@@ -0,0 +1,6 @@
+object Main extends App {
+ while(true){
+ Thread.sleep(1000)
+ println( "process " + cbt.process.currentProcessId + " is still running" )
+ }
+}
diff --git a/libraries/process/build/build.scala b/libraries/process/build/build.scala
new file mode 100644
index 0000000..da859b5
--- /dev/null
+++ b/libraries/process/build/build.scala
@@ -0,0 +1,10 @@
+package cbt_build.process
+import cbt._
+import cbt_internal._
+class Build(val context: Context) extends Library{
+ override def inceptionYear = 2017
+ override def description = "helpers for process calls"
+ override def dependencies = super.dependencies ++ Resolver(mavenCentral).bind(
+ MavenDependency( "net.java.dev.jna", "jna-platform", "4.4.0" )
+ ) :+ libraries.cbt.common_1
+}
diff --git a/libraries/process/build/build/build.scala b/libraries/process/build/build/build.scala
new file mode 100644
index 0000000..d3f98ce
--- /dev/null
+++ b/libraries/process/build/build/build.scala
@@ -0,0 +1,5 @@
+package cbt_build.reflect.build
+import cbt._
+class Build(val context: Context) extends BuildBuild with CbtInternal{
+ override def dependencies = super.dependencies :+ cbtInternal.library
+}
diff --git a/libraries/process/process.scala b/libraries/process/process.scala
new file mode 100644
index 0000000..982c9d0
--- /dev/null
+++ b/libraries/process/process.scala
@@ -0,0 +1,175 @@
+package cbt.process
+import cbt.ExitCode
+import java.io._
+
+object `package` extends Module
+
+trait Module {
+ def runMainForked(
+ className: String,
+ args: Seq[String],
+ classpath: String,
+ directory: Option[File],
+ outErrIn: Option[( OutputStream, OutputStream, InputStream )]
+ ): ( Int, () => ExitCode, () => ExitCode ) = {
+ // FIXME: Windows support
+ val java_exe = new File( System.getProperty( "java.home" ) + "/bin/java" )
+ runWithIO(
+ java_exe.toString +: "-cp" +: classpath +: className +: args,
+ directory,
+ outErrIn
+ )
+ }
+
+ def runWithIO(
+ commandLine: Seq[String],
+ directory: Option[File],
+ outErrIn: Option[( OutputStream, OutputStream, InputStream )]
+ ): ( Int, () => ExitCode, () => ExitCode ) = {
+ val pb = new ProcessBuilder( commandLine: _* )
+ outErrIn.map {
+ case ( out, err, in ) =>
+ val process = directory.map( pb.directory( _ ) ).getOrElse( pb )
+ .redirectInput( ProcessBuilder.Redirect.PIPE )
+ .redirectOutput( ProcessBuilder.Redirect.PIPE )
+ .redirectError( ProcessBuilder.Redirect.PIPE )
+ .start
+
+ (
+ processId( process ),
+ () => {
+ val lock = new AnyRef
+
+ val t1 = asyncPipeCharacterStreamSyncLines( process.getErrorStream, err, lock )
+ val t2 = asyncPipeCharacterStreamSyncLines( process.getInputStream, out, lock )
+ val t3 = asyncPipeCharacterStream( System.in, process.getOutputStream, process.isAlive )
+
+ t1.start
+ t2.start
+ t3.start
+
+ t1.join
+ t2.join
+
+ val e = process.waitFor
+ System.err.println( scala.Console.RESET + "Please press ENTER to continue..." )
+ t3.join
+ ExitCode( e )
+ },
+ () => {
+ process.destroy
+ Thread.sleep( 20 )
+ ExitCode( process.destroyForcibly.waitFor )
+ }
+ )
+ }.getOrElse {
+ val process = pb.inheritIO.start
+ (
+ processId( process ),
+ () => ExitCode( process.waitFor ),
+ () => {
+ process.destroy
+ Thread.sleep( 20 )
+ ExitCode( process.destroyForcibly.waitFor )
+ }
+ )
+ }
+ }
+
+ private def accessField( cls: Class[_], field: String ): java.lang.reflect.Field = {
+ val f = cls.getDeclaredField( field )
+ f.setAccessible( true )
+ f
+ }
+
+ import com.sun.jna.{ Library, Native }
+ private trait CLibrary extends Library {
+ def getpid: Int
+ }
+ private val CLibraryInstance: CLibrary = Native.loadLibrary( "c", classOf[CLibrary] ).asInstanceOf[CLibrary]
+
+ def currentProcessId: Int = {
+ if ( Option( System.getProperty( "os.name" ) ).exists( _.startsWith( "Windows" ) ) ) {
+ com.sun.jna.platform.win32.Kernel32.INSTANCE.GetCurrentProcessId
+ } else {
+ CLibraryInstance.getpid
+ }
+ }
+
+ /** process id of given Process */
+ def processId( process: Process ): Int = {
+ val clsName = process.getClass.getName
+ if ( clsName == "java.lang.UNIXProcess" ) {
+ accessField( process.getClass, "pid" ).getInt( process )
+ } else if ( clsName == "java.lang.Win32Process" || clsName == "java.lang.ProcessImpl" ) {
+ import com.sun.jna.platform.win32.{ WinNT, Kernel32 }
+ val handle = new WinNT.HANDLE
+ handle.setPointer(
+ com.sun.jna.Pointer.createConstant(
+ accessField( process.getClass, "handle" ).getLong( process )
+ )
+ )
+ Kernel32.INSTANCE.GetProcessId( handle )
+ } else {
+ throw new Exception( "Unexpected Process sub-class: " + clsName )
+ }
+ }
+
+ def asyncPipeCharacterStreamSyncLines( inputStream: InputStream, outputStream: OutputStream, lock: AnyRef ): Thread = {
+ new Thread(
+ new Runnable {
+ def run = {
+ val b = new BufferedInputStream( inputStream )
+ Iterator.continually {
+ b.read // block until and read next character
+ }.takeWhile( _ != -1 ).map { c =>
+ lock.synchronized { // synchronize with other invocations
+ outputStream.write( c )
+ Iterator
+ .continually( b.read )
+ .takeWhile( _ != -1 )
+ .map { c =>
+ try {
+ outputStream.write( c )
+ outputStream.flush
+ (
+ c != '\n' // release lock when new line was encountered, allowing other writers to slip in
+ && b.available > 0 // also release when nothing is available to not block other outputs
+ )
+ } catch {
+ case e: IOException if e.getMessage == "Stream closed" => false
+ }
+ }
+ .takeWhile( identity )
+ .length // force entire iterator
+ }
+ }.length // force entire iterator
+ }
+ }
+ )
+ }
+
+ def asyncPipeCharacterStream( inputStream: InputStream, outputStream: OutputStream, continue: => Boolean ) = {
+ new Thread(
+ new Runnable {
+ def run = {
+ Iterator
+ .continually { inputStream.read }
+ .takeWhile( _ != -1 )
+ .map { c =>
+ try {
+ outputStream.write( c )
+ outputStream.flush
+ true
+ } catch {
+ case e: IOException if e.getMessage == "Stream closed" => false
+ }
+ }
+ .takeWhile( identity )
+ .takeWhile( _ => continue )
+ .length // force entire iterator
+ }
+ }
+ )
+ }
+}
diff --git a/libraries/process/test/test.scala b/libraries/process/test/test.scala
new file mode 100644
index 0000000..4e1c1a1
--- /dev/null
+++ b/libraries/process/test/test.scala
@@ -0,0 +1,7 @@
+object Tests{
+ def main(args: Array[String]): Unit = {
+ val pb = new ProcessBuilder("cat")
+ val p = pb.start
+ cbt.process.getProcessId( p ) // checks that it actually gets a process id
+ }
+}
diff --git a/stage1/Stage1.scala b/stage1/Stage1.scala
index d9bde7c..99c7b1e 100644
--- a/stage1/Stage1.scala
+++ b/stage1/Stage1.scala
@@ -94,6 +94,7 @@ object Stage1{
stage2.listFiles
++ (stage2 / "plugins").listOrFail
++ (cbtHome / "libraries" / "eval").listOrFail
+ ++ (cbtHome / "libraries" / "process").listOrFail
).filter(_.isFile).filter(_.toString.endsWith(".scala"))
val cls = this.getClass.getClassLoader.loadClass("cbt.NailgunLauncher")
diff --git a/stage1/Stage1Lib.scala b/stage1/Stage1Lib.scala
index 40b3fed..89e52b6 100644
--- a/stage1/Stage1Lib.scala
+++ b/stage1/Stage1Lib.scala
@@ -417,6 +417,21 @@ ${sourceFiles.sorted.mkString(" \\\n")}
StandardOpenOption.APPEND
)
}
+
+ /**
+ add process id to a cbt internal list of processes to kill
+ when looping after a file change
+ */
+ def addProcessIdToKillList(cwd: File, processId: Int) = {
+ val file = cwd / "target/.cbt-kill.tmp"
+ file.createNewFile
+ lib.write(
+ file,
+ processId.toString + "\n",
+ StandardOpenOption.APPEND
+ )
+ }
+
def cached[T]( targetDirectory: File, inputLastModified: Long )( action: () => T ): (Option[T],Long) = {
val t = targetDirectory
val start = System.currentTimeMillis
@@ -432,99 +447,6 @@ ${sourceFiles.sorted.mkString(" \\\n")}
outputLastModified
)
}
-
- def asyncPipeCharacterStreamSyncLines( inputStream: InputStream, outputStream: OutputStream, lock: AnyRef ): Thread = {
- new Thread(
- new Runnable{
- def run = {
- val b = new BufferedInputStream( inputStream )
- Iterator.continually{
- b.read // block until and read next character
- }.takeWhile(_ != -1).map{ c =>
- lock.synchronized{ // synchronize with other invocations
- outputStream.write(c)
- Iterator
- .continually( b.read )
- .takeWhile( _ != -1 )
- .map{ c =>
- try{
- outputStream.write(c)
- outputStream.flush
- (
- c != '\n' // release lock when new line was encountered, allowing other writers to slip in
- && b.available > 0 // also release when nothing is available to not block other outputs
- )
- } catch {
- case e: IOException if e.getMessage == "Stream closed" => false
- }
- }
- .takeWhile(identity)
- .length // force entire iterator
- }
- }.length // force entire iterator
- }
- }
- )
- }
-
- def asyncPipeCharacterStream( inputStream: InputStream, outputStream: OutputStream, continue: => Boolean ) = {
- new Thread(
- new Runnable{
- def run = {
- Iterator
- .continually{ inputStream.read }
- .takeWhile(_ != -1)
- .map{ c =>
- try{
- outputStream.write(c)
- outputStream.flush
- true
- } catch {
- case e: IOException if e.getMessage == "Stream closed" => false
- }
- }
- .takeWhile( identity )
- .takeWhile( _ => continue )
- .length // force entire iterator
- }
- }
- )
- }
-
- def runWithIO( commandLine: Seq[String], directory: Option[File] = None ): ExitCode = {
- val (out,err,in) = lib.getOutErrIn match { case (l,r, in) => (l.get,r.get, in) }
- val pb = new ProcessBuilder( commandLine: _* )
- val exitCode =
- if( !NailgunLauncher.runningViaNailgun ){
- pb.inheritIO.start.waitFor
- } else {
- val process = directory.map( pb.directory( _ ) ).getOrElse( pb )
- .redirectInput(ProcessBuilder.Redirect.PIPE)
- .redirectOutput(ProcessBuilder.Redirect.PIPE)
- .redirectError(ProcessBuilder.Redirect.PIPE)
- .start
-
- val lock = new AnyRef
-
- val t1 = lib.asyncPipeCharacterStreamSyncLines( process.getErrorStream, err, lock )
- val t2 = lib.asyncPipeCharacterStreamSyncLines( process.getInputStream, out, lock )
- val t3 = lib.asyncPipeCharacterStream( System.in, process.getOutputStream, process.isAlive )
-
- t1.start
- t2.start
- t3.start
-
- t1.join
- t2.join
-
- val e = process.waitFor
- System.err.println( scala.Console.RESET + "Please press ENTER to continue..." )
- t3.join
- e
- }
-
- ExitCode( exitCode )
- }
}
import scala.reflect._
diff --git a/stage1/resolver.scala b/stage1/resolver.scala
index f4a9b13..13d4070 100644
--- a/stage1/resolver.scala
+++ b/stage1/resolver.scala
@@ -77,25 +77,16 @@ trait DependencyImplementation extends Dependency{
)
}
*/
- def fork = false
- def runMain( className: String, args: Seq[String] ): ExitCode = {
- if(fork){
- val java_exe = new File(System.getProperty("java.home")) / "bin" / "java"
- lib.runWithIO(
- java_exe.string +: "-cp" +: classpath.string +: className +: args
- )
- } else {
- lib.getMain( classLoader.loadClass( className ) )( args )
- }
- }
+ def runMain( className: String, args: Seq[String] ): ExitCode =
+ lib.getMain( classLoader.loadClass( className ) )( args )
- def runMain( args: Seq[String] ): ExitCode = {
- val c = mainClass.getOrElse(
- throw new RuntimeException( "No main class found in " + this )
- )
- runMain( c.getName, args )
- }
+ def runMain( args: Seq[String] ): ExitCode =
+ runMain( mainClassOrFail.getName, args )
+
+ def mainClassOrFail = mainClass.getOrElse(
+ throw new RuntimeException( "No main class found in " + this )
+ )
def mainClass = lib.pickOne(
"Which one do you want to run?",
@@ -209,6 +200,7 @@ case class CbtDependencies(cbtLastModified: Long, mavenCache: File, nailgunTarge
stage1Dependency +:
MavenResolver(cbtLastModified, mavenCache,mavenCentral).bind(
MavenDependency("org.eclipse.jgit", "org.eclipse.jgit", "4.2.0.201601211800-r"),
+ MavenDependency("net.java.dev.jna", "jna-platform", "4.4.0"),
MavenDependency("org.scala-lang","scala-compiler",constants.scalaVersion)
)
)
diff --git a/stage2/BasicBuild.scala b/stage2/BasicBuild.scala
index 0bdbad7..3a9c958 100644
--- a/stage2/BasicBuild.scala
+++ b/stage2/BasicBuild.scala
@@ -319,4 +319,45 @@ trait BaseBuild extends BuildInterface with DependencyImplementation with SbtDep
final def crossScalaVersionsArray = Array(scalaVersion)
def publish: Seq[URL] = Seq()
+
+ def fork = false
+
+ def runForked: ExitCode = {
+ val ( pid, waitFor, destroy ) = runForkedHandles
+ waitFor()
+ }
+
+ /** currently only produces output when run via cbt direct */
+ def restart: Int = {
+ val pid = restart( mainClassOrFail.getName, context.args )
+ System.err.print("started process with pid: ")
+ pid
+ }
+
+ def restart( className: String, args: Seq[String] ): Int = {
+ val ( pid, waitFor, destroy ) = runForked( mainClassOrFail.getName, context.args )
+ lib.addProcessIdToKillList( context.cwd, pid )
+ pid
+ }
+
+ protected def runForkedHandles = runForked( mainClassOrFail.getName, context.args )
+
+ def runForked( className: String, args: Seq[String] ): ( Int, () => ExitCode, () => ExitCode ) =
+ lib.runMainForked(
+ className,
+ args,
+ classpath.string,
+ Some( context.workingDirectory ),
+ NailgunLauncher.runningViaNailgun.option(
+ lib.getOutErrIn match { case (l,r, in) => (l.get,r.get, in) }
+ )
+ )
+
+ override def runMain( className: String, args: Seq[String] ): ExitCode = {
+ if(fork){
+ runForked(className, args)._2()
+ } else {
+ super.runMain( className, args )
+ }
+ }
}
diff --git a/stage2/Lib.scala b/stage2/Lib.scala
index 56f24c6..8801b33 100644
--- a/stage2/Lib.scala
+++ b/stage2/Lib.scala
@@ -16,7 +16,10 @@ import scala.reflect.NameTransformer
case class Developer(id: String, name: String, timezone: String, url: URL)
/** Don't extend. Create your own libs :). */
-final class Lib(val logger: Logger) extends Stage1Lib(logger){
+final class Lib(val logger: Logger) extends
+ Stage1Lib(logger) with
+ _root_.cbt.process.Module
+{
lib =>
val buildFileName = "build.scala"
diff --git a/stage2/libraries.scala b/stage2/libraries.scala
index 08a7a74..b4515e1 100644
--- a/stage2/libraries.scala
+++ b/stage2/libraries.scala
@@ -14,6 +14,7 @@ class libraries( context: Context, scalaVersion: String, scalaMajorVersion: Stri
def eval = dep( "eval" )
def file = dep( "file" )
def interfaces = dep( "interfaces" )
+ def process = dep( "process" )
def proguard = dep( "proguard" )
def reflect = dep( "reflect" )
def scalatestRunner = dep( "scalatest-runner" )