Add Process API (implements #12)
This commit is contained in:
parent
7b0b231a55
commit
1cf6c9ac41
1 changed files with 333 additions and 0 deletions
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue