Add Process API (implements #12)
All checks were successful
PRs & Pushes / test (push) Successful in 2m57s
PRs & Pushes / build-jars (push) Successful in 3m52s
PRs & Pushes / build-apidoc (push) Successful in 3m50s

This commit is contained in:
JeremyStar™ 2024-12-27 23:54:09 +01:00
parent 7b0b231a55
commit 1cf6c9ac41
Signed by: JeremyStarTM
GPG key ID: E366BAEF67E4704D

View file

@ -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 <https://www.gnu.org/licenses/>.
*/
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<String>
/**
* 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<UByte.() -> 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<String> = arrayOf(),
workingDirectory: FileAccess? = null,
freshEnvironment: Boolean = false,
environmentVariables: Map<String, String> = mapOf(),
exitEvents: Array<UByte.() -> Unit> = arrayOf(),
inputStreams: Array<Stream> = arrayOf(),
outputStreams: Array<Stream> = arrayOf(),
errorStreams: Array<Stream> = 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()
}
}
}