Search code examples
linuxkotlinprocessbuilderbattery

Getting no response from ProcessBuilder's input stream in some specific case


So I am trying to get battery status from linux, and so far the first command (path variable) returns perfectly and I am able to get its response in form of Sequence from the input stream, but unfortunately the second command (of result variable) returns empty sequence.

fun getLinuxBatteryStatus(): Nothing? {
    val path = """upower --enumerate""".runCommand() ?: return null

    val parameters = listOf("present", "state", "energy-full", "energy", "energy-rate", "time to empty", "percentage")
    val result = """upower -i ${path.first { "battery_BAT" in it }} | grep -E "${parameters.joinToString("|")}""""
        .also { println(it) }
        .runCommand() ?: return null

    result.forEach(::println)   // <- no ouput
    // println(result.count())  // <- 0

    /* Do other thing and return something (that is not related to problem) */
}

Ouput:

upower -i /org/freedesktop/UPower/devices/battery_BAT1 | grep -E "present|state|energy-full|energy|energy-rate|time to empty|percentage"

The above output is from the also block in the last command, just to preview the command's string for debugging. And if I run the above command directly into the terminal I am successfully getting the responses as follows:

    present:             yes
    state:               charging
    energy:              47.903 Wh
    energy-empty:        0 Wh
    energy-full:         50.299 Wh
    energy-full-design:  48.004 Wh
    energy-rate:         17.764 W
    percentage:          95%

Why is the last command not working (not returning any response) with the ProcessBuilder?

Note: the extension function runCommand has been taken from here

private fun String.runCommand(
    workingDir: File = File("."),
    timeoutAmount: Long = 60,
    timeoutUnit: TimeUnit = TimeUnit.SECONDS
): Sequence<String>? = try {
    ProcessBuilder(split("\\s".toRegex()))
        .directory(workingDir)
        .redirectOutput(ProcessBuilder.Redirect.PIPE)
        .redirectError(ProcessBuilder.Redirect.PIPE)
        .start()
        .apply { waitFor(timeoutAmount, timeoutUnit) }
        .inputStream.bufferedReader().lineSequence()
} catch (e: IOException) {
    e.printStackTrace()
    null
}

Solution

  • The problem here is the pipe.

    You're trying to run a pipeline — a construction involving running multiple programs, that needs a shell to interpret.

    But ProcessBuilder runs a single program.  In this case, it's running the program upower and passing it the parameters -i, /org/freedesktop/UPower/devices/battery_BAT1, |, grep, -E, and "present|state|energy-full|energy|energy-rate|time to empty|percentage".  Obviously upower won't know what to do with the | parameter or those after it.

    You could use ProcessBuilder to run up a shell instance, which could then run your pipeline; see this answer.

    But it would probably be simpler, safer, and more efficient to do the filtering in your own code, and avoid calling grep entirely.

    I recommend capturing the process's error output, which would very probably have made the problem clear.