You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何用Kotlin实现与sh进程的真正交互式交互?

实现与sh进程的真正交互式交互

我正尝试与sh进程进行交互,以下是现有实现的局限及解决方案:

现有实现的问题

1. 最简实现(无控制能力)

这段代码通过inheritIO()让进程共享终端IO,但完全无法自定义输入输出的处理逻辑,无法实现可控的交互:

typealias ExitStatus = Int

fun executeIO(cmd: List<String>): ExitStatus =
    ProcessBuilder(cmd).inheritIO().start().waitFor()

2. 固定写入次数的实现(非真正交互)

该实现能多次写入输入,但输出会被缓冲,必须等输入循环结束后才批量返回结果,不符合实时交互的要求:

fun executeNTimes(cmd: List<String>, times: Int) = runBlocking {
    val process = ProcessBuilder(cmd)
        .start()

    launch { process.errorStream.bufferedReader().useLines { seq -> seq.toList().forEach { println("stderr: $it") } } }
    launch { process.inputStream.bufferedReader().useLines { seq -> seq.toList().forEach { println("stdout: $it") } } }

    OutputStreamWriter(process.outputStream, "UTF-8").use { w ->
        repeat(times) {
            readln().let { println("input: $it"); w.write(it) }
            w.appendLine()
            w.flush()
        }
    }
    process.waitFor()
}

执行效果(cmd=sh,times=2)

echo exit on stdout
input: echo exit on stdout
echo exit on stderr 1>&2 
input: echo exit on stderr 1>&2
stderr: exit on stderr
stdout: exit on stdout

问题核心:sh在非终端环境下会使用块缓冲,导致输出无法实时返回,必须等到缓冲满或进程结束才会输出内容。

期望的实时交互效果

input: echo exit on stdout
stdout: exit on stdout
input: echo exit on stderr 1>&2
stderr: exit on stderr
input: exit

解决方案:实时流式处理输入输出

要实现真正的交互式,需解决两个核心问题:禁用sh的块缓冲、实时处理输入输出流。

完整Kotlin代码实现

import kotlinx.coroutines.*
import java.io.*

fun executeInteractive(cmd: List<String>) = runBlocking {
    // 用stdbuf强制sh使用无缓冲模式,确保输出实时返回
    val command = listOf("stdbuf", "-i0", "-o0", "-e0") + cmd
    val process = ProcessBuilder(command)
        .start()

    // 实时读取并打印stdout
    launch(Dispatchers.IO) {
        process.inputStream.bufferedReader().use { reader ->
            var line: String?
            while (reader.readLine().also { line = it } != null) {
                println("stdout: $line")
            }
        }
    }

    // 实时读取并打印stderr
    launch(Dispatchers.IO) {
        process.errorStream.bufferedReader().use { reader ->
            var line: String?
            while (reader.readLine().also { line = it } != null) {
                println("stderr: $line")
            }
        }
    }

    // 持续读取用户输入并实时写入进程
    launch(Dispatchers.IO) {
        OutputStreamWriter(process.outputStream, "UTF-8").use { writer ->
            val consoleReader = BufferedReader(InputStreamReader(System.`in`))
            var input: String?
            while (consoleReader.readLine().also { input = it } != null) {
                println("input: $input")
                writer.write(input)
                writer.write(System.lineSeparator())
                writer.flush()
                // 进程退出则终止输入循环
                if (!process.isAlive) break
            }
        }
    }

    // 等待进程结束并输出退出码
    val exitCode = process.waitFor()
    println("Process exited with code: $exitCode")
}

// 使用示例
fun main() {
    executeInteractive(listOf("sh"))
}

关键细节说明

  • stdbuf命令:-i0禁用输入缓冲,-o0禁用标准输出缓冲,-e0禁用错误输出缓冲,强制sh实时输出内容,而非攒满缓冲块才输出。
  • 实时流读取:两个协程分别循环读取stdout和stderr的每一行,读取到内容立即打印,实现实时响应。
  • 持续输入循环:不限制输入次数,持续读取控制台输入直到用户中断或进程退出,每次写入后立即flush()确保输入实时传递给sh。
  • 进程存活检查:每次输入后判断进程状态,避免向已退出的进程写入数据引发异常。

验证效果

运行代码后会得到符合预期的实时交互效果:

input: echo exit on stdout
stdout: exit on stdout
input: echo exit on stderr 1>&2
stderr: exit on stderr
input: exit
Process exited with code: 0

内容的提问来源于stack exchange,提问作者Arnau Sistach Reinoso

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.28 09:25:46