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

dorkbox.executor.processResults.AsyncProcessOutput.kt Maven / Gradle / Ivy

Go to download

Shell, JVM, and SSH command execution on Linux, MacOS, or Windows for Java 8+

The newest version!
/*
 * Copyright 2020 dorkbox, llc
 * Copyright (C) 2014 ZeroTurnaround 
 * Contains fragments of code from Apache Commons Exec, rights owned
 * by Apache Software Foundation (ASF).
 *
 * 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
 *
 *     http://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.
 */

package dorkbox.executor.processResults

import dorkbox.executor.stream.PumpStreamHandler
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.channels.ClosedReceiveChannelException
import kotlinx.coroutines.yield
import java.io.ByteArrayOutputStream
import java.io.UnsupportedEncodingException
import java.nio.charset.Charset
import java.util.concurrent.atomic.AtomicInteger

/**
 * Standard output of a finished process.
 *
 * @param channel Channel containing the async process output.
 */
open class AsyncProcessOutput(private val channel: Channel, private val processResult: SyncProcessResult?) {
    companion object {
        private val NEW_LINE_WIN = "\r".toCharArray().first().code
        private val NEW_LINE_NIX = "\n".toCharArray().first().code
    }

    @OptIn(ExperimentalCoroutinesApi::class)
    val isOpen: Boolean
    get() {
        return !channel.isClosedForReceive
    }


    var previousValue = AtomicInteger(-1)

    private suspend fun getBuffered(): ByteArray {
        // if the process has FINISHED running, then we have to get the output a different way
        val out = ByteArrayOutputStream(PumpStreamHandler.DEFAULT_SIZE)

        val value = previousValue.getAndSet(-1)
        if (value != -1) {
            out.write(value)
        }

        var toInt: Int
        try {
            while (true) {
                toInt = channel.receive().toInt()
                if (toInt == NEW_LINE_NIX) {
                    // now return the output.
                    break
                }

                if (toInt == NEW_LINE_WIN) {
                    // do we have a *nix line also? /r/n needs to be managed
                    toInt = channel.receive().toInt()

                    if (toInt != NEW_LINE_NIX) {
                        // whoops, not /n, save this
                        previousValue.set(toInt)
                    }

                    // now return the output.
                    break
                } else {
                    // save this to our buffer and keep going!
                    out.write(toInt)
                    yield()
                }
            }
        } catch (ignored: ClosedReceiveChannelException) {
            // the process closed. Read the output from the processResult (if it was defined)
            // The processResult is defined when the process exits.
            val internalBytes = processResult?.output?.bytes_
            if (internalBytes != null) {
                if (out.size() == 0) {
                    return internalBytes
                }
                else {
                    out.write(internalBytes, 0, internalBytes.size)
                }
            }
        }

        return out.toByteArray()
    }

    // instantly get the data in the buffer instead of waiting for a newline
    private suspend fun getRaw(): ByteArray {
        // if the process has FINISHED running, then we have to get the output a different way
        val out = ByteArrayOutputStream(2)

        val value = previousValue.getAndSet(-1)
        if (value != -1) {
            out.write(value)
        }

        try {
            val toInt = channel.receive().toInt()
            if (value != 1) {
                out.write(toInt)
            } else {
                // if we're the only thing, then write us out directly
                return byteArrayOf(toInt.toByte())
            }
        } catch (ignored: ClosedReceiveChannelException) {
            // the process closed. Read the output from the processResult (if it was defined)
            // The processResult is defined when the process exits.
            val internalBytes = processResult?.output?.bytes_
            if (internalBytes != null) {
                if (out.size() == 0) {
                    return internalBytes
                }
                else {
                    out.write(internalBytes, 0, internalBytes.size)
                }
            }
        }

        return out.toByteArray()
    }

    /**
     * @return output of the finished process converted to a String using platform's default encoding.
     */
    suspend fun string(): String {
        return String(getRaw())
    }

    private suspend fun stringBuffered(): String {
        return String(getBuffered())
    }

    /**
     * @return output of the process converted to UTF-8 String.
     */
    suspend fun utf8(): String {
        return string(charset = Charsets.UTF_8)
    }

    /**
     * @return buffered output of the process converted to UTF-8 String.
     */
    suspend fun utf8Buffered(): String {
        return stringBuffered(charset = Charsets.UTF_8)
    }

    /**
     * @param charset The name of a supported char set.
     *
     * @return output of the process converted to a String.
     *
     * @throws IllegalStateException if the char set was not supported.
     */
    @Throws(IllegalStateException::class)
    suspend fun string(charset: Charset): String {
        return try {
            String(getRaw(), charset)
        } catch (e: UnsupportedEncodingException) {
            throw IllegalStateException(e.message)
        }
    }

    /**
     * @param charset The name of a supported char set.
     *
     * @return buffered output of the process converted to a String.
     *
     * @throws IllegalStateException if the char set was not supported.
     */
    @Throws(IllegalStateException::class)
    suspend fun stringBuffered(charset: Charset): String {
        return try {
            String(getBuffered(), charset)
        } catch (e: UnsupportedEncodingException) {
            throw IllegalStateException(e.message)
        }
    }

    /**
     * @return buffered output lines of the finished process converted using platform's default encoding.
     */
    suspend fun lines(): List {
        return ProcessOutput.getLinesFrom(stringBuffered())
    }

    /**
     * @return buffered output lines of the finished process converted using UTF-8.
     */
    suspend fun linesAsUtf8(): List {
        return ProcessOutput.getLinesFrom(utf8Buffered())
    }

    /**
     * @param charset The name of a supported char set.
     *
     * @return buffered output lines of the finished process converted using a given char set.
     */
    suspend fun getLines(charset: Charset): List {
        return ProcessOutput.getLinesFrom(stringBuffered(charset))
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy