diff --git a/base/src/main/kotlin/de/staropensource/engine/base/utility/Process.kt b/base/src/main/kotlin/de/staropensource/engine/base/utility/Process.kt new file mode 100644 index 0000000..c223ccf --- /dev/null +++ b/base/src/main/kotlin/de/staropensource/engine/base/utility/Process.kt @@ -0,0 +1,333 @@ +/* + * STAROPENSOURCE ENGINE SOURCE FILE + * Copyright (c) 2024 The StarOpenSource Engine Authors + * Licensed under the GNU General Public License v3. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package de.staropensource.engine.base.utility + +import de.staropensource.engine.base.Engine.Companion.logger +import de.staropensource.engine.base.exception.io.IOAccessException +import de.staropensource.engine.base.implementable.stream.ReadStream +import de.staropensource.engine.base.implementable.stream.Stream +import de.staropensource.engine.base.implementable.stream.WriteStream +import de.staropensource.engine.base.implementation.stream.JavaReadStream +import de.staropensource.engine.base.implementation.stream.JavaWriteStream +import java.io.IOException +import java.io.InputStream +import java.io.OutputStream + +/** + * Provides a simplified way of + * starting and managing processes. + * + * @since v1-alpha10 + */ +@Suppress("Unused") +class Process { + // -----> Metadata + /** + * Contains an instance of Java's + * [java.lang.Process] class. + * + * @since v1-alpha10 + */ + private val process: java.lang.Process + + /** + * Contains the executable this + * [Process] is running. + * + * @since v1-alpha10 + */ + val executable: FileAccess + + /** + * Contains an array of initial command + * line arguments of this [Process]. + * + * @since v1-alpha10 + */ + val arguments: Array + + /** + * Contains the initial working + * directory of this [Process]. + * + * @since v1-alpha10 + */ + val initialWorkingDirectory: FileAccess + + /** + * Contains if this [Process] was launched + * without inheriting any environment + * variables from this process. + * + * @since v1-alpha10 + */ + val launchedWithFreshEnvironment: Boolean + + /** + * Contains all registered exit events. + * + * These are triggered when this + * [Process] is terminated. + * + * @since v1-alpha10 + */ + val exitEvents: MutableList Unit> + + /** + * Contains the standard input + * [Stream] of this [Process]. + * + * @since v1-alpha10 + */ + val standardInput: WriteStream + + /** + * Contains the standard output + * [Stream] of this [Process]. + * + * @since v1-alpha10 + */ + val standardOutput: ReadStream + + /** + * Contains the standard error + * [Stream] of this [Process]. + * + * @since v1-alpha10 + */ + val standardError: ReadStream + + + // -----> Constructors + /** + * Creates a new process. + * + * @param executable executable to invoke + * @param arguments command line arguments passed to the executable + * @param workingDirectory directory to start the process in + * @param freshEnvironment `true` if the process starts with a fresh environment or `false` if it inherits the environment variables from this process + * @param environmentVariables a map of environment variables to set + * @param exitEvents exit events to register. More can be registered using [exitEvents] + * @param inputStreams array of [Stream]s to connect to the [Process.standardInput]. The [Stream.pipes] list of all [Stream]s passed will be updated to contain this [Process]' [Process.standardInput] + * @param outputStreams array of [Stream]s to connect to the [Process.standardOutput]. The [Stream.pipes] list of [Process.standardOutput] will be updated to contain all passed [Stream]s + * @param errorStreams array of [Stream]s to connect to the [Process.standardError]. The [Stream.pipes] list of [Process.standardError] will be updated to contain all passed [Stream]s + * @param autoWatchStreams whether to automatically invoke [Stream.watch] on the [Process.standardOutput] and [Process.standardError] [Stream]s + * @throws IOAccessException on IO Error + * @since v1-alpha10 + */ + @Throws(IOAccessException::class) + constructor( + executable: FileAccess, + arguments: Array = arrayOf(), + workingDirectory: FileAccess? = null, + freshEnvironment: Boolean = false, + environmentVariables: Map = mapOf(), + exitEvents: Array Unit> = arrayOf(), + inputStreams: Array = arrayOf(), + outputStreams: Array = arrayOf(), + errorStreams: Array = arrayOf(), + autoWatchStreams: Boolean = true, + ) { + try { + val builder: ProcessBuilder = ProcessBuilder() + + // Set properties + this.executable = executable + this.arguments = arguments + this.initialWorkingDirectory = workingDirectory ?: FileAccess(System.getProperty("user.dir")) + this.launchedWithFreshEnvironment = freshEnvironment + this.exitEvents = exitEvents.toMutableList() + + // Reset environment + if (freshEnvironment) + builder.environment().clear() + + // Pass arguments + builder + .command(listOf(executable.toStringRaw()).plus(arguments)) + .directory(workingDirectory?.file) + .environment().putAll(environmentVariables) + + // Start process + process = builder.start() + logger.diag("Started process ${getPid()}") + + // Set streams + this.standardError = StandardOutputErrorStream(this, process.errorStream) + this.standardOutput = StandardOutputErrorStream(this, process.inputStream) + this.standardInput = StandardInputStream(this, process.outputStream) + + // Attach process exit handling + process.onExit().thenAccept { + logger.diag("Process ${getPid()} died") + + // Invoke all exit events + for (exitEvent: UByte.() -> Unit in exitEvents) + exitEvent.invoke(process.exitValue().toUByte()) + + // Close streams + standardInput.close() + standardOutput.close() + standardError.close() + } + + // Connect streams + inputStreams.forEach { it.pipes.add(standardInput) } + standardOutput.pipes.addAll(outputStreams) + standardError.pipes.addAll(errorStreams) + + // Automatically watch + if (autoWatchStreams) { + standardOutput.watch() + standardError.watch() + } + } catch (exception: IOException) { + throw IOAccessException("Unable to spawn new process", exception) + } + } + + + // -----> Lifecycle + /** + * Kills this process. + * + * Does nothing if already terminated. + * + * @return this instance + * @since v1-alpha10 + */ + fun kill(): Process { + logger.diag("Killing process ${getPid()}") + process.destroyForcibly() + return this + } + + + // -----> Getters & setters + /** + * Returns if this process + * is still running. + * + * @return still running? + * @since v1-alpha10 + */ + fun isAlive(): Boolean = process.isAlive + + /** + * Returns the PID of this [Process] + * set by the operating system. + * + * If this [Process] is already dead, + * then it's past PID will be returned. + * + * @return process identifier + * @since v1-alpha10 + */ + fun getPid(): Long = process.pid() + + /** + * Returns the code with which + * this [Process] exited. + * + * @return exit code or `null` if alive + * @since v1-alpha10 + */ + fun getExitCode(): UByte? { + return try { + process.exitValue().toUByte() + } catch (exception: IllegalThreadStateException) { + null + } + } + + + // -----> Miscellaneous + /** + * Waits until this [Process] has exited. + * + * @param maxTime amount of time to wait in milliseconds + * @return this instance + * @since v1-alpha10 + */ + fun waitForExit(maxTime: ULong = 0UL): Process { + val maxTimeReal: Long = System.currentTimeMillis().plus(maxTime.toLong()) + + while (maxTime == 0UL || System.currentTimeMillis() < maxTimeReal) + if (!isAlive()) + break + + return this + } + + + // -----> Inner classes + /** + * Used for the [standardInput] + * stream of a [Process]. + * + * @since v1-alpha10 + */ + private class StandardInputStream( + val process: Process, + stream: OutputStream, + ) : JavaWriteStream( + stream, + autoFlushAfter = 100UL + ) { + // -----> Closure + override fun close() { + if (!process.isAlive()) + super.close() + } + + // -----> Writing + // Assist in flushing + override fun writeByte(byte: Byte): Stream { + super.writeByte(byte) + if (byte.toInt() == 0x0A) + flush() + return this + } + + override fun writeBytes(bytes: ByteArray): Stream { + super.writeBytes(bytes) + if (bytes.contains((0x0A).toByte())) + flush() + return this + } + } + + /** + * Used for the [standardOutput] + * and [standardError] streams + * of a [Process]. + * + * @since v1-alpha10 + */ + private class StandardOutputErrorStream( + val process: Process, + stream: InputStream, + ) : JavaReadStream(stream) { + override fun close() { + if (!process.isAlive()) + super.close() + } + } +}