WIP rework how we run processes in KGP

^KT-66517
diff --git a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/native/cocoapods/tasks/AbstractPodInstallTask.kt b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/native/cocoapods/tasks/AbstractPodInstallTask.kt
index 03803d2..78c150f 100644
--- a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/native/cocoapods/tasks/AbstractPodInstallTask.kt
+++ b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/native/cocoapods/tasks/AbstractPodInstallTask.kt
@@ -17,6 +17,7 @@
 import org.jetbrains.kotlin.gradle.utils.runCommand
 import org.jetbrains.kotlin.gradle.utils.runCommandWithFallback
 import java.io.File
+import java.nio.file.Files
 
 /**
  * The task takes the path to the Podfile and calls `pod install`
@@ -52,7 +53,7 @@
 
     @TaskAction
     open fun doPodInstall() {
-        runPodInstall(false)
+        runPodInstall()
 
         with(podsXcodeProjDirProvider.get()) {
             check(exists() && isDirectory) {
@@ -63,28 +64,66 @@
 
     private fun podExecutable(): String {
         return when (val podPath = podExecutablePath.orNull?.asFile) {
-            is File -> podPath.absolutePath.ifBlank { runWhichPod() }
-            else -> runWhichPod()
+            is File -> podPath.absolutePath.ifBlank { getPodExecutablePath() }
+            else -> getPodExecutablePath()
         }
     }
 
-    private fun runWhichPod(): String {
-        val checkPodCommand = listOf("which", "pod")
-        val output = runCommand(checkPodCommand, logger, { result ->
-            if (result.retCode == 1) {
-                missingPodsError()
-            } else {
-                sharedHandleError(checkPodCommand, result)
+    private fun getPodExecutablePath(): String {
+        val whichPodCommand = listOf("/usr/bin/which", "pod")
+        val output = mutableListOf<String>()
+        runCommand(
+            whichPodCommand,
+            logger,
+            onStdOutLine = { output.add(it) },
+            captureResult = CaptureCommandResult(
+                temporaryDirectory = Files.createTempDirectory("AbstractPodInstallTask").toFile().apply { deleteOnExit() },
+                onResult = { result ->
+                    if (result.retCode != 0) {
+
+                    }
+                }
+            ) { result ->
+//                if (result.retCode == 1) {
+//                    missingPodsError()
+//                } else {
+//                    sharedHandleError(checkPodCommand, result)
+//                }
             }
-        })
-
-        return output.removingTrailingNewline().ifBlank {
-            throw IllegalStateException(missingPodsError())
-        }
+        )
+        if (output.isEmpty()) throw IllegalStateException(missingPodsError())
+        return output[0]
     }
 
-    private fun runPodInstall(updateRepo: Boolean): String {
-        val podInstallCommand = listOfNotNull(podExecutable(), "install", if (updateRepo) "--repo-update" else null)
+    private fun runPodInstall() {
+        val podInstallCommand = listOf(podExecutable(), "install")
+
+        var isRepoOutOfDate = false
+        val result = runPodInstallCommand(
+            podInstallCommand,
+            onStdOutLine = {
+                if (it.contains("out-of-date source repos which you can update with `pod repo update` or with `pod install --repo-update`")) {
+                    isRepoOutOfDate = true
+                }
+            },
+            errorOnNonZeroExitCode = false
+        )
+
+        if (result.returnCode != 0) {
+            if (isRepoOutOfDate) {
+                logger.info("Retrying \"pod install\" with --repo-update")
+                var errorMessagePrefix =
+                runPodInstallCommand(
+                    podInstallCommand + listOf("--repo-update"),
+                    logger,
+                    onStdOutLine = { line ->
+
+                    }
+                )
+            } else {
+
+            }
+        }
 
         return runCommandWithFallback(podInstallCommand,
                                       logger,
@@ -96,13 +135,25 @@
                                               CommandFallback.Error(sharedHandleError(podInstallCommand, result))
                                           }
                                       },
-                                      processConfiguration = {
-                                          directory(workingDir.get())
-                                          // CocoaPods requires to be run with Unicode external encoding
-                                          environment().putIfAbsent("LC_ALL", "en_US.UTF-8")
-                                      })
+                                      )
     }
 
+    private fun runPodInstallCommand(
+        command: List<String>,
+        onStdOutLine: (String) -> Unit,
+        errorOnNonZeroExitCode: Boolean,
+    ): RunProcessResult = runCommand(
+        command,
+        logger,
+        onStdOutLine = onStdOutLine,
+        errorOnNonZeroExitCode = errorOnNonZeroExitCode,
+        processConfiguration = {
+            directory(workingDir.get())
+            // CocoaPods requires to be run with Unicode external encoding
+            environment().putIfAbsent("LC_ALL", "en_US.UTF-8")
+        }
+    )
+
     private fun sharedHandleError(podInstallCommand: List<String>, result: RunProcessResult): String? {
         val command = podInstallCommand.joinToString(" ")
         val output = result.stdErr.ifBlank { result.stdOut }
@@ -111,7 +162,7 @@
             |'$command' command failed with an exception:
             | stdErr: ${result.stdErr}
             | stdOut: ${result.stdOut}
-            | exitCode: ${result.retCode}
+            | exitCode: ${result.returnCode}
             |        
         """.trimMargin()
 
diff --git a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/native/cocoapods/tasks/PodBuildTask.kt b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/native/cocoapods/tasks/PodBuildTask.kt
index a5f9835..d25ce13 100644
--- a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/native/cocoapods/tasks/PodBuildTask.kt
+++ b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/native/cocoapods/tasks/PodBuildTask.kt
@@ -87,9 +87,13 @@
             "-configuration", podBuildSettings.configuration,
         )
 
-        runCommand(podXcodeBuildCommand, logger) {
-            directory(podsXcodeProjDir.asFile.parentFile)
-            environment() // workaround for https://github.com/gradle/gradle/issues/27346
-        }
+        runCommand(
+            podXcodeBuildCommand,
+            logger,
+            processConfiguration = {
+                directory(podsXcodeProjDir.asFile.parentFile)
+                environment() // workaround for https://github.com/gradle/gradle/issues/27346
+            }
+        )
     }
 }
diff --git a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/native/cocoapods/tasks/PodInstallSyntheticTask.kt b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/native/cocoapods/tasks/PodInstallSyntheticTask.kt
index 61a07b2..675e8a3 100644
--- a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/native/cocoapods/tasks/PodInstallSyntheticTask.kt
+++ b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/native/cocoapods/tasks/PodInstallSyntheticTask.kt
@@ -45,7 +45,7 @@
 
     override fun handleError(result: RunProcessResult): String? {
         var message = """
-            |'pod install' command on the synthetic project failed with return code: ${result.retCode}
+            |'pod install' command on the synthetic project failed with return code: ${result.returnCode}
             |
             |        Error: ${result.stdErr.lines().filter { it.contains("[!]") }.joinToString("\n")}
             |       
diff --git a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/native/cocoapods/tasks/PodInstallTask.kt b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/native/cocoapods/tasks/PodInstallTask.kt
index 12eba64..ae19c9c 100644
--- a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/native/cocoapods/tasks/PodInstallTask.kt
+++ b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/native/cocoapods/tasks/PodInstallTask.kt
@@ -42,7 +42,7 @@
         val cocoapodsMessages = pods.get().map { MissingCocoapodsMessage(it).missingMessage }
 
         return listOfNotNull(
-            "'pod install' command failed with code ${result.retCode}.",
+            "'pod install' command failed with code ${result.returnCode}.",
             "Error message:",
             result.stdErr.lines().filter { it.isNotBlank() }.joinToString("\n"),
             """
diff --git a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/native/cocoapods/tasks/PodSetupBuildTask.kt b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/native/cocoapods/tasks/PodSetupBuildTask.kt
index d7c0870..bf2789d 100644
--- a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/native/cocoapods/tasks/PodSetupBuildTask.kt
+++ b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/native/cocoapods/tasks/PodSetupBuildTask.kt
@@ -51,7 +51,13 @@
             "-sdk", appleTarget.get().sdk,
         )
 
-        val outputText = runCommand(buildSettingsReceivingCommand, logger) { directory(podsXcodeProjDir.parentFile) }
+        val outputText = runCommand(
+            buildSettingsReceivingCommand,
+            logger,
+            processConfiguration = {
+                directory(podsXcodeProjDir.parentFile)
+            }
+        )
 
         val buildSettingsProperties = PodBuildSettingsProperties.readSettingsFromReader(outputText.reader())
         buildSettingsFile.getFile().let { bsf ->
diff --git a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/utils/ProcessUtils.kt b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/utils/ProcessUtils.kt
index 5bdf379..195d1c8 100644
--- a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/utils/ProcessUtils.kt
+++ b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/utils/ProcessUtils.kt
@@ -6,102 +6,158 @@
 package org.jetbrains.kotlin.gradle.utils
 
 import org.gradle.api.logging.Logger
+import java.io.File
+import java.nio.file.Files
+import java.util.LinkedList
+import java.util.concurrent.LinkedBlockingQueue
 import kotlin.concurrent.thread
 
-/**
- * Represents the result of running a process.
- *
- * @property stdOut The standard output of the process.
- * @property stdErr The standard error of the process.
- * @property retCode The return code of the process.
- * @property process The underlying `Process` object.
- */
-data class RunProcessResult(
-    val stdOut: String,
-    val stdErr: String,
-    val retCode: Int,
+
+class RunProcessResult(
+    var output: File,
+    val returnCode: Int,
     val process: Process,
-)
+    private val command: List<String>,
+) {
+    /**
+     * Write the full message to the logger, but trim the exception to prevent KT-66517
+     */
+    fun errorOnNonZeroExitCode(
+        headerMessage: String?,
+        logger: Logger,
+    ) {
+        if (returnCode == 0) return
+        val errorMessage = buildString {
+            headerMessage?.let {
+                logger.error(it)
+                appendLine(it)
+            }
 
-/**
- * Executes a command and returns the input text.
- *
- * @param command the command and its arguments to be executed as a list of strings.
- * @param logger an optional logger to log information about the command execution.
- * @param errorHandler (Optional) A function that handles any errors that occur during the command execution.
- * @param processConfiguration a function to configure the process before execution.
- * @return The input text of the executed command.
- */
-internal fun runCommand(
-    command: List<String>,
-    logger: Logger? = null,
-    errorHandler: ((result: RunProcessResult) -> String?)? = null,
-    processConfiguration: ProcessBuilder.() -> Unit = { },
-): String {
-    val runResult = assembleAndRunProcess(command, logger, processConfiguration)
-    check(runResult.retCode == 0) {
-        errorHandler?.invoke(runResult) ?: createErrorMessage(command, runResult)
+            val commandFailedMessage = "Executing of '${command.joinToString(" ")}' failed with code ${returnCode} and message:\n"
+            logger.error(commandFailedMessage)
+            appendLine(commandFailedMessage)
+
+            val outputLimit = 100
+            var hasOverflown = false
+            val buffer = LinkedList<String>()
+            output.reader().forEachLine { line ->
+                logger.error(line)
+                buffer.addLast(line)
+                if (buffer.size > outputLimit) {
+                    hasOverflown = true
+                    buffer.removeFirst()
+                }
+            }
+            if (hasOverflown) {
+                appendLine("... last ${outputLimit} lines shown, see error log for the full output ...")
+            }
+            buffer.forEach { appendLine(it) }
+        }
+        error(errorMessage)
     }
-
-    return runResult.stdOut
 }
 
-/**
- * Sealed class representing the fallback behavior for a command.
- */
+internal fun runCommand(
+    command: List<String>,
+    logger: Logger,
+    processConfiguration: ProcessBuilder.() -> Unit = { },
+    onStdOutLine: ((line: String) -> Unit)? = null,
+    onStdErrLine: ((line: String) -> Unit)? = null,
+    errorOnNonZeroExitCode: Boolean = true,
+): RunProcessResult {
+    val result = assembleAndRunProcess(command, logger, processConfiguration)
+    if (errorOnNonZeroExitCode && result.returnCode != 0) {
+        result.errorOnNonZeroExitCode(
+            headerMessage = null,
+            logger = logger,
+        )
+    }
+    return result
+}
+
 sealed class CommandFallback {
     data class Action(val fallback: String) : CommandFallback()
     data class Error(val error: String?) : CommandFallback()
 }
 
-/**
- * Executes the specified command with fallback behavior in case of non-zero return code.
- *
- * @param command the command and its arguments to be executed as a list of strings.
- * @param logger an optional logger to log information about the command execution.
- * @param fallback a function that provides the fallback behavior. It takes the return code, output, and process as parameters and returns a [CommandFallback] object.
- * @param processConfiguration a function to configure the process before execution.
- * @return the output of the command if the return code is 0, otherwise the fallback action or error.
- */
 internal fun runCommandWithFallback(
     command: List<String>,
     logger: Logger? = null,
     fallback: (result: RunProcessResult) -> CommandFallback,
     processConfiguration: ProcessBuilder.() -> Unit = { },
-): String {
+) {
     val runResult = assembleAndRunProcess(command, logger, processConfiguration)
-    return if (runResult.retCode != 0) {
+    if (runResult.returnCode != 0) {
         when (val fallbackOption = fallback(runResult)) {
             is CommandFallback.Action -> fallbackOption.fallback
             is CommandFallback.Error -> error(fallbackOption.error ?: createErrorMessage(command, runResult))
         }
-    } else {
-        runResult.stdOut
     }
 }
 
+private enum class StreamMessage {
+    STDOUT,
+    STDERR,
+    STDOUT_EOF,
+    STDERR_EOF
+}
+
 private fun assembleAndRunProcess(
     command: List<String>,
     logger: Logger? = null,
     processConfiguration: ProcessBuilder.() -> Unit = { },
+    onStdOutLine: (line: String) -> Unit = {},
+    onStdErrLine: (line: String) -> Unit = {},
 ): RunProcessResult {
-
     val process = ProcessBuilder(command).apply {
         this.processConfiguration()
     }.start()
+    val temporaryDirectory = Files.createTempDirectory("${process.pid()}").toFile().apply { deleteOnExit() }
 
-    var inputText = ""
-    var errorText = ""
+    logger?.info("Information about \"${command.joinToString(" ")}\" call:\n")
 
+    val outputFile = temporaryDirectory.resolve("output")
+    val queue = LinkedBlockingQueue<Pair<String, StreamMessage>>()
     val inputThread = thread {
-        inputText = process.inputStream.use {
-            it.reader().readText()
+        process.inputStream.use {
+            it.reader().forEachLine { line ->
+                queue.put(Pair(line, StreamMessage.STDOUT))
+            }
         }
+        queue.put(Pair("", StreamMessage.STDOUT_EOF))
+    }
+    val errorThread = thread {
+        process.errorStream.use {
+            it.reader().forEachLine { line ->
+                queue.put(Pair(line, StreamMessage.STDERR))
+            }
+        }
+        queue.put(Pair("", StreamMessage.STDERR_EOF))
     }
 
-    val errorThread = thread {
-        errorText = process.errorStream.use {
-            it.reader().readText()
+    var continueReadingStdout = true
+    var continueReadingStderr = true
+    outputFile.writer().use { writer ->
+        while (continueReadingStdout || continueReadingStderr) {
+            val (line, state) = queue.take()
+            when (state) {
+                StreamMessage.STDOUT_EOF -> {
+                    continueReadingStdout = false
+                }
+                StreamMessage.STDERR_EOF -> {
+                    continueReadingStderr = false
+                }
+                StreamMessage.STDOUT -> {
+                    writer.appendLine(line)
+                    onStdOutLine(line)
+                    logger?.info(line)
+                }
+                StreamMessage.STDERR -> {
+                    writer.appendLine(line)
+                    onStdErrLine(line)
+                    logger?.error(line)
+                }
+            }
         }
     }
 
@@ -109,24 +165,11 @@
     errorThread.join()
 
     val retCode = process.waitFor()
-    logger?.info(
-        """
-            |Information about "${command.joinToString(" ")}" call:
-            |
-            |${inputText}
-        """.trimMargin()
+
+    return RunProcessResult(
+        outputFile,
+        retCode,
+        process,
+        command,
     )
-
-    return RunProcessResult(inputText, errorText, retCode, process)
-}
-
-private fun createErrorMessage(command: List<String>, runResult: RunProcessResult): String {
-    return """
-           |Executing of '${command.joinToString(" ")}' failed with code ${runResult.retCode} and message: 
-           |
-           |${runResult.stdOut}
-           |
-           |${runResult.stdErr}
-           |
-           """.trimMargin()
 }
\ No newline at end of file