diff --git a/util/process/src/main/scala/sbt/Process.scala b/util/process/src/main/scala/sbt/Process.scala index 0fe40612d..a370048e4 100644 --- a/util/process/src/main/scala/sbt/Process.scala +++ b/util/process/src/main/scala/sbt/Process.scala @@ -80,7 +80,7 @@ trait SourcePartialBuilder extends NotNull * 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) + def #>(b: ProcessBuilder): ProcessBuilder = new PipedProcessBuilder(toSource, b, false, ExitCodes.firstIfNonzero) private def toFile(f: File, append: Boolean) = #> (new FileOutput(f, append)) def cat = toSource protected def toSource: ProcessBuilder @@ -95,7 +95,7 @@ trait SinkPartialBuilder extends NotNull * 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) + def #<(b: ProcessBuilder): ProcessBuilder = new PipedProcessBuilder(b, toSink, false, ExitCodes.firstIfNonzero) protected def toSink: ProcessBuilder } @@ -174,7 +174,9 @@ trait ProcessBuilder extends SourcePartialBuilder with SinkPartialBuilder 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.*/ + /** Constructs a command that will run this command and pipes the output to `other`. + * `other` must be a simple command. + * The exit code will be that of `other` regardless of whether this command succeeds. */ 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 diff --git a/util/process/src/main/scala/sbt/ProcessImpl.scala b/util/process/src/main/scala/sbt/ProcessImpl.scala index 9b3464703..9a3aae606 100644 --- a/util/process/src/main/scala/sbt/ProcessImpl.scala +++ b/util/process/src/main/scala/sbt/ProcessImpl.scala @@ -114,6 +114,10 @@ object BasicIO def inheritInput(connect: Boolean) = { p: JProcessBuilder => if (connect) InheritInput(p) else false } } +private[sbt] object ExitCodes { + def ignoreFirst: (Int, Int) => Int = (a,b) => b + def firstIfNonzero: (Int, Int) => Int = (a,b) => if(a != 0) a else b +} private abstract class AbstractProcessBuilder extends ProcessBuilder with SinkPartialBuilder with SourcePartialBuilder @@ -123,7 +127,7 @@ private abstract class AbstractProcessBuilder extends ProcessBuilder with SinkPa def #|(other: ProcessBuilder): ProcessBuilder = { require(other.canPipeTo, "Piping to multiple processes is not supported.") - new PipedProcessBuilder(this, other, false) + new PipedProcessBuilder(this, other, false, exitCode = ExitCodes.ignoreFirst) } def ###(other: ProcessBuilder): ProcessBuilder = new SequenceProcessBuilder(this, other) @@ -181,7 +185,7 @@ private[sbt] class FileBuilder(base: File) extends FilePartialBuilder with SinkP 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) + def #<<(b: ProcessBuilder): ProcessBuilder = new PipedProcessBuilder(b, new FileOutput(base, true), false, ExitCodes.firstIfNonzero) } private abstract class BasicBuilder extends AbstractProcessBuilder @@ -235,9 +239,9 @@ private abstract class SequentialProcessBuilder(a: ProcessBuilder, b: ProcessBui checkNotThis(b) override def toString = " ( " + a + " " + operatorString + " " + b + " ) " } -private class PipedProcessBuilder(first: ProcessBuilder, second: ProcessBuilder, toError: Boolean) extends SequentialProcessBuilder(first, second, if(toError) "#|!" else "#|") +private class PipedProcessBuilder(first: ProcessBuilder, second: ProcessBuilder, toError: Boolean, exitCode: (Int,Int) => Int) extends SequentialProcessBuilder(first, second, if(toError) "#|!" else "#|") { - override def createProcess(io: ProcessIO) = new PipedProcesses(first, second, io, toError) + override def createProcess(io: ProcessIO) = new PipedProcesses(first, second, io, toError, exitCode) } private class AndProcessBuilder(first: ProcessBuilder, second: ProcessBuilder) extends SequentialProcessBuilder(first, second, "#&&") { @@ -274,7 +278,7 @@ private class OrProcess(a: ProcessBuilder, b: ProcessBuilder, io: ProcessIO) ext 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 +private class PipedProcesses(a: ProcessBuilder, b: ProcessBuilder, defaultIO: ProcessIO, toError: Boolean, exitCode: (Int, Int) => Int) extends CompoundProcess { protected[this] override def runAndExitValue() = { @@ -302,11 +306,11 @@ private class PipedProcesses(a: ProcessBuilder, b: ProcessBuilder, defaultIO: Pr try { runInterruptible { - first.exitValue + val firstResult = first.exitValue currentSource.put(None) currentSink.put(None) - val result = second.exitValue - result + val secondResult = second.exitValue + exitCode(firstResult, secondResult) } { first.destroy() second.destroy() diff --git a/util/process/src/test/scala/ProcessSpecification.scala b/util/process/src/test/scala/ProcessSpecification.scala index 6810025bf..f48a8282c 100644 --- a/util/process/src/test/scala/ProcessSpecification.scala +++ b/util/process/src/test/scala/ProcessSpecification.scala @@ -6,7 +6,7 @@ import Prop._ import Process._ -private[this] object ProcessSpecification extends Properties("Process I/O") +object ProcessSpecification extends Properties("Process I/O") { implicit val exitCodeArb: Arbitrary[Array[Byte]] = Arbitrary(Gen.choose(0, 10) flatMap { size => Gen.resize(size, Arbitrary.arbArray[Byte].arbitrary) }) @@ -15,8 +15,11 @@ private[this] object ProcessSpecification extends Properties("Process I/O") 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 from input file") = forAll( (data: Array[Byte]) => checkFileIn(data)) property("Pipe to process") = forAll( (data: Array[Byte]) => checkPipe(data)) + property("Pipe to process ignores input exit code") = forAll( (data: Array[Byte], code: Byte) => checkPipeExit(data, code)) + property("Pipe from input file to bad process preserves correct exit code.") = forAll( (data: Array[Byte], code: Byte) => checkFileInExit(data, code)) + property("Pipe to output file from bad process preserves correct exit code.") = forAll( (data: Array[Byte], code: Byte) => checkFileOutExit(data, code)) private def checkBinary(codes: Array[Byte])(reduceProcesses: (ProcessBuilder, ProcessBuilder) => ProcessBuilder)(reduceExit: (Boolean, Boolean) => Boolean) = { @@ -55,29 +58,63 @@ private[this] object ProcessSpecification extends Properties("Process I/O") temporaryFile #> catCommand #| catCommand #> temporaryFile2 } } + private def checkPipeExit(data: Array[Byte], code: Byte) = + withTempFiles { (a,b) => + IO.write(a, data) + val catCommand = process("sbt.cat") + val exitCommand = process(s"sbt.exit $code") + val exit = (a #> exitCommand #| catCommand #> b).! + (s"Exit code: $exit") |: + (s"Output file length: ${b.length}") |: + (exit == 0) && + (b.length == 0) + } + + private def checkFileOutExit(data: Array[Byte], exitCode: Byte) = + withTempFiles { (a,b) => + IO.write(a, data) + val code = unsigned(exitCode) + val command = process(s"sbt.exit $code") + val exit = (a #> command #> b).! + (s"Exit code: $exit, expected: $code") |: + (s"Output file length: ${b.length}") |: + (exit == code) && + (b.length == 0) + } + + private def checkFileInExit(data: Array[Byte], exitCode: Byte) = + withTempFiles { (a,b) => + IO.write(a, data) + val code = unsigned(exitCode) + val command = process(s"sbt.exit $code") + val exit = (a #> command).! + (s"Exit code: $exit, expected: $code") |: + (exit == code) + } + private def temp() = File.createTempFile("sbt", "") private def withData(data: Array[Byte])(f: (File, File) => ProcessBuilder) = + withTempFiles { (a, b) => + IO.write(a, data) + val process = f(a, b) + ( process ! ) == 0 && sameFiles(a, b) + } + private def sameFiles(a: File, b: File) = + IO.readBytes(a) sameElements IO.readBytes(b) + + private def withTempFiles[T](f: (File, File) => T): T = { 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 - } - } + try f(temporaryFile1, temporaryFile2) finally { temporaryFile1.delete() temporaryFile2.delete() } - } - private def unsigned(b: Byte): Int = ((b: Int) +256) % 256 + } + private def unsigned(b: Int): Int = ((b: Int) +256) % 256 + private def unsigned(b: Byte): Int = unsigned(b: Int) 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