diff --git a/library/process/build.gradle.kts b/library/process/build.gradle.kts index f56fdb1..df7f52d 100644 --- a/library/process/build.gradle.kts +++ b/library/process/build.gradle.kts @@ -55,6 +55,20 @@ kmpConfiguration { findByName("jvmTest")?.apply { dependsOn(nonJsTest) } findByName("nativeTest")?.apply { dependsOn(nonJsTest) } } + + val linuxMain = findByName("linuxMain") + val macosMain = findByName("macosMain") + if (linuxMain != null || macosMain != null) { + val forkExecMain = maybeCreate("forkExecMain") + forkExecMain.dependsOn(getByName("nativeMain")) + linuxMain?.apply { dependsOn(forkExecMain) } + macosMain?.apply { dependsOn(forkExecMain) } + + val forkExecTest = maybeCreate("forkExecTest") + forkExecTest.dependsOn(getByName("nativeTest")) + findByName("linuxTest")?.apply { dependsOn(forkExecTest) } + findByName("macosTest")?.apply { dependsOn(forkExecTest) } + } } targets.filterIsInstance().spawnCInterop() } diff --git a/library/process/src/forkExecMain/kotlin/io/matthewnelson/kmp/process/internal/ForkExecPlatform.kt b/library/process/src/forkExecMain/kotlin/io/matthewnelson/kmp/process/internal/ForkExecPlatform.kt new file mode 100644 index 0000000..6b2c6ee --- /dev/null +++ b/library/process/src/forkExecMain/kotlin/io/matthewnelson/kmp/process/internal/ForkExecPlatform.kt @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2024 Matthew Nelson + * + * 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 + * + * https://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 io.matthewnelson.kmp.process.internal + +import io.matthewnelson.kmp.file.IOException +import io.matthewnelson.kmp.process.Signal +import io.matthewnelson.kmp.process.Stdio +import kotlinx.cinterop.ExperimentalForeignApi +import kotlinx.cinterop.MemScope + +@OptIn(ExperimentalForeignApi::class) +@Throws(IOException::class, UnsupportedOperationException::class) +internal actual fun MemScope.forkExec( + command: String, + args: List, + env: Map, + stdio: Stdio.Config, + destroy: Signal, +): NativeProcess { + throw IOException("Not yet implemented") +} diff --git a/library/process/src/iosMain/kotlin/io/matthewnelson/kmp/process/internal/IosPlatform.kt b/library/process/src/iosMain/kotlin/io/matthewnelson/kmp/process/internal/IosPlatform.kt new file mode 100644 index 0000000..7985a54 --- /dev/null +++ b/library/process/src/iosMain/kotlin/io/matthewnelson/kmp/process/internal/IosPlatform.kt @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2024 Matthew Nelson + * + * 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 + * + * https://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 io.matthewnelson.kmp.process.internal + +import io.matthewnelson.kmp.file.IOException +import io.matthewnelson.kmp.process.Signal +import io.matthewnelson.kmp.process.Stdio +import kotlinx.cinterop.ExperimentalForeignApi +import kotlinx.cinterop.MemScope + +@OptIn(ExperimentalForeignApi::class) +@Throws(IOException::class, UnsupportedOperationException::class) +internal actual fun MemScope.forkExec( + command: String, + args: List, + env: Map, + stdio: Stdio.Config, + destroy: Signal, +): NativeProcess { + throw UnsupportedOperationException("Fork & Exec is not supported on iOS") +} diff --git a/library/process/src/nativeMain/kotlin/io/matthewnelson/kmp/process/internal/NativePlatform.kt b/library/process/src/nativeMain/kotlin/io/matthewnelson/kmp/process/internal/NativePlatform.kt index 034f8b9..6a6b66c 100644 --- a/library/process/src/nativeMain/kotlin/io/matthewnelson/kmp/process/internal/NativePlatform.kt +++ b/library/process/src/nativeMain/kotlin/io/matthewnelson/kmp/process/internal/NativePlatform.kt @@ -21,7 +21,10 @@ import io.matthewnelson.kmp.file.DelicateFileApi import io.matthewnelson.kmp.file.IOException import io.matthewnelson.kmp.file.InterruptedException import io.matthewnelson.kmp.file.errnoToIOException +import io.matthewnelson.kmp.process.Signal +import io.matthewnelson.kmp.process.Stdio import kotlinx.cinterop.ExperimentalForeignApi +import kotlinx.cinterop.MemScope import platform.posix.errno import platform.posix.usleep import kotlin.contracts.ExperimentalContracts @@ -29,8 +32,25 @@ import kotlin.contracts.InvocationKind import kotlin.contracts.contract import kotlin.time.Duration -@Suppress("NOTHING_TO_INLINE") -internal expect inline fun PlatformBuilder.parentEnvironment(): MutableMap +@OptIn(ExperimentalForeignApi::class) +@Throws(IOException::class, UnsupportedOperationException::class) +internal expect fun MemScope.posixSpawn( + command: String, + args: List, + env: Map, + stdio: Stdio.Config, + destroy: Signal, +): NativeProcess + +@OptIn(ExperimentalForeignApi::class) +@Throws(IOException::class, UnsupportedOperationException::class) +internal expect fun MemScope.forkExec( + command: String, + args: List, + env: Map, + stdio: Stdio.Config, + destroy: Signal, +): NativeProcess @Suppress("NOTHING_TO_INLINE") @Throws(InterruptedException::class) diff --git a/library/process/src/nativeMain/kotlin/io/matthewnelson/kmp/process/internal/PlatformBuilder.kt b/library/process/src/nativeMain/kotlin/io/matthewnelson/kmp/process/internal/PlatformBuilder.kt new file mode 100644 index 0000000..e4a69d4 --- /dev/null +++ b/library/process/src/nativeMain/kotlin/io/matthewnelson/kmp/process/internal/PlatformBuilder.kt @@ -0,0 +1,77 @@ +/* + * Copyright (c) 2024 Matthew Nelson + * + * 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 + * + * https://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. + **/ +@file:Suppress("EXPECT_ACTUAL_CLASSIFIERS_ARE_IN_BETA_WARNING", "KotlinRedundantDiagnosticSuppress") + +package io.matthewnelson.kmp.process.internal + +import io.matthewnelson.kmp.file.IOException +import io.matthewnelson.kmp.file.wrapIOException +import io.matthewnelson.kmp.process.Output +import io.matthewnelson.kmp.process.Process +import io.matthewnelson.kmp.process.Signal +import io.matthewnelson.kmp.process.Stdio +import kotlinx.cinterop.ExperimentalForeignApi +import kotlinx.cinterop.memScoped + +@Suppress("NOTHING_TO_INLINE") +internal expect inline fun PlatformBuilder.parentEnvironment(): MutableMap + +// nativeMain +internal actual class PlatformBuilder actual constructor() { + + internal actual val env: MutableMap by lazy { + parentEnvironment() + } + + @Throws(IOException::class) + internal actual fun output( + command: String, + args: List, + env: Map, + stdio: Stdio.Config, + options: Output.Options, + destroy: Signal, + ): Output = blockingOutput(command, args, env, stdio, options, destroy) + + @Throws(IOException::class) + @OptIn(ExperimentalForeignApi::class) + internal actual fun spawn( + command: String, + args: List, + env: Map, + stdio: Stdio.Config, + destroy: Signal, + ): Process { + try { + val p: NativeProcess = memScoped { + posixSpawn(command, args, env, stdio, destroy) + } + return p + } catch (_: UnsupportedOperationException) { + /* ignore and try fork/exec */ + } + + try { + val p: NativeProcess = memScoped { + forkExec(command, args, env, stdio, destroy) + } + + return p + } catch (e: UnsupportedOperationException) { + throw e.wrapIOException { "Neither posix_spawn or fork/exec were supported" } + } + } +} \ No newline at end of file diff --git a/library/process/src/unixMain/kotlin/io/matthewnelson/kmp/process/internal/PlatformBuilder.kt b/library/process/src/unixMain/kotlin/io/matthewnelson/kmp/process/internal/PlatformBuilder.kt deleted file mode 100644 index f50440e..0000000 --- a/library/process/src/unixMain/kotlin/io/matthewnelson/kmp/process/internal/PlatformBuilder.kt +++ /dev/null @@ -1,113 +0,0 @@ -/* - * Copyright (c) 2024 Matthew Nelson - * - * 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 - * - * https://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. - **/ -@file:Suppress("EXPECT_ACTUAL_CLASSIFIERS_ARE_IN_BETA_WARNING", "KotlinRedundantDiagnosticSuppress") - -package io.matthewnelson.kmp.process.internal - -import io.matthewnelson.kmp.file.IOException -import io.matthewnelson.kmp.process.Output -import io.matthewnelson.kmp.process.Process -import io.matthewnelson.kmp.process.Signal -import io.matthewnelson.kmp.process.Stdio -import io.matthewnelson.kmp.process.internal.PosixSpawnAttrs.Companion.posixSpawnAttrInit -import io.matthewnelson.kmp.process.internal.PosixSpawnFileActions.Companion.posixSpawnFileActionsInit -import kotlinx.cinterop.* -import platform.posix.pid_tVar - -// unixMain -internal actual class PlatformBuilder internal actual constructor() { - - internal actual val env: MutableMap by lazy { - parentEnvironment() - } - - @Throws(IOException::class) - internal actual fun output( - command: String, - args: List, - env: Map, - stdio: Stdio.Config, - options: Output.Options, - destroy: Signal, - ): Output { - throw IOException("Not yet implemented") - } - - @Throws(IOException::class) - @OptIn(ExperimentalForeignApi::class) - internal actual fun spawn( - command: String, - args: List, - env: Map, - stdio: Stdio.Config, - destroy: Signal, - ): Process = memScoped { - - // TODO: pipes Issue #2 - - val fileActions = posixSpawnFileActionsInit() - val attrs = posixSpawnAttrInit() - - val pid = alloc() - - // null terminated c-string array - val argv = allocArray>(args.size + 2).apply { - // First argument for posix_spawn's argv should be the executable name. - // If command is the absolute path to the executable, take the final - // path argument after last separator. - this[0] = command.substringAfterLast('/').cstr.ptr - - var i = 1 - val iterator = args.iterator() - while (iterator.hasNext()) { - this[i++] = iterator.next().cstr.ptr - } - - this[i] = null - } - - // null terminated c-string array - val envp = allocArray>(env.size + 1).apply { - var i = 0 - val iterator = env.entries.iterator() - while (iterator.hasNext()) { - this[i++] = iterator.next().toString().cstr.ptr - } - - this[i] = null - } - - val result = if (command.startsWith('/')) { - // Absolute path, utilize posix_spawn - posixSpawn(command, pid.ptr, fileActions, attrs, argv, envp) - } else { - // relative path or program name, utilize posix_spawnp - posixSpawnP(command, pid.ptr, fileActions, attrs, argv, envp) - } - - // TODO: close things - result.check() - - NativeProcess( - pid.value, - command, - args, - env, - stdio, - destroy, - ) - } -} diff --git a/library/process/src/unixMain/kotlin/io/matthewnelson/kmp/process/internal/UnixPlatform.kt b/library/process/src/unixMain/kotlin/io/matthewnelson/kmp/process/internal/UnixPlatform.kt index 56f8916..0516df3 100644 --- a/library/process/src/unixMain/kotlin/io/matthewnelson/kmp/process/internal/UnixPlatform.kt +++ b/library/process/src/unixMain/kotlin/io/matthewnelson/kmp/process/internal/UnixPlatform.kt @@ -18,12 +18,88 @@ package io.matthewnelson.kmp.process.internal import io.matthewnelson.kmp.file.File +import io.matthewnelson.kmp.file.IOException import io.matthewnelson.kmp.file.toFile +import io.matthewnelson.kmp.process.Signal +import io.matthewnelson.kmp.process.Stdio +import io.matthewnelson.kmp.process.internal.PosixSpawnAttrs.Companion.posixSpawnAttrInit +import io.matthewnelson.kmp.process.internal.PosixSpawnFileActions.Companion.posixSpawnFileActionsInit import kotlinx.cinterop.* import platform.posix.* internal actual val STDIO_NULL: File = "/dev/null".toFile() +@OptIn(ExperimentalForeignApi::class) +@Throws(IOException::class, UnsupportedOperationException::class) +internal actual fun MemScope.posixSpawn( + command: String, + args: List, + env: Map, + stdio: Stdio.Config, + destroy: Signal, +): NativeProcess { + val fileActions = posixSpawnFileActionsInit() + val attrs = posixSpawnAttrInit() + + // TODO: try chg dir first + // - Linux glibc < 2.24 throw UnsupportedOperationException + // . + // - iOS throw IOException (it's not supported, but we want + // to stop early w/o trying fork & exec b/c that is not + // supported on iOS either. + + // TODO: streams Issue #2 + + val pid = alloc() + + // null terminated c-string array + val argv = allocArray>(args.size + 2).apply { + // First argument for posix_spawn's argv should be the executable name. + // If command is the absolute path to the executable, take the final + // path argument after last separator. + this[0] = command.substringAfterLast('/').cstr.ptr + + var i = 1 + val iterator = args.iterator() + while (iterator.hasNext()) { + this[i++] = iterator.next().cstr.ptr + } + + this[i] = null + } + + // null terminated c-string array + val envp = allocArray>(env.size + 1).apply { + var i = 0 + val iterator = env.entries.iterator() + while (iterator.hasNext()) { + this[i++] = iterator.next().toString().cstr.ptr + } + + this[i] = null + } + + val result = if (command.startsWith('/')) { + // Absolute path, utilize posix_spawn + posixSpawn(command, pid.ptr, fileActions, attrs, argv, envp) + } else { + // relative path or program name, utilize posix_spawnp + posixSpawnP(command, pid.ptr, fileActions, attrs, argv, envp) + } + + // TODO: close things + result.check() + + return NativeProcess( + pid.value, + command, + args, + env, + stdio, + destroy, + ) +} + @Suppress("NOTHING_TO_INLINE") @OptIn(ExperimentalForeignApi::class) internal expect inline fun MemScope.posixSpawn(