blob: 19c2a7ffa8623ff555cdd100d25dfac443e2e4bc [file] [log] [blame]
/*
* Copyright 2018 The Bazel Authors. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.bazel.kotlin.builder
import com.google.devtools.build.lib.worker.WorkerProtocol
import io.bazel.kotlin.builder.utils.rootCause
import java.io.*
import java.nio.charset.StandardCharsets.UTF_8
import java.nio.file.Files
import java.nio.file.Paths
import java.util.stream.Collectors
sealed class ToolException(
msg: String,
ex: Throwable? = null
) : RuntimeException(msg, ex)
class CompilationException(msg: String, cause: Throwable? = null) :
ToolException(msg, cause)
class CompilationStatusException(
msg: String,
val status: Int,
val lines: List<String> = emptyList()
) : ToolException("$msg:${lines.joinToString("\n", "\n")}")
/**
* Interface for command line programs.
*
* This is the same thing as a main function, except not static.
*/
interface CommandLineProgram {
/**
* Runs blocking program start to finish.
*
* This function might be called multiple times throughout the life of this object. Output
* must be sent to [System.out] and [System.err].
*
* @param args command line arguments
* @return program exit code, i.e. 0 for success, non-zero for failure
*/
fun apply(args: List<String>): Int
}
/**
* Bazel worker runner.
*
* This class adapts a traditional command line program so it can be spawned by Bazel as a
* persistent worker process that handles multiple invocations per JVM. It will also be backwards
* compatible with being run as a normal single-invocation command.
*
* @param <T> delegate program type
</T> */
class BazelWorker(
private val delegate: CommandLineProgram,
private val output: PrintStream,
private val mnemonic: String
) : CommandLineProgram {
companion object {
private const val INTERUPTED_STATUS = 0
private const val ERROR_STATUS = 1
}
override fun apply(args: List<String>): Int {
return if (args.contains("--persistent_worker"))
runAsPersistentWorker(args)
else delegate.apply(loadArguments(args, false))
}
@Suppress("UNUSED_PARAMETER")
private fun runAsPersistentWorker(ignored: List<String>): Int {
val realStdIn = System.`in`
val realStdOut = System.out
val realStdErr = System.err
try {
ByteArrayInputStream(ByteArray(0)).use { emptyIn ->
ByteArrayOutputStream().use { buffer ->
PrintStream(buffer).use { ps ->
System.setIn(emptyIn)
System.setOut(ps)
System.setErr(ps)
while (true) {
val request = WorkerProtocol.WorkRequest.parseDelimitedFrom(realStdIn) ?: return 0
var exitCode: Int
exitCode = try {
delegate.apply(loadArguments(request.argumentsList, true))
} catch (e: RuntimeException) {
if (wasInterrupted(e)) {
return INTERUPTED_STATUS
}
System.err.println(
"ERROR: Worker threw uncaught exception with args: " + request.argumentsList.stream().collect(Collectors.joining(" "))
)
e.printStackTrace(System.err)
ERROR_STATUS
}
WorkerProtocol.WorkResponse.newBuilder()
.setOutput(buffer.toString())
.setExitCode(exitCode)
.build()
.writeDelimitedTo(realStdOut)
realStdOut.flush()
buffer.reset()
System.gc() // be a good little worker process and consume less memory when idle
}
}
}
}
} catch (e: IOException) {
if (wasInterrupted(e)) {
return INTERUPTED_STATUS
}
throw e
} catch (e: RuntimeException) {
if (wasInterrupted(e)) {
return INTERUPTED_STATUS
}
throw e
} finally {
System.setIn(realStdIn)
System.setOut(realStdOut)
System.setErr(realStdErr)
}
throw RuntimeException("drop through")
}
private fun loadArguments(args: List<String>, isWorker: Boolean): List<String> {
if (args.isNotEmpty()) {
val lastArg = args[args.size - 1]
if (lastArg.startsWith("@")) {
val pathElement = lastArg.substring(1)
val flagFile = Paths.get(pathElement)
if (isWorker && lastArg.startsWith("@@") || Files.exists(flagFile)) {
if (!isWorker && !mnemonic.isEmpty()) {
output.printf(
"HINT: %s will compile faster if you run: " + "echo \"build --strategy=%s=worker\" >>~/.bazelrc\n",
mnemonic, mnemonic
)
}
try {
return Files.readAllLines(flagFile, UTF_8)
} catch (e: IOException) {
throw RuntimeException(e)
}
}
}
}
return args
}
private fun wasInterrupted(e: Throwable): Boolean {
val cause = e.rootCause
if (cause is InterruptedException || cause is InterruptedIOException) {
output.println("Terminating worker due to interrupt signal")
return true
}
return false
}
}