All Downloads are FREE. Search and download functionalities are using the official Maven repository.

toolkit.utils.common-utils.33.0.0.source-code.ProcessCapture.kt Maven / Gradle / Ivy

Go to download

Part of the OSS Review Toolkit (ORT), a suite to automate software compliance checks.

There is a newer version: 41.0.0
Show newest version
/*
 * Copyright (C) 2017 The ORT Project Authors (see )
 *
 * 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
 *
 *     https://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.
 *
 * SPDX-License-Identifier: Apache-2.0
 * License-Filename: LICENSE
 */

package org.ossreviewtoolkit.utils.common

import java.io.File
import java.io.IOException

import kotlin.io.path.createTempDirectory

import org.apache.logging.log4j.kotlin.logger

/**
 * An (almost) drop-in replacement for ProcessBuilder that is able to capture huge outputs to the standard output and
 * standard error streams by redirecting output to temporary files.
 */
class ProcessCapture(
    vararg command: CharSequence,
    workingDir: File? = null,
    environment: Map = emptyMap()
) {
    // A convenience constructor to avoid the need for a named parameter if only the [workingDir] argument needs to be
    // specified. Even in unambiguous cases Kotlin unfortunately requires named parameters for arguments that follow
    // vararg parameters, see https://stackoverflow.com/a/46456379/1127485.
    constructor(workingDir: File?, vararg command: CharSequence) : this(*command, workingDir = workingDir)

    companion object {
        private const val MAX_OUTPUT_LINES = 20
        private const val MAX_OUTPUT_FOOTER =
            "(Above output is limited to each $MAX_OUTPUT_LINES heading and tailing lines.)"

        private fun limitOutputLines(message: String): String {
            val lines = message.lines()
            val lineCount = lines.size

            return if (lineCount > MAX_OUTPUT_LINES * 2) {
                val prefix = lines.take(MAX_OUTPUT_LINES)
                val suffix = lines.takeLast(MAX_OUTPUT_LINES)
                val skippedLineCount = lineCount - MAX_OUTPUT_LINES * 2

                // Insert an ellipsis in the middle of a long message.
                (prefix + "[...skipping $skippedLineCount lines...]" + suffix + MAX_OUTPUT_FOOTER).joinToString("\n")
            } else {
                message
            }
        }
    }

    private val tempDir = createTempDirectory(javaClass.simpleName).toFile().apply { deleteOnExit() }
    private val commandName = command.first().toString().substringAfterLast(File.separatorChar)
    private val stdoutFile = tempDir.resolve("$commandName.stdout").apply { deleteOnExit() }
    private val stderrFile = tempDir.resolve("$commandName.stderr").apply { deleteOnExit() }

    /**
     * Get the standard output stream of the terminated process as a string.
     */
    val stdout
        get() = stdoutFile.readText()

    /**
     * Get the standard error stream of the terminated process as a string.
     */
    val stderr
        get() = stderrFile.readText()

    private val builder = ProcessBuilder(*unmaskedStrings(*command))
        .directory(workingDir)
        .redirectOutput(stdoutFile)
        .redirectError(stderrFile)
        .apply {
            // Apply the environment filter to the map with inherited variables, but not to the variables added
            // explicitly via the environment parameter, since these are expected to be always relevant for the command.
            EnvironmentVariableFilter.filter(environment()).putAll(environment)
        }

    val commandLine = command.joinToString(" ")
    val usedWorkingDir = builder.directory() ?: System.getProperty("user.dir")!!

    private val process = builder.start()

    /**
     * Get the exit value of the terminated process.
     */
    val exitValue
        get() = process.exitValue()

    /**
     * Is true if the process terminated with an error, i.e. the [exitValue] is not 0.
     */
    val isError
        get() = exitValue != 0

    /**
     * Is true if the process terminated without an error, i.e. the [exitValue] is 0.
     */
    val isSuccess
        get() = exitValue == 0

    /**
     * A generic error message, can be used when [exitValue] is not 0.
     */
    val errorMessage
        get(): String {
            // Fall back to stdout for the error message if stderr does not provide meaningful information.
            val message = stderr.takeUnless {
                val notContainsErrorButStdoutDoes =
                    !it.contains("error", ignoreCase = true) && stdout.contains("error", ignoreCase = true)
                it.isBlank() || notContainsErrorButStdoutDoes
            } ?: stdout

            return "Running '$commandLine' in '$usedWorkingDir' failed with exit code $exitValue:\n" +
                limitOutputLines(message)
        }

    init {
        logger.info {
            "Running '$commandLine' in '$usedWorkingDir'..."
        }

        process.waitFor()

        if (logger.delegate.isDebugEnabled) {
            // No need to use curly-braces-syntax for logging below as the log level check is already done above.

            if (stdoutFile.length() > 0L) {
                limitOutputLines(stdout).lines().forEach { logger.debug(it) }
            }

            if (stderrFile.length() > 0L) {
                limitOutputLines(stderr).lines().forEach { logger.debug(it) }
            }
        }
    }

    /**
     * Throw an [IOException] in case [exitValue] is not 0.
     */
    fun requireSuccess() = also { if (isError) throw IOException(errorMessage) }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy