diff --git a/util/complete/NOTICE b/util/complete/NOTICE new file mode 100644 index 000000000..a6f2c1de4 --- /dev/null +++ b/util/complete/NOTICE @@ -0,0 +1,3 @@ +Simple Build Tool: Completion Component +Copyright 2010 Mark Harrah +Licensed under BSD-style license (see LICENSE) \ No newline at end of file diff --git a/util/log/BufferedLogger.scala b/util/log/BufferedLogger.scala index 689d21b14..054a4b55d 100644 --- a/util/log/BufferedLogger.scala +++ b/util/log/BufferedLogger.scala @@ -1,7 +1,7 @@ /* sbt -- Simple Build Tool * Copyright 2008, 2009, 2010 Mark Harrah */ - package xsbt +package sbt import sbt.{AbstractLogger, ControlEvent, Level, Log, LogEvent, SetLevel, SetTrace, Success, Trace} import scala.collection.mutable.ListBuffer @@ -50,14 +50,14 @@ class BufferedLogger(delegate: AbstractLogger) extends AbstractLogger def setLevel(newLevel: Level.Value) { - buffer += new SetLevel(newLevel) + buffer += new SetLevel(newLevel) delegate.setLevel(newLevel) } def getLevel = delegate.getLevel def getTrace = delegate.getTrace def setTrace(level: Int) { - buffer += new SetTrace(level) + buffer += new SetTrace(level) delegate.setTrace(level) } diff --git a/util/log/FullLogger.scala b/util/log/FullLogger.scala new file mode 100644 index 000000000..091664244 --- /dev/null +++ b/util/log/FullLogger.scala @@ -0,0 +1,24 @@ +/* sbt -- Simple Build Tool + * Copyright 2010 Mark Harrah + */ +package sbt + +/** Promotes the simple Logger interface to the full AbstractLogger interface. */ +class FullLogger(delegate: Logger, override val ansiCodesSupported: Boolean = false) extends BasicLogger +{ + def trace(t: => Throwable) + { + if(traceEnabled) + delegate.trace(t) + } + def log(level: Level.Value, message: => String) + { + if(atLevel(level)) + delegate.log(level, message) + } + def success(message: => String): Unit = + info(message) + def control(event: ControlEvent.Value, message: => String): Unit = + info(message) + def logAll(events: Seq[LogEvent]): Unit = events.foreach(log) +} diff --git a/util/log/Logger.scala b/util/log/Logger.scala index 1be353c4b..68e1f26d8 100644 --- a/util/log/Logger.scala +++ b/util/log/Logger.scala @@ -1,28 +1,20 @@ /* sbt -- Simple Build Tool - * Copyright 2008, 2009 Mark Harrah + * Copyright 2008, 2009, 2010 Mark Harrah */ package sbt import xsbti.{Logger => xLogger, F0} -abstract class AbstractLogger extends xLogger with NotNull +abstract class AbstractLogger extends Logger { def getLevel: Level.Value def setLevel(newLevel: Level.Value) def setTrace(flag: Int) def getTrace: Int final def traceEnabled = getTrace >= 0 - def ansiCodesSupported = false def atLevel(level: Level.Value) = level.id >= getLevel.id - def trace(t: => Throwable): Unit - final def verbose(message: => String): Unit = debug(message) - final def debug(message: => String): Unit = log(Level.Debug, message) - final def info(message: => String): Unit = log(Level.Info, message) - final def warn(message: => String): Unit = log(Level.Warn, message) - final def error(message: => String): Unit = log(Level.Error, message) def success(message: => String): Unit - def log(level: Level.Value, message: => String): Unit def control(event: ControlEvent.Value, message: => String): Unit def logAll(events: Seq[LogEvent]): Unit @@ -39,11 +31,27 @@ abstract class AbstractLogger extends xLogger with NotNull case c: ControlEvent => control(c.event, c.msg) } } +} +/** This is intended to be the simplest logging interface for use by code that wants to log. +* It does not include configuring the logger. */ +trait Logger extends xLogger +{ + final def verbose(message: => String): Unit = debug(message) + final def debug(message: => String): Unit = log(Level.Debug, message) + final def info(message: => String): Unit = log(Level.Info, message) + final def warn(message: => String): Unit = log(Level.Warn, message) + final def error(message: => String): Unit = log(Level.Error, message) + + def ansiCodesSupported = false + + def trace(t: => Throwable): Unit + def log(level: Level.Value, message: => String): Unit + def debug(msg: F0[String]): Unit = log(Level.Debug, msg) def warn(msg: F0[String]): Unit = log(Level.Warn, msg) def info(msg: F0[String]): Unit = log(Level.Info, msg) def error(msg: F0[String]): Unit = log(Level.Error, msg) def trace(msg: F0[Throwable]) = trace(msg.apply) def log(level: Level.Value, msg: F0[String]): Unit = log(level, msg.apply) -} +} \ No newline at end of file diff --git a/util/log/LoggerWriter.scala b/util/log/LoggerWriter.scala index 885646973..81c0d89d0 100644 --- a/util/log/LoggerWriter.scala +++ b/util/log/LoggerWriter.scala @@ -5,9 +5,9 @@ package sbt /** Provides a `java.io.Writer` interface to a `Logger`. Content is line-buffered and logged at `level`. * A line is delimited by `nl`, which is by default the platform line separator.*/ -class LoggerWriter(delegate: AbstractLogger, level: Level.Value, nl: String) extends java.io.Writer +class LoggerWriter(delegate: Logger, level: Level.Value, nl: String) extends java.io.Writer { - def this(delegate: AbstractLogger, level: Level.Value) = this(delegate, level, System.getProperty("line.separator")) + def this(delegate: Logger, level: Level.Value) = this(delegate, level, System.getProperty("line.separator")) private[this] val buffer = new StringBuilder diff --git a/util/process/Process.scala b/util/process/Process.scala new file mode 100644 index 000000000..183469516 --- /dev/null +++ b/util/process/Process.scala @@ -0,0 +1,167 @@ +/* sbt -- Simple Build Tool + * Copyright 2009 Mark Harrah + */ +package sbt + +import java.lang.{Process => JProcess, ProcessBuilder => JProcessBuilder} +import java.io.{Closeable, File, IOException} +import java.io.{BufferedReader, InputStream, InputStreamReader, OutputStream, PipedInputStream, PipedOutputStream} +import java.net.URL + +/** Methods for constructing simple commands that can then be combined. */ +object Process +{ + implicit def apply(command: String): ProcessBuilder = apply(command, None) + implicit def apply(command: Seq[String]): ProcessBuilder = apply (command.toArray, None) + def apply(command: String, arguments: Seq[String]): ProcessBuilder = apply(command :: arguments.toList, None) + /** create ProcessBuilder with working dir set to File and extra environment variables */ + def apply(command: String, cwd: File, extraEnv: (String,String)*): ProcessBuilder = + apply(command, Some(cwd), extraEnv : _*) + /** create ProcessBuilder with working dir optionaly set to File and extra environment variables */ + def apply(command: String, cwd: Option[File], extraEnv: (String,String)*): ProcessBuilder = { + apply(command.split("""\s+"""), cwd, extraEnv : _*) + // not smart to use this on windows, because CommandParser uses \ to escape ". + /*CommandParser.parse(command) match { + case Left(errorMsg) => error(errorMsg) + case Right((cmd, args)) => apply(cmd :: args, cwd, extraEnv : _*) + }*/ + } + /** create ProcessBuilder with working dir optionaly set to File and extra environment variables */ + def apply(command: Seq[String], cwd: Option[File], extraEnv: (String,String)*): ProcessBuilder = { + val jpb = new JProcessBuilder(command.toArray : _*) + cwd.foreach(jpb directory _) + extraEnv.foreach { case (k, v) => jpb.environment.put(k, v) } + apply(jpb) + } + implicit def apply(builder: JProcessBuilder): ProcessBuilder = new SimpleProcessBuilder(builder) + implicit def apply(file: File): FilePartialBuilder = new FileBuilder(file) + implicit def apply(url: URL): URLPartialBuilder = new URLBuilder(url) + implicit def apply(command: scala.xml.Elem): ProcessBuilder = apply(command.text.trim) + implicit def applySeq[T](builders: Seq[T])(implicit convert: T => SourcePartialBuilder): Seq[SourcePartialBuilder] = builders.map(convert) + def apply(value: Boolean): ProcessBuilder = apply(value.toString, if(value) 0 else 1) + def apply(name: String, exitValue: => Int): ProcessBuilder = new DummyProcessBuilder(name, exitValue) + + def cat(file: SourcePartialBuilder, files: SourcePartialBuilder*): ProcessBuilder = cat(file :: files.toList) + def cat(files: Seq[SourcePartialBuilder]): ProcessBuilder = + { + require(!files.isEmpty) + files.map(_.cat).reduceLeft(_ #&& _) + } +} + +trait SourcePartialBuilder extends NotNull +{ + /** Writes the output stream of this process to the given file. */ + def #> (f: File): ProcessBuilder = toFile(f, false) + /** Appends the output stream of this process to the given file. */ + def #>> (f: File): ProcessBuilder = toFile(f, true) + /** Writes the output stream of this process to the given OutputStream. The + * argument is call-by-name, so the stream is recreated, written, and closed each + * time this process is executed. */ + def #>(out: => OutputStream): ProcessBuilder = #> (new OutputStreamBuilder(out)) + def #>(b: ProcessBuilder): ProcessBuilder = new PipedProcessBuilder(toSource, b, false) + private def toFile(f: File, append: Boolean) = #> (new FileOutput(f, append)) + def cat = toSource + protected def toSource: ProcessBuilder +} +trait SinkPartialBuilder extends NotNull +{ + /** Reads the given file into the input stream of this process. */ + def #< (f: File): ProcessBuilder = #< (new FileInput(f)) + /** Reads the given URL into the input stream of this process. */ + def #< (f: URL): ProcessBuilder = #< (new URLInput(f)) + /** Reads the given InputStream into the input stream of this process. The + * argument is call-by-name, so the stream is recreated, read, and closed each + * time this process is executed. */ + def #<(in: => InputStream): ProcessBuilder = #< (new InputStreamBuilder(in)) + def #<(b: ProcessBuilder): ProcessBuilder = new PipedProcessBuilder(b, toSink, false) + protected def toSink: ProcessBuilder +} + +trait URLPartialBuilder extends SourcePartialBuilder +trait FilePartialBuilder extends SinkPartialBuilder with SourcePartialBuilder +{ + def #<<(f: File): ProcessBuilder + def #<<(u: URL): ProcessBuilder + def #<<(i: => InputStream): ProcessBuilder + def #<<(p: ProcessBuilder): ProcessBuilder +} + +/** Represents a process that is running or has finished running. +* It may be a compound process with several underlying native processes (such as 'a #&& b`).*/ +trait Process extends NotNull +{ + /** Blocks until this process exits and returns the exit code.*/ + def exitValue(): Int + /** Destroys this process. */ + def destroy(): Unit +} +/** Represents a runnable process. */ +trait ProcessBuilder extends SourcePartialBuilder with SinkPartialBuilder +{ + /** Starts the process represented by this builder, blocks until it exits, and returns the output as a String. Standard error is + * sent to the console. If the exit code is non-zero, an exception is thrown.*/ + def !! : String + /** Starts the process represented by this builder, blocks until it exits, and returns the output as a String. Standard error is + * sent to the provided Logger. If the exit code is non-zero, an exception is thrown.*/ + def !!(log: Logger) : String + /** Starts the process represented by this builder. The output is returned as a Stream that blocks when lines are not available + * but the process has not completed. Standard error is sent to the console. If the process exits with a non-zero value, + * the Stream will provide all lines up to termination and then throw an exception. */ + def lines: Stream[String] + /** Starts the process represented by this builder. The output is returned as a Stream that blocks when lines are not available + * but the process has not completed. Standard error is sent to the provided Logger. If the process exits with a non-zero value, + * the Stream will provide all lines up to termination but will not throw an exception. */ + def lines(log: Logger): Stream[String] + /** Starts the process represented by this builder. The output is returned as a Stream that blocks when lines are not available + * but the process has not completed. Standard error is sent to the console. If the process exits with a non-zero value, + * the Stream will provide all lines up to termination but will not throw an exception. */ + def lines_! : Stream[String] + /** Starts the process represented by this builder. The output is returned as a Stream that blocks when lines are not available + * but the process has not completed. Standard error is sent to the provided Logger. If the process exits with a non-zero value, + * the Stream will provide all lines up to termination but will not throw an exception. */ + def lines_!(log: Logger): Stream[String] + /** Starts the process represented by this builder, blocks until it exits, and returns the exit code. Standard output and error are + * sent to the console.*/ + def ! : Int + /** Starts the process represented by this builder, blocks until it exits, and returns the exit code. Standard output and error are + * sent to the given Logger.*/ + def !(log: Logger): Int + /** Starts the process represented by this builder, blocks until it exits, and returns the exit code. Standard output and error are + * sent to the console. The newly started process reads from standard input of the current process.*/ + def !< : Int + /** Starts the process represented by this builder, blocks until it exits, and returns the exit code. Standard output and error are + * sent to the given Logger. The newly started process reads from standard input of the current process.*/ + def !<(log: Logger) : Int + /** Starts the process represented by this builder. Standard output and error are sent to the console.*/ + def run(): Process + /** Starts the process represented by this builder. Standard output and error are sent to the given Logger.*/ + def run(log: Logger): Process + /** Starts the process represented by this builder. I/O is handled by the given ProcessIO instance.*/ + def run(io: ProcessIO): Process + /** Starts the process represented by this builder. Standard output and error are sent to the console. + * The newly started process reads from standard input of the current process if `connectInput` is true.*/ + def run(connectInput: Boolean): Process + /** Starts the process represented by this builder, blocks until it exits, and returns the exit code. Standard output and error are + * sent to the given Logger. + * The newly started process reads from standard input of the current process if `connectInput` is true.*/ + def run(log: Logger, connectInput: Boolean): Process + + /** Constructs a command that runs this command first and then `other` if this command succeeds.*/ + def #&& (other: ProcessBuilder): ProcessBuilder + /** Constructs a command that runs this command first and then `other` if this command does not succeed.*/ + def #|| (other: ProcessBuilder): ProcessBuilder + /** Constructs a command that will run this command and pipes the output to `other`. `other` must be a simple command.*/ + def #| (other: ProcessBuilder): ProcessBuilder + /** Constructs a command that will run this command and then `other`. The exit code will be the exit code of `other`.*/ + def ### (other: ProcessBuilder): ProcessBuilder + + def canPipeTo: Boolean +} +/** Each method will be called in a separate thread.*/ +final class ProcessIO(val writeInput: OutputStream => Unit, val processOutput: InputStream => Unit, val processError: InputStream => Unit) extends NotNull +{ + def withOutput(process: InputStream => Unit): ProcessIO = new ProcessIO(writeInput, process, processError) + def withError(process: InputStream => Unit): ProcessIO = new ProcessIO(writeInput, processOutput, process) + def withInput(write: OutputStream => Unit): ProcessIO = new ProcessIO(write, processOutput, processError) +} \ No newline at end of file diff --git a/util/process/ProcessImpl.scala b/util/process/ProcessImpl.scala new file mode 100644 index 000000000..5f89a4749 --- /dev/null +++ b/util/process/ProcessImpl.scala @@ -0,0 +1,473 @@ +/* sbt -- Simple Build Tool + * Copyright 2009 Mark Harrah, Vesa Vilhonen + */ +package sbt + +import java.lang.{Process => JProcess, ProcessBuilder => JProcessBuilder} +import java.io.{BufferedReader, Closeable, InputStream, InputStreamReader, IOException, OutputStream, PrintStream} +import java.io.{FilterInputStream, FilterOutputStream, PipedInputStream, PipedOutputStream} +import java.io.{File, FileInputStream, FileOutputStream} +import java.net.URL + +import scala.concurrent.SyncVar + +/** Runs provided code in a new Thread and returns the Thread instance. */ +private object Spawn +{ + def apply(f: => Unit): Thread = apply(f, false) + def apply(f: => Unit, daemon: Boolean): Thread = + { + val thread = new Thread() { override def run() = { f } } + thread.setDaemon(daemon) + thread.start() + thread + } +} +private object Future +{ + def apply[T](f: => T): () => T = + { + val result = new SyncVar[Either[Throwable, T]] + def run: Unit = + try { result.set(Right(f)) } + catch { case e: Exception => result.set(Left(e)) } + Spawn(run) + () => + result.get match + { + case Right(value) => value + case Left(exception) => throw exception + } + } +} + +object BasicIO +{ + def apply(buffer: StringBuffer, log: Option[Logger], withIn: Boolean) = new ProcessIO(input(withIn), processFully(buffer), getErr(log)) + def apply(log: Logger, withIn: Boolean) = new ProcessIO(input(withIn), processFully(log, Level.Info), processFully(log, Level.Error)) + + def getErr(log: Option[Logger]) = log match { case Some(lg) => processFully(lg, Level.Error); case None => toStdErr } + + def ignoreOut = (i: OutputStream) => () + final val BufferSize = 8192 + final val Newline = System.getProperty("line.separator") + + def close(c: java.io.Closeable) = try { c.close() } catch { case _: java.io.IOException => () } + def processFully(log: Logger, level: Level.Value): InputStream => Unit = processFully(line => log.log(level, line)) + def processFully(buffer: Appendable): InputStream => Unit = processFully(appendLine(buffer)) + def processFully(processLine: String => Unit): InputStream => Unit = + in => + { + val reader = new BufferedReader(new InputStreamReader(in)) + processLinesFully(processLine)(reader.readLine) + } + def processLinesFully(processLine: String => Unit)(readLine: () => String) + { + def readFully() + { + val line = readLine() + if(line != null) + { + processLine(line) + readFully() + } + } + readFully() + } + def connectToIn(o: OutputStream) { transferFully(System.in, o) } + def input(connect: Boolean): OutputStream => Unit = if(connect) connectToIn else ignoreOut + def standard(connectInput: Boolean): ProcessIO = standard(input(connectInput)) + def standard(in: OutputStream => Unit): ProcessIO = new ProcessIO(in, toStdOut, toStdErr) + + def toStdErr = (in: InputStream) => transferFully(in, System.err) + def toStdOut = (in: InputStream) => transferFully(in, System.out) + + def transferFully(in: InputStream, out: OutputStream): Unit = + try { transferFullyImpl(in, out) } + catch { case _: InterruptedException => () } + + private[this] def appendLine(buffer: Appendable): String => Unit = + line => + { + buffer.append(line) + buffer.append(Newline) + } + + private[this] def transferFullyImpl(in: InputStream, out: OutputStream) + { + val continueCount = 1//if(in.isInstanceOf[PipedInputStream]) 1 else 0 + val buffer = new Array[Byte](BufferSize) + def read + { + val byteCount = in.read(buffer) + if(byteCount >= continueCount) + { + out.write(buffer, 0, byteCount) + out.flush() + read + } + } + read + } +} + + +private abstract class AbstractProcessBuilder extends ProcessBuilder with SinkPartialBuilder with SourcePartialBuilder +{ + def #&&(other: ProcessBuilder): ProcessBuilder = new AndProcessBuilder(this, other) + def #||(other: ProcessBuilder): ProcessBuilder = new OrProcessBuilder(this, other) + def #|(other: ProcessBuilder): ProcessBuilder = + { + require(other.canPipeTo, "Piping to multiple processes is not supported.") + new PipedProcessBuilder(this, other, false) + } + def ###(other: ProcessBuilder): ProcessBuilder = new SequenceProcessBuilder(this, other) + + protected def toSource = this + protected def toSink = this + + def run(): Process = run(false) + def run(connectInput: Boolean): Process = run(BasicIO.standard(connectInput)) + def run(log: Logger): Process = run(log, false) + def run(log: Logger, connectInput: Boolean): Process = run(BasicIO(log, connectInput)) + + private[this] def getString(log: Option[Logger], withIn: Boolean): String = + { + val buffer = new StringBuffer + val code = this ! BasicIO(buffer, log, withIn) + if(code == 0) buffer.toString else error("Nonzero exit value: " + code) + } + def !! = getString(None, false) + def !!(log: Logger) = getString(Some(log), false) + def !!< = getString(None, true) + def !!<(log: Logger) = getString(Some(log), true) + + def lines: Stream[String] = lines(false, true, None) + def lines(log: Logger): Stream[String] = lines(false, true, Some(log)) + def lines_! : Stream[String] = lines(false, false, None) + def lines_!(log: Logger): Stream[String] = lines(false, false, Some(log)) + + private[this] def lines(withInput: Boolean, nonZeroException: Boolean, log: Option[Logger]): Stream[String] = + { + val streamed = Streamed[String](nonZeroException) + val process = run(new ProcessIO(BasicIO.input(withInput), BasicIO.processFully(streamed.process), BasicIO.getErr(log))) + Spawn { streamed.done(process.exitValue()) } + streamed.stream() + } + + def ! = run(false).exitValue() + def !< = run(true).exitValue() + def !(log: Logger) = runBuffered(log, false) + def !<(log: Logger) = runBuffered(log, true) + private[this] def runBuffered(log: Logger, connectInput: Boolean) = + { + val log2 = new BufferedLogger(new FullLogger(log)) + log2.buffer { run(log2, connectInput).exitValue() } + } + def !(io: ProcessIO) = run(io).exitValue() + + def canPipeTo = false +} + +private[sbt] class URLBuilder(url: URL) extends URLPartialBuilder with SourcePartialBuilder +{ + protected def toSource = new URLInput(url) +} +private[sbt] class FileBuilder(base: File) extends FilePartialBuilder with SinkPartialBuilder with SourcePartialBuilder +{ + protected def toSource = new FileInput(base) + protected def toSink = new FileOutput(base, false) + def #<<(f: File): ProcessBuilder = #<<(new FileInput(f)) + def #<<(u: URL): ProcessBuilder = #<<(new URLInput(u)) + def #<<(s: => InputStream): ProcessBuilder = #<<(new InputStreamBuilder(s)) + def #<<(b: ProcessBuilder): ProcessBuilder = new PipedProcessBuilder(b, new FileOutput(base, true), false) +} + +private abstract class BasicBuilder extends AbstractProcessBuilder +{ + protected[this] def checkNotThis(a: ProcessBuilder) = require(a != this, "Compound process '" + a + "' cannot contain itself.") + final def run(io: ProcessIO): Process = + { + val p = createProcess(io) + p.start() + p + } + protected[this] def createProcess(io: ProcessIO): BasicProcess +} +private abstract class BasicProcess extends Process +{ + def start(): Unit +} + +private abstract class CompoundProcess extends BasicProcess +{ + def destroy() { destroyer() } + def exitValue() = getExitValue().getOrElse(error("No exit code: process destroyed.")) + + def start() = getExitValue + + protected lazy val (getExitValue, destroyer) = + { + val code = new SyncVar[Option[Int]]() + code.set(None) + val thread = Spawn(code.set(runAndExitValue())) + + ( + Future { thread.join(); code.get }, + () => thread.interrupt() + ) + } + + /** Start and block until the exit value is available and then return it in Some. Return None if destroyed (use 'run')*/ + protected[this] def runAndExitValue(): Option[Int] + + protected[this] def runInterruptible[T](action: => T)(destroyImpl: => Unit): Option[T] = + { + try { Some(action) } + catch { case _: InterruptedException => destroyImpl; None } + } +} + +private abstract class SequentialProcessBuilder(a: ProcessBuilder, b: ProcessBuilder, operatorString: String) extends BasicBuilder +{ + checkNotThis(a) + checkNotThis(b) + override def toString = " ( " + a + " " + operatorString + " " + b + " ) " +} +private class PipedProcessBuilder(first: ProcessBuilder, second: ProcessBuilder, toError: Boolean) extends SequentialProcessBuilder(first, second, if(toError) "#|!" else "#|") +{ + override def createProcess(io: ProcessIO) = new PipedProcesses(first, second, io, toError) +} +private class AndProcessBuilder(first: ProcessBuilder, second: ProcessBuilder) extends SequentialProcessBuilder(first, second, "#&&") +{ + override def createProcess(io: ProcessIO) = new AndProcess(first, second, io) +} +private class OrProcessBuilder(first: ProcessBuilder, second: ProcessBuilder) extends SequentialProcessBuilder(first, second, "#||") +{ + override def createProcess(io: ProcessIO) = new OrProcess(first, second, io) +} +private class SequenceProcessBuilder(first: ProcessBuilder, second: ProcessBuilder) extends SequentialProcessBuilder(first, second, "###") +{ + override def createProcess(io: ProcessIO) = new ProcessSequence(first, second, io) +} + +private class SequentialProcess(a: ProcessBuilder, b: ProcessBuilder, io: ProcessIO, evaluateSecondProcess: Int => Boolean) extends CompoundProcess +{ + protected[this] override def runAndExitValue() = + { + val first = a.run(io) + runInterruptible(first.exitValue)(first.destroy()) flatMap + { codeA => + if(evaluateSecondProcess(codeA)) + { + val second = b.run(io) + runInterruptible(second.exitValue)(second.destroy()) + } + else + Some(codeA) + } + } +} +private class AndProcess(a: ProcessBuilder, b: ProcessBuilder, io: ProcessIO) extends SequentialProcess(a, b, io, _ == 0) +private class OrProcess(a: ProcessBuilder, b: ProcessBuilder, io: ProcessIO) extends SequentialProcess(a, b, io, _ != 0) +private class ProcessSequence(a: ProcessBuilder, b: ProcessBuilder, io: ProcessIO) extends SequentialProcess(a, b, io, ignore => true) + + +private class PipedProcesses(a: ProcessBuilder, b: ProcessBuilder, defaultIO: ProcessIO, toError: Boolean) extends CompoundProcess +{ + protected[this] override def runAndExitValue() = + { + val currentSource = new SyncVar[Option[InputStream]] + val pipeOut = new PipedOutputStream + val source = new PipeSource(currentSource, pipeOut, a.toString) + source.start() + + val pipeIn = new PipedInputStream(pipeOut) + val currentSink = new SyncVar[Option[OutputStream]] + val sink = new PipeSink(pipeIn, currentSink, b.toString) + sink.start() + + def handleOutOrError(fromOutput: InputStream) = currentSource.put(Some(fromOutput)) + + val firstIO = + if(toError) + defaultIO.withError(handleOutOrError) + else + defaultIO.withOutput(handleOutOrError) + val secondIO = defaultIO.withInput(toInput => currentSink.put(Some(toInput)) ) + + val second = b.run(secondIO) + val first = a.run(firstIO) + try + { + runInterruptible { + first.exitValue + currentSource.put(None) + currentSink.put(None) + val result = second.exitValue + result + } { + first.destroy() + second.destroy() + } + } + finally + { + BasicIO.close(pipeIn) + BasicIO.close(pipeOut) + } + } +} +private class PipeSource(currentSource: SyncVar[Option[InputStream]], pipe: PipedOutputStream, label: => String) extends Thread +{ + final override def run() + { + currentSource.get match + { + case Some(source) => + try { BasicIO.transferFully(source, pipe) } + catch { case e: IOException => println("I/O error " + e.getMessage + " for process: " + label); e.printStackTrace() } + finally + { + BasicIO.close(source) + currentSource.unset() + } + run() + case None => + currentSource.unset() + BasicIO.close(pipe) + } + } +} +private class PipeSink(pipe: PipedInputStream, currentSink: SyncVar[Option[OutputStream]], label: => String) extends Thread +{ + final override def run() + { + currentSink.get match + { + case Some(sink) => + try { BasicIO.transferFully(pipe, sink) } + catch { case e: IOException => println("I/O error " + e.getMessage + " for process: " + label); e.printStackTrace() } + finally + { + BasicIO.close(sink) + currentSink.unset() + } + run() + case None => + currentSink.unset() + } + } +} + +private[sbt] class DummyProcessBuilder(override val toString: String, exitValue : => Int) extends AbstractProcessBuilder +{ + override def run(io: ProcessIO): Process = new DummyProcess(exitValue) + override def canPipeTo = true +} +/** A thin wrapper around a java.lang.Process. `ioThreads` are the Threads created to do I/O. +* The implementation of `exitValue` waits until these threads die before returning. */ +private class DummyProcess(action: => Int) extends Process +{ + private[this] val exitCode = Future(action) + override def exitValue() = exitCode() + override def destroy() {} +} +/** Represents a simple command without any redirection or combination. */ +private[sbt] class SimpleProcessBuilder(p: JProcessBuilder) extends AbstractProcessBuilder +{ + override def run(io: ProcessIO): Process = + { + val process = p.start() // start the external process + import io.{writeInput, processOutput, processError} + // spawn threads that process the input, output, and error streams using the functions defined in `io` + val inThread = Spawn(writeInput(process.getOutputStream), true) + val outThread = Spawn(processOutput(process.getInputStream)) + val errorThread = + if(!p.redirectErrorStream) + Spawn(processError(process.getErrorStream)) :: Nil + else + Nil + new SimpleProcess(process, inThread, outThread :: errorThread) + } + override def toString = p.command.toString + override def canPipeTo = true +} +/** A thin wrapper around a java.lang.Process. `outputThreads` are the Threads created to read from the +* output and error streams of the process. `inputThread` is the Thread created to write to the input stream of +* the process. +* The implementation of `exitValue` interrupts `inputThread` and then waits until all I/O threads die before +* returning. */ +private class SimpleProcess(p: JProcess, inputThread: Thread, outputThreads: List[Thread]) extends Process +{ + override def exitValue() = + { + try { p.waitFor() }// wait for the process to terminate + finally { inputThread.interrupt() } // we interrupt the input thread to notify it that it can terminate + outputThreads.foreach(_.join()) // this ensures that all output is complete before returning (waitFor does not ensure this) + p.exitValue() + } + override def destroy() = + { + try { p.destroy() } + finally { inputThread.interrupt() } + } +} + +private class FileOutput(file: File, append: Boolean) extends OutputStreamBuilder(new FileOutputStream(file, append), file.getAbsolutePath) +private class URLInput(url: URL) extends InputStreamBuilder(url.openStream, url.toString) +private class FileInput(file: File) extends InputStreamBuilder(new FileInputStream(file), file.getAbsolutePath) + +import Uncloseable.protect +private class OutputStreamBuilder(stream: => OutputStream, label: String) extends ThreadProcessBuilder(label, _.writeInput(protect(stream))) +{ + def this(stream: => OutputStream) = this(stream, "") +} +private class InputStreamBuilder(stream: => InputStream, label: String) extends ThreadProcessBuilder(label, _.processOutput(protect(stream))) +{ + def this(stream: => InputStream) = this(stream, "") +} + +private abstract class ThreadProcessBuilder(override val toString: String, runImpl: ProcessIO => Unit) extends AbstractProcessBuilder +{ + override def run(io: ProcessIO): Process = + { + val success = new SyncVar[Boolean] + success.put(false) + new ThreadProcess(Spawn {runImpl(io); success.set(true) }, success) + } +} +private final class ThreadProcess(thread: Thread, success: SyncVar[Boolean]) extends Process +{ + override def exitValue() = + { + thread.join() + if(success.get) 0 else 1 + } + override def destroy() { thread.interrupt() } +} + +object Uncloseable +{ + def apply(in: InputStream): InputStream = new FilterInputStream(in) { override def close() {} } + def apply(out: OutputStream): OutputStream = new FilterOutputStream(out) { override def close() {} } + def protect(in: InputStream): InputStream = if(in eq System.in) Uncloseable(in) else in + def protect(out: OutputStream): OutputStream = if( (out eq System.out) || (out eq System.err)) Uncloseable(out) else out +} +private object Streamed +{ + def apply[T](nonzeroException: Boolean): Streamed[T] = + { + val q = new java.util.concurrent.LinkedBlockingQueue[Either[Int, T]] + def next(): Stream[T] = + q.take match + { + case Left(0) => Stream.empty + case Left(code) => if(nonzeroException) error("Nonzero exit code: " + code) else Stream.empty + case Right(s) => Stream.cons(s, next) + } + new Streamed((s: T) => q.put(Right(s)), code => q.put(Left(code)), () => next()) + } +} + +private final class Streamed[T](val process: T => Unit, val done: Int => Unit, val stream: () => Stream[T]) extends NotNull \ No newline at end of file diff --git a/util/process/src/test/scala/ProcessSpecification.scala b/util/process/src/test/scala/ProcessSpecification.scala new file mode 100644 index 000000000..0d7141635 --- /dev/null +++ b/util/process/src/test/scala/ProcessSpecification.scala @@ -0,0 +1,93 @@ +package sbt + +import java.io.File +import org.scalacheck.{Arbitrary, Gen, Prop, Properties} +import Prop._ + +import Process._ + +object ProcessSpecification extends Properties("Process I/O") +{ + private val log = new ConsoleLogger + + implicit val exitCodeArb: Arbitrary[Array[Byte]] = Arbitrary(Gen.choose(0, 10) flatMap { size => Gen.resize(size, Arbitrary.arbArray[Byte].arbitrary) }) + + /*property("Correct exit code") = forAll( (exitCode: Byte) => checkExit(exitCode)) + property("#&& correct") = forAll( (exitCodes: Array[Byte]) => checkBinary(exitCodes)(_ #&& _)(_ && _)) + property("#|| correct") = forAll( (exitCodes: Array[Byte]) => checkBinary(exitCodes)(_ #|| _)(_ || _)) + property("### correct") = forAll( (exitCodes: Array[Byte]) => checkBinary(exitCodes)(_ ### _)( (x,latest) => latest))*/ + property("Pipe to output file") = forAll( (data: Array[Byte]) => checkFileOut(data)) + property("Pipe to input file") = forAll( (data: Array[Byte]) => checkFileIn(data)) + property("Pipe to process") = forAll( (data: Array[Byte]) => checkPipe(data)) + + private def checkBinary(codes: Array[Byte])(reduceProcesses: (ProcessBuilder, ProcessBuilder) => ProcessBuilder)(reduceExit: (Boolean, Boolean) => Boolean) = + { + (codes.length > 1) ==> + { + val unsignedCodes = codes.map(unsigned) + val exitCode = unsignedCodes.map(code => Process(process("sbt.exit " + code))).reduceLeft(reduceProcesses) ! + val expectedExitCode = unsignedCodes.map(toBoolean).reduceLeft(reduceExit) + toBoolean(exitCode) == expectedExitCode + } + } + private def toBoolean(exitCode: Int) = exitCode == 0 + private def checkExit(code: Byte) = + { + val exitCode = unsigned(code) + (process("sbt.exit " + exitCode) !) == exitCode + } + private def checkFileOut(data: Array[Byte]) = + { + withData(data) { (temporaryFile, temporaryFile2) => + val catCommand = process("sbt.cat " + temporaryFile.getAbsolutePath) + catCommand #> temporaryFile2 + } + } + private def checkFileIn(data: Array[Byte]) = + { + withData(data) { (temporaryFile, temporaryFile2) => + val catCommand = process("sbt.cat") + temporaryFile #> catCommand #> temporaryFile2 + } + } + private def checkPipe(data: Array[Byte]) = + { + withData(data) { (temporaryFile, temporaryFile2) => + val catCommand = process("sbt.cat") + temporaryFile #> catCommand #| catCommand #> temporaryFile2 + } + } + private def temp() = File.createTempFile("sbt", "") + private def withData(data: Array[Byte])(f: (File, File) => ProcessBuilder) = + { + val temporaryFile1 = temp() + val temporaryFile2 = temp() + try + { + IO.write(temporaryFile1, data) + val process = f(temporaryFile1, temporaryFile2) + ( process ! ) == 0 && + { + val b1 = IO.readBytes(temporaryFile1) + val b2 = IO.readBytes(temporaryFile2) + b1 sameElements b2 + } + } + finally + { + temporaryFile1.delete() + temporaryFile2.delete() + } + } + private def unsigned(b: Byte): Int = ((b: Int) +256) % 256 + private def process(command: String) = + { + val ignore = echo // just for the compile dependency so that this test is rerun when TestedProcess.scala changes, not used otherwise + + val thisClasspath = List(getSource[ScalaObject], getSource[IO.type], getSource[SourceTag]).mkString(File.pathSeparator) + "java -cp " + thisClasspath + " " + command + } + private def getSource[T : Manifest]: String = + IO.classLocationFile[T].getAbsolutePath +} +private trait SourceTag \ No newline at end of file diff --git a/util/process/src/test/scala/TestedProcess.scala b/util/process/src/test/scala/TestedProcess.scala new file mode 100644 index 000000000..c013de531 --- /dev/null +++ b/util/process/src/test/scala/TestedProcess.scala @@ -0,0 +1,56 @@ +package sbt + +import java.io.{File, FileNotFoundException, IOException} + +object exit +{ + def main(args: Array[String]) + { + System.exit(java.lang.Integer.parseInt(args(0))) + } +} +object cat +{ + def main(args: Array[String]) + { + try { + if(args.length == 0) + IO.transfer(System.in, System.out) + else + catFiles(args.toList) + System.exit(0) + } catch { + case e => + e.printStackTrace() + System.err.println("Error: " + e.toString) + System.exit(1) + } + } + private def catFiles(filenames: List[String]): Option[String] = + { + filenames match + { + case head :: tail => + val file = new File(head) + if(file.isDirectory) + throw new IOException("Is directory: " + file) + else if(file.exists) + { + Using.fileInputStream(file) { stream => + IO.transfer(stream, System.out) + } + catFiles(tail) + } + else + throw new FileNotFoundException("No such file or directory: " + file) + case Nil => None + } + } +} +object echo +{ + def main(args: Array[String]) + { + System.out.println(args.mkString(" ")) + } +} \ No newline at end of file