From e8535cd25a0388be8cf2af41304a539c807496bb Mon Sep 17 00:00:00 2001 From: JeremyStarTM Date: Sun, 29 Dec 2024 05:00:35 +0100 Subject: [PATCH] Migrate Process --- .../base/implementable/stream/Stream.kt | 15 +- .../engine/base/platform/ProcessPlatform.kt | 92 +++++++++ .../engine/base/platform/StreamPlatform.kt | 2 +- .../engine/base/utility/Process.kt | 162 +++++---------- .../platform/{Platform.kt => PlatformImpl.kt} | 0 .../base/platform/ProcessPlatformImpl.kt | 185 ++++++++++++++++++ .../base/platform/StreamPlatformImpl.kt | 4 +- 7 files changed, 345 insertions(+), 115 deletions(-) create mode 100644 base/src/commonMain/kotlin/de/staropensource/engine/base/platform/ProcessPlatform.kt rename base/src/jvmMain/kotlin/de/staropensource/engine/base/platform/{Platform.kt => PlatformImpl.kt} (100%) create mode 100644 base/src/jvmMain/kotlin/de/staropensource/engine/base/platform/ProcessPlatformImpl.kt diff --git a/base/src/commonMain/kotlin/de/staropensource/engine/base/implementable/stream/Stream.kt b/base/src/commonMain/kotlin/de/staropensource/engine/base/implementable/stream/Stream.kt index 6cb0909..38e012d 100644 --- a/base/src/commonMain/kotlin/de/staropensource/engine/base/implementable/stream/Stream.kt +++ b/base/src/commonMain/kotlin/de/staropensource/engine/base/implementable/stream/Stream.kt @@ -22,7 +22,7 @@ package de.staropensource.engine.base.implementable.stream import de.staropensource.engine.base.exception.io.IOAccessException import de.staropensource.engine.base.implementable.PlatformData import de.staropensource.engine.base.platform.StreamPlatform -import de.staropensource.engine.base.platform.platformStreamPlatform +import de.staropensource.engine.base.platform.platformStreamCreate import de.staropensource.engine.base.platform.platformStreamStandardError import de.staropensource.engine.base.platform.platformStreamStandardInput import de.staropensource.engine.base.platform.platformStreamStandardOutput @@ -74,10 +74,19 @@ abstract class Stream( } - // -----> Properties + // -----> Platform-specific override val platformData: MutableMap = mutableMapOf() - private val platform: StreamPlatform = platformStreamPlatform(this) + /** + * Platform-specific [StreamPlatform] + * instance for this [Stream]. + * + * @since v1-alpha10 + */ + private val platform: StreamPlatform = platformStreamCreate(this) + + + // -----> Properties /** * Indicates whether this stream is closed. * diff --git a/base/src/commonMain/kotlin/de/staropensource/engine/base/platform/ProcessPlatform.kt b/base/src/commonMain/kotlin/de/staropensource/engine/base/platform/ProcessPlatform.kt new file mode 100644 index 0000000..9f6f5a9 --- /dev/null +++ b/base/src/commonMain/kotlin/de/staropensource/engine/base/platform/ProcessPlatform.kt @@ -0,0 +1,92 @@ +/* + * 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.platform + +import de.staropensource.engine.base.implementable.stream.ReadStream +import de.staropensource.engine.base.implementable.stream.WriteStream +import de.staropensource.engine.base.utility.Process + +/** + * Platform-specific implementation of [Process]. + * + * @constructor Initializes this platform implementation + * @param process [Process] instance + * @since v1-alpha10 + */ +abstract class ProcessPlatform ( + val process: Process +) { + // -----> Lifecycle + /** + * Initializes the process. + * + * @since v1-alpha10 + */ + abstract fun start() + + /** + * Kills the process. + * + * @since v1-alpha10 + */ + abstract fun kill() + + + // -----> Getters + abstract fun isAlive(): Boolean + abstract fun getPid(): Long + abstract fun getExitCode(): UByte? + + + // -----> Stream getters + /** + * Returns the standard input stream. + * + * @return standard input stream + * @since v1-alpha10 + */ + abstract fun getStandardInput(): WriteStream + + /** + * Returns the standard output stream. + * + * @return standard output stream + * @since v1-alpha10 + */ + abstract fun getStandardOutput(): ReadStream + + /** + * Returns the standard error stream. + * + * @return standard error stream + * @since v1-alpha10 + */ + abstract fun getStandardError(): ReadStream +} + +/** + * Returns a [ProcessPlatform] implementation + * for the supplied [Process] instance. + * + * @param process [Process] instance + * @return matching [ProcessPlatform] instance + * @since v1-alpha10 + */ +internal expect fun platformProcessCreate(process: Process): ProcessPlatform diff --git a/base/src/commonMain/kotlin/de/staropensource/engine/base/platform/StreamPlatform.kt b/base/src/commonMain/kotlin/de/staropensource/engine/base/platform/StreamPlatform.kt index 52c012f..7500f5e 100644 --- a/base/src/commonMain/kotlin/de/staropensource/engine/base/platform/StreamPlatform.kt +++ b/base/src/commonMain/kotlin/de/staropensource/engine/base/platform/StreamPlatform.kt @@ -50,7 +50,7 @@ abstract class StreamPlatform( * @return matching [StreamPlatform] instance * @since v1-alpha10 */ -internal expect fun platformStreamPlatform(stream: Stream): StreamPlatform +internal expect fun platformStreamCreate(stream: Stream): StreamPlatform /** * Returns a [ReadStream] representing diff --git a/base/src/commonMain/kotlin/de/staropensource/engine/base/utility/Process.kt b/base/src/commonMain/kotlin/de/staropensource/engine/base/utility/Process.kt index b6520f2..a40be7a 100644 --- a/base/src/commonMain/kotlin/de/staropensource/engine/base/utility/Process.kt +++ b/base/src/commonMain/kotlin/de/staropensource/engine/base/utility/Process.kt @@ -22,14 +22,14 @@ package de.staropensource.engine.base.utility import de.staropensource.engine.base.Engine.Companion.logger import de.staropensource.engine.base.exception.VerificationFailedException import de.staropensource.engine.base.exception.io.IOAccessException +import de.staropensource.engine.base.implementable.PlatformData 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 de.staropensource.engine.base.platform.ProcessPlatform +import de.staropensource.engine.base.platform.platformProcessCreate import java.io.IOException -import java.io.InputStream -import java.io.OutputStream +import kotlin.Throws /** * Provides a simplified way of @@ -38,16 +38,20 @@ import java.io.OutputStream * @since v1-alpha10 */ @Suppress("Unused") -class Process { - // -----> Metadata +class Process : PlatformData { + // -----> Platform-specific + override val platformData: MutableMap = mutableMapOf() + /** - * Contains an instance of Java's - * [java.lang.Process] class. + * Platform-specific [ProcessPlatform] + * instance for this [Stream]. * * @since v1-alpha10 */ - private val process: java.lang.Process + private val platform: ProcessPlatform = platformProcessCreate(this) + + // -----> Metadata /** * Contains the executable this * [Process] is running. @@ -70,7 +74,7 @@ class Process { * * @since v1-alpha10 */ - val initialWorkingDirectory: FileAccess + val workingDirectory: FileAccess /** * Contains if this [Process] was launched @@ -81,6 +85,13 @@ class Process { */ val launchedWithFreshEnvironment: Boolean + /** + * Contains all initial environment variables. + * + * @since v1-alpha10 + */ + val environmentVariables: Map + /** * Contains all registered exit events. * @@ -147,47 +158,21 @@ class Process { 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.workingDirectory = workingDirectory ?: FileAccess(System.getProperty("user.dir")) this.launchedWithFreshEnvironment = freshEnvironment + this.environmentVariables = environmentVariables.toMap() 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() + platform.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() - } + standardInput = platform.getStandardInput() + standardOutput = platform.getStandardOutput() + standardError = platform.getStandardError() // Connect streams inputStreams.forEach { it.pipes.add(standardInput) } @@ -216,10 +201,32 @@ class Process { */ fun kill(): Process { logger.diag("Killing process ${getPid()}") - process.destroyForcibly() + platform.kill() return this } + /** + * Handles the death of this [Process]. + * + * Must only be called by the + * matching [ProcessPlatform] + * implementation. + * + * @since v1-alpha10 + */ + fun platformDeath() { + logger.diag("Process ${getPid()} died") + + // Invoke all exit events + for (exitEvent: UByte.() -> Unit in exitEvents) + exitEvent.invoke(getExitCode()!!) + + // Close streams + standardInput.close() + standardOutput.close() + standardError.close() + } + // -----> Getters & setters /** @@ -229,7 +236,7 @@ class Process { * @return still running? * @since v1-alpha10 */ - fun isAlive(): Boolean = process.isAlive + fun isAlive(): Boolean = platform.isAlive() /** * Returns the PID of this [Process] @@ -241,7 +248,7 @@ class Process { * @return process identifier * @since v1-alpha10 */ - fun getPid(): Long = process.pid() + fun getPid(): Long = platform.getPid() /** * Returns the code with which @@ -250,13 +257,7 @@ class Process { * @return exit code or `null` if alive * @since v1-alpha10 */ - fun getExitCode(): UByte? { - return try { - process.exitValue().toUByte() - } catch (exception: IllegalThreadStateException) { - null - } - } + fun getExitCode(): UByte? = platform.getExitCode() // -----> Miscellaneous @@ -351,61 +352,4 @@ class Process { throw VerificationFailedException("Expected that process ${getPid()} was not launched with a fresh environment") return this } - - - // -----> Inner classes - /** - * Used for the [standardInput] - * stream of a [Process]. - * - * @constructor Initializes this stream - * @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]. - * - * @constructor Initializes this stream - * @since v1-alpha10 - */ - private class StandardOutputErrorStream( - val process: Process, - stream: InputStream, - ) : JavaReadStream(stream) { - override fun close() { - if (!process.isAlive()) - super.close() - } - } } diff --git a/base/src/jvmMain/kotlin/de/staropensource/engine/base/platform/Platform.kt b/base/src/jvmMain/kotlin/de/staropensource/engine/base/platform/PlatformImpl.kt similarity index 100% rename from base/src/jvmMain/kotlin/de/staropensource/engine/base/platform/Platform.kt rename to base/src/jvmMain/kotlin/de/staropensource/engine/base/platform/PlatformImpl.kt diff --git a/base/src/jvmMain/kotlin/de/staropensource/engine/base/platform/ProcessPlatformImpl.kt b/base/src/jvmMain/kotlin/de/staropensource/engine/base/platform/ProcessPlatformImpl.kt new file mode 100644 index 0000000..9b50d9d --- /dev/null +++ b/base/src/jvmMain/kotlin/de/staropensource/engine/base/platform/ProcessPlatformImpl.kt @@ -0,0 +1,185 @@ +/* + * 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.platform + +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 de.staropensource.engine.base.utility.Process +import java.io.InputStream +import java.io.OutputStream + +private typealias JProcess = java.lang.Process + +@Suppress("EXPECT_ACTUAL_CLASSIFIERS_ARE_IN_BETA_WARNING") +/** + * Java implementation of [ProcessPlatform]. + * + * @constructor Initializes this platform implementation + * @param process [Process] instance + * @since v1-alpha10 + */ +private class ProcessPlatformImpl (process: Process) : ProcessPlatform(process) { + /** + * Contains an instance of Java's + * [java.lang.Process] class. + * + * @since v1-alpha10 + */ + private var jProcess: JProcess? = null + + /** + * Contains the standard input + * [Stream] of this [Process]. + * + * @since v1-alpha10 + */ + var standardInput: WriteStream? = null + + /** + * Contains the standard output + * [Stream] of this [Process]. + * + * @since v1-alpha10 + */ + var standardOutput: ReadStream? = null + + /** + * Contains the standard error + * [Stream] of this [Process]. + * + * @since v1-alpha10 + */ + var standardError: ReadStream? = null + + + // -----> Lifecycle + override fun start() { + val builder: ProcessBuilder = ProcessBuilder() + + // Reset environment + if (process.launchedWithFreshEnvironment) + builder.environment().clear() + + // Pass arguments + builder + .command(listOf(process.executable.toStringRaw()).plus(process.arguments)) + .directory(process.workingDirectory.file) + .environment().putAll(process.environmentVariables) + + // Start process + jProcess = builder.start() + + // Set streams + standardError = StandardOutputErrorStream(process, jProcess!!.errorStream) + standardOutput = StandardOutputErrorStream(process, jProcess!!.inputStream) + standardInput = StandardInputStream(process, jProcess!!.outputStream) + + // Attach process exit handling + jProcess!!.onExit().thenAccept { + process.platformDeath() + } + } + + override fun kill() { + jProcess!!.destroyForcibly() + } + + + // -----> Getters + override fun isAlive(): Boolean = jProcess!!.isAlive + + override fun getPid(): Long = jProcess!!.pid() + + override fun getExitCode(): UByte? = try { + jProcess!!.exitValue().toUByte() + } catch (_: IllegalThreadStateException) { + null + } + + + // -----> Stream getters + override fun getStandardInput(): WriteStream = standardInput!! + override fun getStandardOutput(): ReadStream = standardOutput!! + override fun getStandardError(): ReadStream = standardError!! +} + + +// -----> Actual methods +actual fun platformProcessCreate(process: Process): ProcessPlatform = ProcessPlatformImpl(process) + + +// -----> Inner classes +/** + * Used for the standard input + * stream of a [Process]. + * + * @constructor Initializes this stream + * @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 standard output + * and standard error streams + * of a [Process]. + * + * @constructor Initializes this stream + * @since v1-alpha10 + */ +private class StandardOutputErrorStream( + val process: Process, + stream: InputStream, +) : JavaReadStream(stream) { + override fun close() { + if (!process.isAlive()) + super.close() + } +} diff --git a/base/src/jvmMain/kotlin/de/staropensource/engine/base/platform/StreamPlatformImpl.kt b/base/src/jvmMain/kotlin/de/staropensource/engine/base/platform/StreamPlatformImpl.kt index ce8a8a8..2219cc9 100644 --- a/base/src/jvmMain/kotlin/de/staropensource/engine/base/platform/StreamPlatformImpl.kt +++ b/base/src/jvmMain/kotlin/de/staropensource/engine/base/platform/StreamPlatformImpl.kt @@ -38,7 +38,7 @@ import java.io.OutputStream * @param stream [Stream] instance * @since v1-alpha10 */ -class StreamPlatformImpl(stream: Stream) : StreamPlatform(stream) { +private class StreamPlatformImpl(stream: Stream) : StreamPlatform(stream) { override fun startWatchingThread() { Thread .ofVirtual() @@ -53,7 +53,7 @@ class StreamPlatformImpl(stream: Stream) : StreamPlatform(stream) { // -----> Actual methods -actual fun platformStreamPlatform(stream: Stream): StreamPlatform = stream.platformData.getOrPut("platform") { StreamPlatformImpl(stream) } as StreamPlatform +actual fun platformStreamCreate(stream: Stream): StreamPlatform = StreamPlatformImpl(stream) actual fun platformStreamStandardInput(): ReadStream = object : JavaReadStream(System.`in`) { override fun close() = Unit override fun closeStream() = Unit