Skip to content

Commit

Permalink
Try to automatically recover from toolchain install failures
Browse files Browse the repository at this point in the history
Issue: #37
  • Loading branch information
dtretyakov committed Dec 28, 2017
1 parent a9b2d18 commit 91924a7
Show file tree
Hide file tree
Showing 8 changed files with 266 additions and 21 deletions.
2 changes: 2 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ allprojects {
subprojects {
apply plugin: "kotlin"

kotlin { experimental { coroutines 'enable' } }

test.useTestNG()
jar.version = null

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,15 @@ package jetbrains.buildServer.rust

import jetbrains.buildServer.agent.AgentBuildRunnerInfo
import jetbrains.buildServer.agent.BuildAgentConfiguration
import jetbrains.buildServer.agent.runner.CommandLineBuildService
import jetbrains.buildServer.agent.runner.CommandLineBuildServiceFactory
import jetbrains.buildServer.agent.BuildRunnerContext
import jetbrains.buildServer.agent.runner.MultiCommandBuildSessionFactory

/**
* Cargo runner service factory.
*/
class CargoRunnerBuildServiceFactory(private val commandExecutor: CommandExecutor)
: CommandLineBuildServiceFactory {
class CargoBuildSessionFactory : MultiCommandBuildSessionFactory {

override fun createService(): CommandLineBuildService {
return CargoRunnerBuildService(commandExecutor)
}
override fun createSession(runnerContext: BuildRunnerContext) = CargoCommandBuildSession(runnerContext)

override fun getBuildRunnerInfo(): AgentBuildRunnerInfo {
return object : AgentBuildRunnerInfo {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
/*
* Copyright 2000-2017 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License").
* See LICENSE in the project root for license information.
*/

package jetbrains.buildServer.rust

import jetbrains.buildServer.agent.BuildFinishedStatus
import jetbrains.buildServer.agent.BuildRunnerContext
import jetbrains.buildServer.agent.runner.CommandExecution
import jetbrains.buildServer.agent.runner.CommandLineBuildService
import jetbrains.buildServer.agent.runner.MultiCommandBuildSession
import jetbrains.buildServer.util.FileUtil
import java.io.File
import kotlin.coroutines.experimental.buildIterator

/**
* Cargo runner service.
*/
class CargoCommandBuildSession(private val runnerContext: BuildRunnerContext) : MultiCommandBuildSession {

private var buildSteps: Iterator<CommandExecution>? = null
private var lastCommands = arrayListOf<CommandExecutionAdapter>()

override fun sessionStarted() {
buildSteps = getSteps()
}

override fun getNextCommand(): CommandExecution? {
buildSteps?.let {
if (it.hasNext()) {
return it.next()
}
}

return null
}

override fun sessionFinished(): BuildFinishedStatus? {
return lastCommands.last().result
}

private fun getSteps() = buildIterator<CommandExecution> {
runnerContext.runnerParameters[CargoConstants.PARAM_TOOLCHAIN]?.let {
if (it.isNotBlank()) {
val installToolchain = RustupBuildService("install")
yield(addCommand(installToolchain))

// Rustup could fail to install toolchain
// We could try to resolve it by execution uninstall of toolchain
// and cleaning up temporary directories
if (installToolchain.errors.isNotEmpty()) {
val logger = runnerContext.build.buildLogger
logger.message("Installation has failed, will remove toolchain '${installToolchain.version}' and try again")

val uninstallToolchain = RustupBuildService("uninstall")
yield(addCommand(uninstallToolchain))

val rustupCache = File(System.getProperty("user.home"), ".rustup")
installToolchain.version.let {
// Cleanup temp directories
FileUtil.delete(File(rustupCache, CargoConstants.RUSTUP_DOWNLOADS_DIR))
FileUtil.delete(File(rustupCache, CargoConstants.RUSTUP_TMP_DIR))

// Remove toolchain files
FileUtil.delete(File(rustupCache, "${CargoConstants.RUSTUP_TOOLCHAINS_DIR}/$it"))
FileUtil.delete(File(rustupCache, "${CargoConstants.RUSTUP_HASHES_DIR}/$it"))
}

yield(addCommand(RustupBuildService("install")))
}
}
}

yield(addCommand(CargoRunnerBuildService()))
}

private fun addCommand(buildService: CommandLineBuildService) = CommandExecutionAdapter(buildService.apply {
this.initialize(runnerContext.build, runnerContext)
}).apply {
lastCommands.add(this)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,16 @@ import jetbrains.buildServer.agent.runner.BuildServiceAdapter
import jetbrains.buildServer.agent.runner.ProcessListener
import jetbrains.buildServer.agent.runner.ProgramCommandLine
import jetbrains.buildServer.rust.cargo.*
import jetbrains.buildServer.rust.logging.BlockListener
import jetbrains.buildServer.rust.logging.CargoLoggerFactory
import jetbrains.buildServer.rust.logging.CargoLoggingListener
import jetbrains.buildServer.util.StringUtil

/**
* Cargo runner service.
*/
class CargoRunnerBuildService(private val commandExecutor: CommandExecutor) : BuildServiceAdapter() {
class CargoRunnerBuildService : BuildServiceAdapter() {

private val osName = System.getProperty("os.name").toLowerCase()
private val myCargoWithStdErrVersion = Version.forIntegers(0, 13)
private val myArgumentsProviders = mapOf(
Expand Down Expand Up @@ -59,10 +61,6 @@ class CargoRunnerBuildService(private val commandExecutor: CommandExecutor) : Bu
val toolchainVersion = parameters[CargoConstants.PARAM_TOOLCHAIN]?.trim() ?: ""
val (toolPath, arguments) = if (toolchainVersion.isNotEmpty()) {
val rustupPath = getPath(CargoConstants.RUSTUP_CONFIG_NAME)

logger.message("Using rust toolchain: $toolchainVersion")
commandExecutor.executeWithReadLock(rustupPath, arrayListOf("toolchain", "install", toolchainVersion))

rustupPath to argumentsProvider.getArguments(runnerContext).toMutableList().apply {
addAll(0, arrayListOf("run", toolchainVersion, "cargo"))
}
Expand All @@ -72,24 +70,22 @@ class CargoRunnerBuildService(private val commandExecutor: CommandExecutor) : Bu

runnerContext.configParameters[CargoConstants.CARGO_CONFIG_NAME]?.let {
if (Version.valueOf(it).greaterThanOrEqualTo(myCargoWithStdErrVersion)) {
if (osName.startsWith("windows")) {
return createProgramCommandline("cmd.exe", arrayListOf("/c", "2>&1", toolPath).apply {
return if (osName.startsWith("windows")) {
createProgramCommandline("cmd.exe", arrayListOf("/c", "2>&1", toolPath).apply {
addAll(arguments)
})
} else if (osName.startsWith("freebsd") || osName.startsWith("sunos")) {
return createProgramCommandline("sh",
arrayListOf("-c", "$toolPath ${arguments.joinToString(" ")} 2>&1"))
createProgramCommandline("sh", arrayListOf("-c", "$toolPath ${arguments.joinToString(" ")} 2>&1"))
} else {
return createProgramCommandline("bash",
arrayListOf("-c", "$toolPath ${arguments.joinToString(" ")} 2>&1"))
createProgramCommandline("bash", arrayListOf("-c", "$toolPath ${arguments.joinToString(" ")} 2>&1"))
}
}
}

return createProgramCommandline(toolPath, arguments)
}

fun getPath(toolName: String): String {
private fun getPath(toolName: String): String {
try {
return getToolPath(toolName)
} catch (e: ToolCannotBeFoundException) {
Expand All @@ -103,6 +99,10 @@ class CargoRunnerBuildService(private val commandExecutor: CommandExecutor) : Bu

override fun getListeners(): List<ProcessListener> {
val loggerFactory = CargoLoggerFactory(logger)
return listOf<ProcessListener>(CargoLoggingListener(loggerFactory))
val blockName = "cargo ${runnerParameters[CargoConstants.PARAM_COMMAND]}"
return listOf(
CargoLoggingListener(loggerFactory),
BlockListener(blockName, logger)
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
/*
* Copyright 2000-2017 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License").
* See LICENSE in the project root for license information.
*/

package jetbrains.buildServer.rust

import jetbrains.buildServer.agent.BuildFinishedStatus
import jetbrains.buildServer.agent.runner.*
import java.io.File

class CommandExecutionAdapter(private val buildService: CommandLineBuildService) : CommandExecution {

private val processListeners by lazy { buildService.listeners }

var result: BuildFinishedStatus? = null
private set

override fun processFinished(exitCode: Int) {
buildService.afterProcessFinished()

processListeners.forEach {
it.processFinished(exitCode)
}

result = buildService.getRunResult(exitCode)
if (result == BuildFinishedStatus.FINISHED_SUCCESS) {
buildService.afterProcessSuccessfullyFinished()
}
}

override fun processStarted(programCommandLine: String, workingDirectory: File) {
processListeners.forEach {
it.processStarted(programCommandLine, workingDirectory)
}
}

override fun onStandardOutput(text: String) {
processListeners.forEach {
it.onStandardOutput(text)
}
}

override fun onErrorOutput(text: String) {
processListeners.forEach {
it.onErrorOutput(text)
}
}

override fun interruptRequested(): TerminationAction {
return buildService.interrupt()
}

override fun makeProgramCommandLine(): ProgramCommandLine {
return buildService.makeProgramCommandLine()
}

override fun isCommandLineLoggingEnabled() = buildService.isCommandLineLoggingEnabled

override fun beforeProcessStarted() {
buildService.beforeProcessStarted()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
/*
* Copyright 2000-2017 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License").
* See LICENSE in the project root for license information.
*/

package jetbrains.buildServer.rust

import jetbrains.buildServer.RunBuildException
import jetbrains.buildServer.agent.ToolCannotBeFoundException
import jetbrains.buildServer.agent.runner.BuildServiceAdapter
import jetbrains.buildServer.agent.runner.ProcessListener
import jetbrains.buildServer.agent.runner.ProcessListenerAdapter
import jetbrains.buildServer.agent.runner.ProgramCommandLine
import jetbrains.buildServer.rust.logging.BlockListener

/**
* Rustup runner service.
*/
class RustupBuildService(private val action: String) : BuildServiceAdapter() {

val errors = arrayListOf<String>()
var foundVersion: String? = null

val version: String
get() = foundVersion ?: runnerParameters[CargoConstants.PARAM_TOOLCHAIN]!!

override fun makeProgramCommandLine(): ProgramCommandLine {
val toolchainVersion = runnerParameters[CargoConstants.PARAM_TOOLCHAIN]!!.trim()
val rustupPath = getPath(CargoConstants.RUSTUP_CONFIG_NAME)

return createProgramCommandline(rustupPath, arrayListOf("toolchain", action, toolchainVersion))
}

private fun getPath(toolName: String): String {
try {
return getToolPath(toolName)
} catch (e: ToolCannotBeFoundException) {
val buildException = RunBuildException(e)
buildException.isLogStacktrace = false
throw buildException
}
}

override fun isCommandLineLoggingEnabled() = false

override fun getListeners(): MutableList<ProcessListener> {
return arrayListOf<ProcessListener>().apply {
val blockName = "$action toolchain: ${runnerParameters[CargoConstants.PARAM_TOOLCHAIN]}"
this.add(BlockListener(blockName, logger))
this.add(object : ProcessListenerAdapter() {
override fun onStandardOutput(text: String) {
processOutput(text)
}

override fun onErrorOutput(text: String) {
processOutput(text)
}
})
}
}

fun processOutput(text: String) {
if (text.startsWith("error:")) {
errors.add(text)
}

toolchainVersion.matchEntire(text)?.let {
foundVersion = it.groupValues.last()
}

logger.message(text)
}

companion object {
val toolchainVersion = Regex("info: syncing channel updates for '([^']+)'")
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package jetbrains.buildServer.rust.logging

import jetbrains.buildServer.agent.BuildProgressLogger
import jetbrains.buildServer.agent.runner.ProcessListenerAdapter
import java.io.File

class BlockListener(private val blockName:String,
private val logger: BuildProgressLogger) : ProcessListenerAdapter() {

override fun processStarted(programCommandLine: String, workingDirectory: File) {
logger.message("##teamcity[blockOpened name='$blockName']")
}

override fun processFinished(exitCode: Int) {
logger.message("##teamcity[blockClosed name='$blockName']")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd"
default-autowire="constructor">
<bean class="jetbrains.buildServer.rust.CargoRunnerBuildServiceFactory"/>
<bean class="jetbrains.buildServer.rust.CargoBuildSessionFactory"/>
<bean class="jetbrains.buildServer.rust.CargoToolProvider"/>
<bean class="jetbrains.buildServer.rust.RustCommandExecutor"/>
<bean class="jetbrains.buildServer.rust.RustcToolProvider"/>
Expand Down

0 comments on commit 91924a7

Please sign in to comment.