lyngio: added processes and docs for it, JVM and linux
This commit is contained in:
parent
d91acd593a
commit
660a80a26b
@ -44,6 +44,7 @@ and it is multithreaded on platforms supporting it (automatically, no code chang
|
|||||||
- [Language home](https://lynglang.com)
|
- [Language home](https://lynglang.com)
|
||||||
- [introduction and tutorial](docs/tutorial.md) - start here please
|
- [introduction and tutorial](docs/tutorial.md) - start here please
|
||||||
- [Testing and Assertions](docs/Testing.md)
|
- [Testing and Assertions](docs/Testing.md)
|
||||||
|
- [Filesystem and Processes (lyngio)](docs/lyngio.md)
|
||||||
- [Return Statement](docs/return_statement.md)
|
- [Return Statement](docs/return_statement.md)
|
||||||
- [Efficient Iterables in Kotlin Interop](docs/EfficientIterables.md)
|
- [Efficient Iterables in Kotlin Interop](docs/EfficientIterables.md)
|
||||||
- [Samples directory](docs/samples)
|
- [Samples directory](docs/samples)
|
||||||
|
|||||||
@ -8,7 +8,7 @@ This module provides a uniform, suspend-first filesystem API to Lyng scripts, ba
|
|||||||
|
|
||||||
It exposes a Lyng class `Path` with methods for file and directory operations, including streaming readers for large files.
|
It exposes a Lyng class `Path` with methods for file and directory operations, including streaming readers for large files.
|
||||||
|
|
||||||
It is a separate library because access to teh filesystem is a security risk we compensate with a separate API that user must explicitly include to the dependency and allow. Together with `FsAceessPolicy` that is required to `createFs()` which actually adds the filesystem to the scope, the security risk is isolated.
|
It is a separate library because access to the filesystem is a security risk we compensate with a separate API that user must explicitly include to the dependency and allow. Together with `FsAccessPolicy` that is required to `createFs()` which actually adds the filesystem to the scope, the security risk is isolated.
|
||||||
|
|
||||||
Also, it helps keep Lyng core small and focused.
|
Also, it helps keep Lyng core small and focused.
|
||||||
|
|
||||||
@ -23,7 +23,7 @@ dependencies {
|
|||||||
implementation("net.sergeych:lyngio:0.0.1-SNAPSHOT")
|
implementation("net.sergeych:lyngio:0.0.1-SNAPSHOT")
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
Note on maven repository. Lyngio uses ths same maven as Lyng code (`lynglib`) so it is most likely already in your project. If ont, add it to the proper section of your `build.gradle.kts` or settings.gradle.kts:
|
Note on maven repository. Lyngio uses the same maven as Lyng code (`lynglib`) so it is most likely already in your project. If not, add it to the proper section of your `build.gradle.kts` or settings.gradle.kts:
|
||||||
|
|
||||||
```kotlin
|
```kotlin
|
||||||
repositories {
|
repositories {
|
||||||
@ -43,9 +43,13 @@ This brings in:
|
|||||||
|
|
||||||
The filesystem module is not installed automatically. You must explicitly register it in the scope’s `ImportManager` using the installer. You can customize access control via `FsAccessPolicy`.
|
The filesystem module is not installed automatically. You must explicitly register it in the scope’s `ImportManager` using the installer. You can customize access control via `FsAccessPolicy`.
|
||||||
|
|
||||||
Kotlin (host) bootstrap example (imports omitted for brevity):
|
Kotlin (host) bootstrap example:
|
||||||
|
|
||||||
```kotlin
|
```kotlin
|
||||||
|
import net.sergeych.lyng.Scope
|
||||||
|
import net.sergeych.lyng.io.fs.createFs
|
||||||
|
import net.sergeych.lyngio.fs.security.PermitAllAccessPolicy
|
||||||
|
|
||||||
val scope: Scope = Scope.new()
|
val scope: Scope = Scope.new()
|
||||||
val installed: Boolean = createFs(PermitAllAccessPolicy, scope)
|
val installed: Boolean = createFs(PermitAllAccessPolicy, scope)
|
||||||
// installed == true on first registration in this ImportManager, false on repeats
|
// installed == true on first registration in this ImportManager, false on repeats
|
||||||
|
|||||||
136
docs/lyng.io.process.md
Normal file
136
docs/lyng.io.process.md
Normal file
@ -0,0 +1,136 @@
|
|||||||
|
### lyng.io.process — Process execution and control for Lyng scripts
|
||||||
|
|
||||||
|
This module provides a way to run external processes and shell commands from Lyng scripts. It is designed to be multiplatform and uses coroutines for non-blocking execution.
|
||||||
|
|
||||||
|
> **Note:** `lyngio` is a separate library module. It must be explicitly added as a dependency to your host application and initialized in your Lyng scopes.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### Add the library to your project (Gradle)
|
||||||
|
|
||||||
|
If you use this repository as a multi-module project, add a dependency on `:lyngio`:
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
dependencies {
|
||||||
|
implementation("net.sergeych:lyngio:0.0.1-SNAPSHOT")
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
For external projects, ensure you have the appropriate Maven repository configured (see `lyng.io.fs` documentation).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### Install the module into a Lyng Scope
|
||||||
|
|
||||||
|
The process module is not installed automatically. You must explicitly register it in the scope’s `ImportManager` using `createProcessModule`. You can customize access control via `ProcessAccessPolicy`.
|
||||||
|
|
||||||
|
Kotlin (host) bootstrap example:
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
import net.sergeych.lyng.Scope
|
||||||
|
import net.sergeych.lyng.Script
|
||||||
|
import net.sergeych.lyng.io.process.createProcessModule
|
||||||
|
import net.sergeych.lyngio.process.security.PermitAllProcessAccessPolicy
|
||||||
|
|
||||||
|
// ... inside a suspend function or runBlocking
|
||||||
|
val scope: Scope = Script.newScope()
|
||||||
|
createProcessModule(PermitAllProcessAccessPolicy, scope)
|
||||||
|
|
||||||
|
// In scripts (or via scope.eval), import the module:
|
||||||
|
scope.eval("import lyng.io.process")
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### Using from Lyng scripts
|
||||||
|
|
||||||
|
```lyng
|
||||||
|
import lyng.io.process
|
||||||
|
|
||||||
|
// Execute a process with arguments
|
||||||
|
val p = Process.execute("ls", ["-l", "/tmp"])
|
||||||
|
for (line in p.stdout) {
|
||||||
|
println("OUT: " + line)
|
||||||
|
}
|
||||||
|
val exitCode = p.waitFor()
|
||||||
|
println("Process exited with: " + exitCode)
|
||||||
|
|
||||||
|
// Run a shell command
|
||||||
|
val sh = Process.shell("echo 'Hello from shell' | wc -w")
|
||||||
|
for (line in sh.stdout) {
|
||||||
|
println("Word count: " + line.trim())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Platform information
|
||||||
|
val details = Platform.details()
|
||||||
|
println("OS: " + details.name + " " + details.version + " (" + details.arch + ")")
|
||||||
|
if (details.kernelVersion != null) {
|
||||||
|
println("Kernel: " + details.kernelVersion)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Platform.isSupported()) {
|
||||||
|
println("Processes are supported!")
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### API Reference
|
||||||
|
|
||||||
|
##### `Process` (static methods)
|
||||||
|
- `execute(executable: String, args: List<String>): RunningProcess` — Start an external process.
|
||||||
|
- `shell(command: String): RunningProcess` — Run a command through the system shell (e.g., `/bin/sh` or `cmd.exe`).
|
||||||
|
|
||||||
|
##### `RunningProcess` (instance methods)
|
||||||
|
- `stdout: Flow` — Standard output stream as a Lyng Flow of lines.
|
||||||
|
- `stderr: Flow` — Standard error stream as a Lyng Flow of lines.
|
||||||
|
- `waitFor(): Int` — Wait for the process to exit and return the exit code.
|
||||||
|
- `signal(name: String)` — Send a signal to the process (e.g., `"SIGINT"`, `"SIGTERM"`, `"SIGKILL"`).
|
||||||
|
- `destroy()` — Forcefully terminate the process.
|
||||||
|
|
||||||
|
##### `Platform` (static methods)
|
||||||
|
- `details(): Map` — Get platform details. Returned map keys: `name`, `version`, `arch`, `kernelVersion`.
|
||||||
|
- `isSupported(): Bool` — True if process execution is supported on the current platform.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### Security Policy
|
||||||
|
|
||||||
|
Process execution is a sensitive operation. `lyngio` uses `ProcessAccessPolicy` to control access to `execute` and `shell` operations.
|
||||||
|
|
||||||
|
- `ProcessAccessPolicy` — Interface for custom policies.
|
||||||
|
- `PermitAllProcessAccessPolicy` — Allows all operations.
|
||||||
|
- `ProcessAccessOp` (sealed) — Operations to check:
|
||||||
|
- `Execute(executable, args)`
|
||||||
|
- `Shell(command)`
|
||||||
|
|
||||||
|
Example of a restricted policy in Kotlin:
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
import net.sergeych.lyngio.fs.security.AccessDecision
|
||||||
|
import net.sergeych.lyngio.fs.security.Decision
|
||||||
|
import net.sergeych.lyngio.process.security.ProcessAccessOp
|
||||||
|
import net.sergeych.lyngio.process.security.ProcessAccessPolicy
|
||||||
|
|
||||||
|
val restrictedPolicy = object : ProcessAccessPolicy {
|
||||||
|
override suspend fun check(op: ProcessAccessOp, ctx: AccessContext): AccessDecision {
|
||||||
|
return when (op) {
|
||||||
|
is ProcessAccessOp.Execute -> {
|
||||||
|
if (op.executable == "ls") AccessDecision(Decision.Allow)
|
||||||
|
else AccessDecision(Decision.Deny, "Only 'ls' is allowed")
|
||||||
|
}
|
||||||
|
is ProcessAccessOp.Shell -> AccessDecision(Decision.Deny, "Shell is forbidden")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
createProcessModule(restrictedPolicy, scope)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### Platform Support
|
||||||
|
|
||||||
|
- **JVM:** Full support using `ProcessBuilder`.
|
||||||
|
- **Native (Linux/macOS):** Support via POSIX.
|
||||||
|
- **Windows:** Support planned.
|
||||||
|
- **Android/JS/iOS/Wasm:** Currently not supported; `isSupported()` returns `false` and attempts to run processes will throw `UnsupportedOperationException`.
|
||||||
87
docs/lyngio.md
Normal file
87
docs/lyngio.md
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
### lyngio — Extended I/O and System Library for Lyng
|
||||||
|
|
||||||
|
`lyngio` is a separate library that extends the Lyng core (`lynglib`) with powerful, multiplatform, and secure I/O capabilities.
|
||||||
|
|
||||||
|
#### Why a separate module?
|
||||||
|
|
||||||
|
1. **Security:** I/O and process execution are sensitive operations. By keeping them in a separate module, we ensure that the Lyng core remains 100% safe by default. You only enable what you explicitly need.
|
||||||
|
2. **Footprint:** Not every script needs filesystem or process access. Keeping these as a separate module helps minimize the dependency footprint for small embedded projects.
|
||||||
|
3. **Control:** `lyngio` provides fine-grained security policies (`FsAccessPolicy`, `ProcessAccessPolicy`) that allow you to control exactly what a script can do.
|
||||||
|
|
||||||
|
#### Included Modules
|
||||||
|
|
||||||
|
- **[lyng.io.fs](lyng.io.fs.md):** Async filesystem access. Provides the `Path` class for file/directory operations, streaming, and globbing.
|
||||||
|
- **[lyng.io.process](lyng.io.process.md):** External process execution and shell commands. Provides `Process`, `RunningProcess`, and `Platform` information.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### Quick Start: Embedding lyngio
|
||||||
|
|
||||||
|
##### 1. Add Dependencies (Gradle)
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
repositories {
|
||||||
|
maven("https://gitea.sergeych.net/api/packages/SergeychWorks/maven")
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
// Both are required for full I/O support
|
||||||
|
implementation("net.sergeych:lynglib:0.0.1-SNAPSHOT")
|
||||||
|
implementation("net.sergeych:lyngio:0.0.1-SNAPSHOT")
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
##### 2. Initialize in Kotlin (JVM or Native)
|
||||||
|
|
||||||
|
To use `lyngio` modules in your scripts, you must install them into your Lyng scope and provide a security policy.
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
import net.sergeych.lyng.Script
|
||||||
|
import net.sergeych.lyng.io.fs.createFs
|
||||||
|
import net.sergeych.lyng.io.process.createProcessModule
|
||||||
|
import net.sergeych.lyngio.fs.security.PermitAllAccessPolicy
|
||||||
|
import net.sergeych.lyngio.process.security.PermitAllProcessAccessPolicy
|
||||||
|
|
||||||
|
suspend fun runMyScript() {
|
||||||
|
val scope = Script.newScope()
|
||||||
|
|
||||||
|
// Install modules with policies
|
||||||
|
createFs(PermitAllAccessPolicy, scope)
|
||||||
|
createProcessModule(PermitAllProcessAccessPolicy, scope)
|
||||||
|
|
||||||
|
// Now scripts can import them
|
||||||
|
scope.eval("""
|
||||||
|
import lyng.io.fs
|
||||||
|
import lyng.io.process
|
||||||
|
|
||||||
|
println("Working dir: " + Path(".").readUtf8())
|
||||||
|
println("OS: " + Platform.details().name)
|
||||||
|
""")
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### Security Tools
|
||||||
|
|
||||||
|
`lyngio` is built with a "Secure by Default" philosophy. Every I/O or process operation is checked against a policy.
|
||||||
|
|
||||||
|
- **Filesystem Security:** Implement `FsAccessPolicy` to restrict access to specific paths or operations (e.g., read-only access to a sandbox directory).
|
||||||
|
- **Process Security:** Implement `ProcessAccessPolicy` to restrict which executables can be run or to disable shell execution entirely.
|
||||||
|
|
||||||
|
For more details, see the specific module documentation:
|
||||||
|
- [Filesystem Security Details](lyng.io.fs.md#access-policy-security)
|
||||||
|
- [Process Security Details](lyng.io.process.md#security-policy)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### Platform Support Overview
|
||||||
|
|
||||||
|
| Platform | lyng.io.fs | lyng.io.process |
|
||||||
|
| :--- | :---: | :---: |
|
||||||
|
| **JVM** | ✅ | ✅ |
|
||||||
|
| **Native (Linux/macOS)** | ✅ | ✅ |
|
||||||
|
| **Native (Windows)** | ✅ | 🚧 (Planned) |
|
||||||
|
| **Android** | ✅ | ❌ |
|
||||||
|
| **NodeJS** | ✅ | ❌ |
|
||||||
|
| **Browser / Wasm** | ✅ (In-memory) | ❌ |
|
||||||
@ -0,0 +1,33 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com
|
||||||
|
*
|
||||||
|
* 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
|
||||||
|
*
|
||||||
|
* http://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 net.sergeych.lyngio.process
|
||||||
|
|
||||||
|
actual fun getPlatformDetails(): PlatformDetails {
|
||||||
|
return PlatformDetails(
|
||||||
|
name = "Android",
|
||||||
|
version = android.os.Build.VERSION.RELEASE,
|
||||||
|
arch = android.os.Build.SUPPORTED_ABIS.firstOrNull() ?: "unknown",
|
||||||
|
kernelVersion = System.getProperty("os.version")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
actual fun isProcessSupported(): Boolean = false
|
||||||
|
|
||||||
|
actual fun getSystemProcessRunner(): LyngProcessRunner {
|
||||||
|
throw UnsupportedOperationException("Processes are not supported on Android yet")
|
||||||
|
}
|
||||||
@ -0,0 +1,234 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com
|
||||||
|
*
|
||||||
|
* 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
|
||||||
|
*
|
||||||
|
* http://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 net.sergeych.lyng.io.process
|
||||||
|
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import net.sergeych.lyng.ModuleScope
|
||||||
|
import net.sergeych.lyng.Scope
|
||||||
|
import net.sergeych.lyng.miniast.*
|
||||||
|
import net.sergeych.lyng.obj.*
|
||||||
|
import net.sergeych.lyng.pacman.ImportManager
|
||||||
|
import net.sergeych.lyng.statement
|
||||||
|
import net.sergeych.lyngio.process.*
|
||||||
|
import net.sergeych.lyngio.process.security.ProcessAccessDeniedException
|
||||||
|
import net.sergeych.lyngio.process.security.ProcessAccessPolicy
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Install Lyng module `lyng.io.process` into the given scope's ImportManager.
|
||||||
|
*/
|
||||||
|
fun createProcessModule(policy: ProcessAccessPolicy, scope: Scope): Boolean =
|
||||||
|
createProcessModule(policy, scope.importManager)
|
||||||
|
|
||||||
|
/** Same as [createProcessModule] but with explicit [ImportManager]. */
|
||||||
|
fun createProcessModule(policy: ProcessAccessPolicy, manager: ImportManager): Boolean {
|
||||||
|
val name = "lyng.io.process"
|
||||||
|
if (manager.packageNames.contains(name)) return false
|
||||||
|
|
||||||
|
manager.addPackage(name) { module ->
|
||||||
|
buildProcessModule(module, policy)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun buildProcessModule(module: ModuleScope, policy: ProcessAccessPolicy) {
|
||||||
|
val runner = try {
|
||||||
|
SecuredLyngProcessRunner(getSystemProcessRunner(), policy)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
|
val runningProcessType = object : ObjClass("RunningProcess") {}
|
||||||
|
|
||||||
|
runningProcessType.apply {
|
||||||
|
addFnDoc(
|
||||||
|
name = "stdout",
|
||||||
|
doc = "Get standard output stream as a Flow of lines.",
|
||||||
|
returns = type("lyng.Flow"),
|
||||||
|
moduleName = module.packageName
|
||||||
|
) {
|
||||||
|
val self = thisAs<ObjRunningProcess>()
|
||||||
|
self.process.stdout.toLyngFlow(this)
|
||||||
|
}
|
||||||
|
addFnDoc(
|
||||||
|
name = "stderr",
|
||||||
|
doc = "Get standard error stream as a Flow of lines.",
|
||||||
|
returns = type("lyng.Flow"),
|
||||||
|
moduleName = module.packageName
|
||||||
|
) {
|
||||||
|
val self = thisAs<ObjRunningProcess>()
|
||||||
|
self.process.stderr.toLyngFlow(this)
|
||||||
|
}
|
||||||
|
addFnDoc(
|
||||||
|
name = "signal",
|
||||||
|
doc = "Send a signal to the process (e.g. 'SIGINT', 'SIGTERM', 'SIGKILL').",
|
||||||
|
params = listOf(ParamDoc("signal", type("lyng.String"))),
|
||||||
|
moduleName = module.packageName
|
||||||
|
) {
|
||||||
|
processGuard {
|
||||||
|
val sigStr = requireOnlyArg<ObjString>().value.uppercase()
|
||||||
|
val sig = try {
|
||||||
|
ProcessSignal.valueOf(sigStr)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
try {
|
||||||
|
ProcessSignal.valueOf("SIG$sigStr")
|
||||||
|
} catch (e2: Exception) {
|
||||||
|
raiseIllegalArgument("Unknown signal: $sigStr")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
thisAs<ObjRunningProcess>().process.sendSignal(sig)
|
||||||
|
ObjVoid
|
||||||
|
}
|
||||||
|
}
|
||||||
|
addFnDoc(
|
||||||
|
name = "waitFor",
|
||||||
|
doc = "Wait for the process to exit and return its exit code.",
|
||||||
|
returns = type("lyng.Int"),
|
||||||
|
moduleName = module.packageName
|
||||||
|
) {
|
||||||
|
processGuard {
|
||||||
|
thisAs<ObjRunningProcess>().process.waitFor().toObj()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
addFnDoc(
|
||||||
|
name = "destroy",
|
||||||
|
doc = "Forcefully terminate the process.",
|
||||||
|
moduleName = module.packageName
|
||||||
|
) {
|
||||||
|
thisAs<ObjRunningProcess>().process.destroy()
|
||||||
|
ObjVoid
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val processType = object : ObjClass("Process") {}
|
||||||
|
|
||||||
|
processType.apply {
|
||||||
|
addClassFnDoc(
|
||||||
|
name = "execute",
|
||||||
|
doc = "Execute a process with arguments.",
|
||||||
|
params = listOf(ParamDoc("executable", type("lyng.String")), ParamDoc("args", type("lyng.List"))),
|
||||||
|
returns = type("RunningProcess"),
|
||||||
|
moduleName = module.packageName
|
||||||
|
) {
|
||||||
|
if (runner == null) raiseError("Processes are not supported on this platform")
|
||||||
|
processGuard {
|
||||||
|
val executable = requiredArg<ObjString>(0).value
|
||||||
|
val args = requiredArg<ObjList>(1).list.map { it.toString() }
|
||||||
|
val lp = runner.execute(executable, args)
|
||||||
|
ObjRunningProcess(runningProcessType, lp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
addClassFnDoc(
|
||||||
|
name = "shell",
|
||||||
|
doc = "Execute a command via system shell.",
|
||||||
|
params = listOf(ParamDoc("command", type("lyng.String"))),
|
||||||
|
returns = type("RunningProcess"),
|
||||||
|
moduleName = module.packageName
|
||||||
|
) {
|
||||||
|
if (runner == null) raiseError("Processes are not supported on this platform")
|
||||||
|
processGuard {
|
||||||
|
val command = requireOnlyArg<ObjString>().value
|
||||||
|
val lp = runner.shell(command)
|
||||||
|
ObjRunningProcess(runningProcessType, lp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val platformType = object : ObjClass("Platform") {}
|
||||||
|
|
||||||
|
platformType.apply {
|
||||||
|
addClassFnDoc(
|
||||||
|
name = "details",
|
||||||
|
doc = "Get platform core details.",
|
||||||
|
returns = type("lyng.Map"),
|
||||||
|
moduleName = module.packageName
|
||||||
|
) {
|
||||||
|
val d = getPlatformDetails()
|
||||||
|
ObjMap(mutableMapOf(
|
||||||
|
ObjString("name") to ObjString(d.name),
|
||||||
|
ObjString("version") to ObjString(d.version),
|
||||||
|
ObjString("arch") to ObjString(d.arch),
|
||||||
|
ObjString("kernelVersion") to (d.kernelVersion?.toObj() ?: ObjNull)
|
||||||
|
))
|
||||||
|
}
|
||||||
|
addClassFnDoc(
|
||||||
|
name = "isSupported",
|
||||||
|
doc = "Check if processes are supported on this platform.",
|
||||||
|
returns = type("lyng.Bool"),
|
||||||
|
moduleName = module.packageName
|
||||||
|
) {
|
||||||
|
isProcessSupported().toObj()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.addConstDoc(
|
||||||
|
name = "Process",
|
||||||
|
value = processType,
|
||||||
|
doc = "Process execution and control.",
|
||||||
|
type = type("Process"),
|
||||||
|
moduleName = module.packageName
|
||||||
|
)
|
||||||
|
module.addConstDoc(
|
||||||
|
name = "Platform",
|
||||||
|
value = platformType,
|
||||||
|
doc = "Platform information.",
|
||||||
|
type = type("Platform"),
|
||||||
|
moduleName = module.packageName
|
||||||
|
)
|
||||||
|
module.addConstDoc(
|
||||||
|
name = "RunningProcess",
|
||||||
|
value = runningProcessType,
|
||||||
|
doc = "Handle to a running process.",
|
||||||
|
type = type("RunningProcess"),
|
||||||
|
moduleName = module.packageName
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
class ObjRunningProcess(
|
||||||
|
override val objClass: ObjClass,
|
||||||
|
val process: LyngProcess
|
||||||
|
) : Obj() {
|
||||||
|
override fun toString(): String = "RunningProcess($process)"
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend inline fun Scope.processGuard(crossinline block: suspend () -> Obj): Obj {
|
||||||
|
return try {
|
||||||
|
block()
|
||||||
|
} catch (e: ProcessAccessDeniedException) {
|
||||||
|
raiseError(ObjIllegalOperationException(this, e.reasonDetail ?: "process access denied"))
|
||||||
|
} catch (e: Exception) {
|
||||||
|
raiseError(ObjIllegalOperationException(this, e.message ?: "process error"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Flow<String>.toLyngFlow(flowScope: Scope): ObjFlow {
|
||||||
|
val producer = statement {
|
||||||
|
val builder = (this as? net.sergeych.lyng.ClosureScope)?.callScope?.thisObj as? ObjFlowBuilder
|
||||||
|
?: this.thisObj as? ObjFlowBuilder
|
||||||
|
|
||||||
|
this@toLyngFlow.collect {
|
||||||
|
try {
|
||||||
|
builder?.output?.send(ObjString(it))
|
||||||
|
} catch (e: Exception) {
|
||||||
|
// Channel closed or other error, stop collecting
|
||||||
|
return@collect
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ObjVoid
|
||||||
|
}
|
||||||
|
return ObjFlow(producer, flowScope)
|
||||||
|
}
|
||||||
@ -1,3 +1,20 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com
|
||||||
|
*
|
||||||
|
* 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
|
||||||
|
*
|
||||||
|
* http://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.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Filesystem module builtin docs registration, located in lyngio so core library
|
* Filesystem module builtin docs registration, located in lyngio so core library
|
||||||
* does not depend on external packages. The IDEA plugin (and any other tooling)
|
* does not depend on external packages. The IDEA plugin (and any other tooling)
|
||||||
|
|||||||
@ -0,0 +1,93 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com
|
||||||
|
*
|
||||||
|
* 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
|
||||||
|
*
|
||||||
|
* http://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 net.sergeych.lyngio.process
|
||||||
|
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import net.sergeych.lyngio.process.security.ProcessAccessOp
|
||||||
|
import net.sergeych.lyngio.process.security.ProcessAccessPolicy
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Common signals for process control.
|
||||||
|
*/
|
||||||
|
enum class ProcessSignal {
|
||||||
|
SIGINT, SIGTERM, SIGKILL
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Multiplatform process representation.
|
||||||
|
*/
|
||||||
|
interface LyngProcess {
|
||||||
|
/**
|
||||||
|
* Standard output stream as a flow of strings (lines).
|
||||||
|
*/
|
||||||
|
val stdout: Flow<String>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Standard error stream as a flow of strings (lines).
|
||||||
|
*/
|
||||||
|
val stderr: Flow<String>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send a signal to the process.
|
||||||
|
* Throws exception if signals are not supported on the platform or for this process.
|
||||||
|
*/
|
||||||
|
suspend fun sendSignal(signal: ProcessSignal)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wait for the process to exit and return the exit code.
|
||||||
|
*/
|
||||||
|
suspend fun waitFor(): Int
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Forcefully terminate the process.
|
||||||
|
*/
|
||||||
|
fun destroy()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface for running processes.
|
||||||
|
*/
|
||||||
|
interface LyngProcessRunner {
|
||||||
|
/**
|
||||||
|
* Execute a process with the given executable and arguments.
|
||||||
|
*/
|
||||||
|
suspend fun execute(executable: String, args: List<String>): LyngProcess
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute a command via the platform's default shell.
|
||||||
|
*/
|
||||||
|
suspend fun shell(command: String): LyngProcess
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Secured implementation of [LyngProcessRunner] that checks against a [ProcessAccessPolicy].
|
||||||
|
*/
|
||||||
|
class SecuredLyngProcessRunner(
|
||||||
|
private val runner: LyngProcessRunner,
|
||||||
|
private val policy: ProcessAccessPolicy
|
||||||
|
) : LyngProcessRunner {
|
||||||
|
override suspend fun execute(executable: String, args: List<String>): LyngProcess {
|
||||||
|
policy.require(ProcessAccessOp.Execute(executable, args))
|
||||||
|
return runner.execute(executable, args)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun shell(command: String): LyngProcess {
|
||||||
|
policy.require(ProcessAccessOp.Shell(command))
|
||||||
|
return runner.shell(command)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,44 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com
|
||||||
|
*
|
||||||
|
* 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
|
||||||
|
*
|
||||||
|
* http://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 net.sergeych.lyngio.process
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Platform core details.
|
||||||
|
*/
|
||||||
|
data class PlatformDetails(
|
||||||
|
val name: String,
|
||||||
|
val version: String,
|
||||||
|
val arch: String,
|
||||||
|
val kernelVersion: String? = null
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the current platform core details.
|
||||||
|
*/
|
||||||
|
expect fun getPlatformDetails(): PlatformDetails
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check whether the current platform supports processes and shell execution.
|
||||||
|
*/
|
||||||
|
expect fun isProcessSupported(): Boolean
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the system default [LyngProcessRunner].
|
||||||
|
* Throws [UnsupportedOperationException] if processes are not supported on this platform.
|
||||||
|
*/
|
||||||
|
expect fun getSystemProcessRunner(): LyngProcessRunner
|
||||||
@ -0,0 +1,59 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com
|
||||||
|
*
|
||||||
|
* 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
|
||||||
|
*
|
||||||
|
* http://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 net.sergeych.lyngio.process.security
|
||||||
|
|
||||||
|
import net.sergeych.lyngio.fs.security.AccessContext
|
||||||
|
import net.sergeych.lyngio.fs.security.AccessDecision
|
||||||
|
import net.sergeych.lyngio.fs.security.Decision
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Primitive process operations for access control decisions.
|
||||||
|
*/
|
||||||
|
sealed interface ProcessAccessOp {
|
||||||
|
data class Execute(val executable: String, val args: List<String>) : ProcessAccessOp
|
||||||
|
data class Shell(val command: String) : ProcessAccessOp
|
||||||
|
}
|
||||||
|
|
||||||
|
class ProcessAccessDeniedException(
|
||||||
|
val op: ProcessAccessOp,
|
||||||
|
val reasonDetail: String? = null,
|
||||||
|
) : IllegalStateException("Process access denied for $op" + (reasonDetail?.let { ": $it" } ?: ""))
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Policy interface that decides whether a specific process operation is allowed.
|
||||||
|
*/
|
||||||
|
interface ProcessAccessPolicy {
|
||||||
|
suspend fun check(op: ProcessAccessOp, ctx: AccessContext = AccessContext()): AccessDecision
|
||||||
|
|
||||||
|
// Convenience helpers
|
||||||
|
suspend fun require(op: ProcessAccessOp, ctx: AccessContext = AccessContext()) {
|
||||||
|
val res = check(op, ctx)
|
||||||
|
if (!res.isAllowed()) throw ProcessAccessDeniedException(op, res.reason)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun canExecute(executable: String, args: List<String>, ctx: AccessContext = AccessContext()) =
|
||||||
|
check(ProcessAccessOp.Execute(executable, args), ctx).isAllowed()
|
||||||
|
|
||||||
|
suspend fun canShell(command: String, ctx: AccessContext = AccessContext()) =
|
||||||
|
check(ProcessAccessOp.Shell(command), ctx).isAllowed()
|
||||||
|
}
|
||||||
|
|
||||||
|
object PermitAllProcessAccessPolicy : ProcessAccessPolicy {
|
||||||
|
override suspend fun check(op: ProcessAccessOp, ctx: AccessContext): AccessDecision =
|
||||||
|
AccessDecision(Decision.Allow)
|
||||||
|
}
|
||||||
@ -0,0 +1,26 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com
|
||||||
|
*
|
||||||
|
* 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
|
||||||
|
*
|
||||||
|
* http://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 net.sergeych.lyngio.process
|
||||||
|
|
||||||
|
internal actual fun getNativeKernelVersion(): String? = null
|
||||||
|
|
||||||
|
internal actual fun isNativeProcessSupported(): Boolean = false
|
||||||
|
|
||||||
|
internal actual fun getNativeProcessRunner(): LyngProcessRunner {
|
||||||
|
throw UnsupportedOperationException("Processes are not supported on iOS")
|
||||||
|
}
|
||||||
@ -0,0 +1,32 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com
|
||||||
|
*
|
||||||
|
* 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
|
||||||
|
*
|
||||||
|
* http://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 net.sergeych.lyngio.process
|
||||||
|
|
||||||
|
actual fun getPlatformDetails(): PlatformDetails {
|
||||||
|
return PlatformDetails(
|
||||||
|
name = "JavaScript",
|
||||||
|
version = "unknown",
|
||||||
|
arch = "unknown"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
actual fun isProcessSupported(): Boolean = false
|
||||||
|
|
||||||
|
actual fun getSystemProcessRunner(): LyngProcessRunner {
|
||||||
|
throw UnsupportedOperationException("Processes are not supported on JS")
|
||||||
|
}
|
||||||
@ -0,0 +1,107 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com
|
||||||
|
*
|
||||||
|
* 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
|
||||||
|
*
|
||||||
|
* http://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 net.sergeych.lyngio.process
|
||||||
|
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.flow
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
|
actual fun getPlatformDetails(): PlatformDetails {
|
||||||
|
val osName = System.getProperty("os.name")
|
||||||
|
return PlatformDetails(
|
||||||
|
name = osName,
|
||||||
|
version = System.getProperty("os.version"),
|
||||||
|
arch = System.getProperty("os.arch"),
|
||||||
|
kernelVersion = if (osName.lowercase().contains("linux")) {
|
||||||
|
System.getProperty("os.version")
|
||||||
|
} else null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
actual fun isProcessSupported(): Boolean = true
|
||||||
|
|
||||||
|
actual fun getSystemProcessRunner(): LyngProcessRunner = JvmProcessRunner
|
||||||
|
|
||||||
|
object JvmProcessRunner : LyngProcessRunner {
|
||||||
|
override suspend fun execute(executable: String, args: List<String>): LyngProcess {
|
||||||
|
val process = ProcessBuilder(listOf(executable) + args)
|
||||||
|
.start()
|
||||||
|
return JvmLyngProcess(process)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun shell(command: String): LyngProcess {
|
||||||
|
val os = System.getProperty("os.name").lowercase()
|
||||||
|
val shellCmd = if (os.contains("win")) {
|
||||||
|
listOf("cmd.exe", "/c", command)
|
||||||
|
} else {
|
||||||
|
listOf("sh", "-c", command)
|
||||||
|
}
|
||||||
|
val process = ProcessBuilder(shellCmd)
|
||||||
|
.start()
|
||||||
|
return JvmLyngProcess(process)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class JvmLyngProcess(private val process: Process) : LyngProcess {
|
||||||
|
override val stdout: Flow<String> = flow {
|
||||||
|
val reader = process.inputStream.bufferedReader()
|
||||||
|
while (true) {
|
||||||
|
val line = reader.readLine() ?: break
|
||||||
|
emit(line)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override val stderr: Flow<String> = flow {
|
||||||
|
val reader = process.errorStream.bufferedReader()
|
||||||
|
while (true) {
|
||||||
|
val line = reader.readLine() ?: break
|
||||||
|
emit(line)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun sendSignal(signal: ProcessSignal) {
|
||||||
|
when (signal) {
|
||||||
|
ProcessSignal.SIGINT -> {
|
||||||
|
// SIGINT is hard on JVM without native calls or external 'kill'
|
||||||
|
val os = System.getProperty("os.name").lowercase()
|
||||||
|
if (os.contains("win")) {
|
||||||
|
throw UnsupportedOperationException("SIGINT not supported on Windows JVM")
|
||||||
|
} else {
|
||||||
|
// Try to use kill -2 <pid>
|
||||||
|
try {
|
||||||
|
val pid = process.pid()
|
||||||
|
Runtime.getRuntime().exec(arrayOf("kill", "-2", pid.toString())).waitFor()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
throw UnsupportedOperationException("Failed to send SIGINT: ${e.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ProcessSignal.SIGTERM -> process.destroy()
|
||||||
|
ProcessSignal.SIGKILL -> process.destroyForcibly()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun waitFor(): Int = withContext(Dispatchers.IO) {
|
||||||
|
process.waitFor()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun destroy() {
|
||||||
|
process.destroy()
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,87 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com
|
||||||
|
*
|
||||||
|
* 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
|
||||||
|
*
|
||||||
|
* http://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 net.sergeych.lyng.io.process
|
||||||
|
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
|
import net.sergeych.lyng.Compiler
|
||||||
|
import net.sergeych.lyng.Script
|
||||||
|
import net.sergeych.lyngio.process.security.PermitAllProcessAccessPolicy
|
||||||
|
import kotlin.test.Test
|
||||||
|
import kotlin.test.assertTrue
|
||||||
|
|
||||||
|
class LyngProcessModuleTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testLyngProcess() = runBlocking {
|
||||||
|
val scope = Script.newScope()
|
||||||
|
createProcessModule(PermitAllProcessAccessPolicy, scope)
|
||||||
|
|
||||||
|
val code = """
|
||||||
|
import lyng.io.process
|
||||||
|
|
||||||
|
var p = Process.execute("echo", ["hello", "lyng"])
|
||||||
|
var output = []
|
||||||
|
for (line in p.stdout()) {
|
||||||
|
output.add(line)
|
||||||
|
}
|
||||||
|
p.waitFor()
|
||||||
|
output
|
||||||
|
""".trimIndent()
|
||||||
|
|
||||||
|
val script = Compiler.compile(code)
|
||||||
|
val result = script.execute(scope)
|
||||||
|
assertTrue(result.inspect(scope).contains("hello lyng"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testLyngShell() = runBlocking {
|
||||||
|
val scope = Script.newScope()
|
||||||
|
createProcessModule(PermitAllProcessAccessPolicy, scope)
|
||||||
|
|
||||||
|
val code = """
|
||||||
|
import lyng.io.process
|
||||||
|
|
||||||
|
var p = Process.shell("echo 'shell lyng'")
|
||||||
|
var output = ""
|
||||||
|
for (line in p.stdout()) {
|
||||||
|
output = output + line
|
||||||
|
}
|
||||||
|
p.waitFor()
|
||||||
|
output
|
||||||
|
""".trimIndent()
|
||||||
|
|
||||||
|
val script = Compiler.compile(code)
|
||||||
|
val result = script.execute(scope)
|
||||||
|
assertTrue(result.inspect(scope).contains("shell lyng"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testPlatformDetails() = runBlocking {
|
||||||
|
val scope = Script.newScope()
|
||||||
|
createProcessModule(PermitAllProcessAccessPolicy, scope)
|
||||||
|
|
||||||
|
val code = """
|
||||||
|
import lyng.io.process
|
||||||
|
Platform.details()
|
||||||
|
""".trimIndent()
|
||||||
|
|
||||||
|
val script = Compiler.compile(code)
|
||||||
|
val result = script.execute(scope)
|
||||||
|
assertTrue(result.inspect(scope).contains("name"), "Result should contain 'name', but was: ${result.inspect(scope)}")
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,57 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com
|
||||||
|
*
|
||||||
|
* 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
|
||||||
|
*
|
||||||
|
* http://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 net.sergeych.lyngio.process
|
||||||
|
|
||||||
|
import kotlinx.coroutines.flow.toList
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
|
import net.sergeych.lyngio.process.security.PermitAllProcessAccessPolicy
|
||||||
|
import kotlin.test.Test
|
||||||
|
import kotlin.test.assertEquals
|
||||||
|
import kotlin.test.assertTrue
|
||||||
|
|
||||||
|
class JvmProcessTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testExecute() = runBlocking {
|
||||||
|
val runner = getSystemProcessRunner()
|
||||||
|
val secured = SecuredLyngProcessRunner(runner, PermitAllProcessAccessPolicy)
|
||||||
|
|
||||||
|
val process = secured.execute("echo", listOf("hello", "world"))
|
||||||
|
val output = process.stdout.toList()
|
||||||
|
assertEquals(listOf("hello world"), output)
|
||||||
|
assertEquals(0, process.waitFor())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testShell() = runBlocking {
|
||||||
|
val runner = getSystemProcessRunner()
|
||||||
|
val secured = SecuredLyngProcessRunner(runner, PermitAllProcessAccessPolicy)
|
||||||
|
|
||||||
|
val process = secured.shell("echo 'hello shell'")
|
||||||
|
val output = process.stdout.toList()
|
||||||
|
assertEquals(listOf("hello shell"), output)
|
||||||
|
assertEquals(0, process.waitFor())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testPlatformDetails() {
|
||||||
|
val details = getPlatformDetails()
|
||||||
|
assertTrue(details.name.isNotEmpty())
|
||||||
|
assertTrue(isProcessSupported())
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,35 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com
|
||||||
|
*
|
||||||
|
* 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
|
||||||
|
*
|
||||||
|
* http://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 net.sergeych.lyngio.process
|
||||||
|
|
||||||
|
import kotlinx.cinterop.*
|
||||||
|
import platform.posix.*
|
||||||
|
|
||||||
|
@OptIn(ExperimentalForeignApi::class)
|
||||||
|
internal actual fun getNativeKernelVersion(): String? {
|
||||||
|
return memScoped {
|
||||||
|
val u = alloc<utsname>()
|
||||||
|
if (uname(u.ptr) == 0) {
|
||||||
|
u.release.toKString()
|
||||||
|
} else null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal actual fun isNativeProcessSupported(): Boolean = true
|
||||||
|
|
||||||
|
internal actual fun getNativeProcessRunner(): LyngProcessRunner = PosixProcessRunner
|
||||||
@ -0,0 +1,152 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com
|
||||||
|
*
|
||||||
|
* 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
|
||||||
|
*
|
||||||
|
* http://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 net.sergeych.lyngio.process
|
||||||
|
|
||||||
|
import kotlinx.cinterop.*
|
||||||
|
import kotlinx.coroutines.*
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.flow
|
||||||
|
import kotlinx.coroutines.flow.flowOn
|
||||||
|
import platform.posix.*
|
||||||
|
|
||||||
|
@OptIn(ExperimentalForeignApi::class)
|
||||||
|
internal class NativeLyngProcess(
|
||||||
|
private val pid: pid_t,
|
||||||
|
private val stdoutFd: Int,
|
||||||
|
private val stderrFd: Int
|
||||||
|
) : LyngProcess {
|
||||||
|
override val stdout: Flow<String> = createPipeFlow(stdoutFd)
|
||||||
|
override val stderr: Flow<String> = createPipeFlow(stderrFd)
|
||||||
|
|
||||||
|
override suspend fun sendSignal(signal: ProcessSignal) {
|
||||||
|
val sig = when (signal) {
|
||||||
|
ProcessSignal.SIGINT -> SIGINT
|
||||||
|
ProcessSignal.SIGTERM -> SIGTERM
|
||||||
|
ProcessSignal.SIGKILL -> SIGKILL
|
||||||
|
}
|
||||||
|
if (kill(pid, sig) != 0) {
|
||||||
|
throw RuntimeException("Failed to send signal $signal to process $pid: ${strerror(errno)?.toKString()}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun waitFor(): Int = withContext(Dispatchers.Default) {
|
||||||
|
memScoped {
|
||||||
|
val status = alloc<IntVar>()
|
||||||
|
if (waitpid(pid, status.ptr, 0) == -1) {
|
||||||
|
throw RuntimeException("Failed to wait for process $pid: ${strerror(errno)?.toKString()}")
|
||||||
|
}
|
||||||
|
val s = status.value
|
||||||
|
if ((s and 0x7f) == 0) (s shr 8) and 0xff else -1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun destroy() {
|
||||||
|
kill(pid, SIGKILL)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalForeignApi::class)
|
||||||
|
private fun createPipeFlow(fd: Int): Flow<String> = flow {
|
||||||
|
val buffer = ByteArray(4096)
|
||||||
|
val lineBuffer = StringBuilder()
|
||||||
|
|
||||||
|
try {
|
||||||
|
while (true) {
|
||||||
|
val bytesRead = buffer.usePinned { pinned ->
|
||||||
|
read(fd, pinned.addressOf(0), buffer.size.toULong())
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bytesRead <= 0L) break
|
||||||
|
|
||||||
|
val text = buffer.decodeToString(endIndex = bytesRead.toInt())
|
||||||
|
lineBuffer.append(text)
|
||||||
|
|
||||||
|
var newlineIdx = lineBuffer.indexOf('\n')
|
||||||
|
while (newlineIdx != -1) {
|
||||||
|
val line = lineBuffer.substring(0, newlineIdx)
|
||||||
|
emit(line)
|
||||||
|
lineBuffer.deleteRange(0, newlineIdx + 1)
|
||||||
|
newlineIdx = lineBuffer.indexOf('\n')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (lineBuffer.isNotEmpty()) {
|
||||||
|
emit(lineBuffer.toString())
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
close(fd)
|
||||||
|
}
|
||||||
|
}.flowOn(Dispatchers.Default)
|
||||||
|
|
||||||
|
@OptIn(ExperimentalForeignApi::class)
|
||||||
|
object PosixProcessRunner : LyngProcessRunner {
|
||||||
|
override suspend fun execute(executable: String, args: List<String>): LyngProcess = withContext(Dispatchers.Default) {
|
||||||
|
memScoped {
|
||||||
|
val pipeStdout = allocArray<IntVar>(2)
|
||||||
|
val pipeStderr = allocArray<IntVar>(2)
|
||||||
|
|
||||||
|
if (pipe(pipeStdout) != 0) throw RuntimeException("Failed to create stdout pipe")
|
||||||
|
if (pipe(pipeStderr) != 0) {
|
||||||
|
close(pipeStdout[0])
|
||||||
|
close(pipeStdout[1])
|
||||||
|
throw RuntimeException("Failed to create stderr pipe")
|
||||||
|
}
|
||||||
|
|
||||||
|
val pid = fork()
|
||||||
|
if (pid == -1) {
|
||||||
|
close(pipeStdout[0])
|
||||||
|
close(pipeStdout[1])
|
||||||
|
close(pipeStderr[0])
|
||||||
|
close(pipeStderr[1])
|
||||||
|
throw RuntimeException("Failed to fork: ${strerror(errno)?.toKString()}")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pid == 0) {
|
||||||
|
// Child process
|
||||||
|
dup2(pipeStdout[1], 1)
|
||||||
|
dup2(pipeStderr[1], 2)
|
||||||
|
|
||||||
|
close(pipeStdout[0])
|
||||||
|
close(pipeStdout[1])
|
||||||
|
close(pipeStderr[0])
|
||||||
|
close(pipeStderr[1])
|
||||||
|
|
||||||
|
val argv = allocArray<CPointerVar<ByteVar>>(args.size + 2)
|
||||||
|
argv[0] = executable.cstr.ptr
|
||||||
|
for (i in args.indices) {
|
||||||
|
argv[i + 1] = args[i].cstr.ptr
|
||||||
|
}
|
||||||
|
argv[args.size + 1] = null
|
||||||
|
|
||||||
|
execvp(executable, argv)
|
||||||
|
|
||||||
|
// If we are here, exec failed
|
||||||
|
_exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parent process
|
||||||
|
close(pipeStdout[1])
|
||||||
|
close(pipeStderr[1])
|
||||||
|
|
||||||
|
NativeLyngProcess(pid, pipeStdout[0], pipeStderr[0])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun shell(command: String): LyngProcess {
|
||||||
|
return execute("/bin/sh", listOf("-c", command))
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,95 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com
|
||||||
|
*
|
||||||
|
* 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
|
||||||
|
*
|
||||||
|
* http://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 net.sergeych.lyngio.process
|
||||||
|
|
||||||
|
import kotlinx.coroutines.flow.toList
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
|
import net.sergeych.lyng.Compiler
|
||||||
|
import net.sergeych.lyng.Script
|
||||||
|
import net.sergeych.lyng.io.process.createProcessModule
|
||||||
|
import net.sergeych.lyngio.process.security.PermitAllProcessAccessPolicy
|
||||||
|
import kotlin.test.Test
|
||||||
|
import kotlin.test.assertEquals
|
||||||
|
import kotlin.test.assertTrue
|
||||||
|
|
||||||
|
class LinuxProcessTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testExecuteEcho() = runBlocking {
|
||||||
|
val process = PosixProcessRunner.execute("echo", listOf("hello", "native"))
|
||||||
|
val stdout = process.stdout.toList()
|
||||||
|
val exitCode = process.waitFor()
|
||||||
|
|
||||||
|
assertEquals(0, exitCode)
|
||||||
|
assertEquals(listOf("hello native"), stdout)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testShellCommand() = runBlocking {
|
||||||
|
val process = PosixProcessRunner.shell("echo 'shell native' && printf 'line2'")
|
||||||
|
val stdout = process.stdout.toList()
|
||||||
|
val exitCode = process.waitFor()
|
||||||
|
|
||||||
|
assertEquals(0, exitCode)
|
||||||
|
assertEquals(listOf("shell native", "line2"), stdout)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testStderrCapture() = runBlocking {
|
||||||
|
val process = PosixProcessRunner.shell("echo 'to stdout'; echo 'to stderr' >&2")
|
||||||
|
val stdout = process.stdout.toList()
|
||||||
|
val stderr = process.stderr.toList()
|
||||||
|
process.waitFor()
|
||||||
|
|
||||||
|
assertEquals(listOf("to stdout"), stdout)
|
||||||
|
assertEquals(listOf("to stderr"), stderr)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testPlatformDetails() {
|
||||||
|
val details = getPlatformDetails()
|
||||||
|
assertEquals("LINUX", details.name)
|
||||||
|
assertTrue(details.kernelVersion != null)
|
||||||
|
assertTrue(details.kernelVersion!!.isNotEmpty())
|
||||||
|
println("Linux Native Details: $details")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testLyngModuleNative() = runBlocking {
|
||||||
|
val scope = Script.newScope()
|
||||||
|
createProcessModule(PermitAllProcessAccessPolicy, scope)
|
||||||
|
|
||||||
|
val code = """
|
||||||
|
import lyng.io.process
|
||||||
|
|
||||||
|
var p = Process.execute("echo", ["hello", "lyng", "native"])
|
||||||
|
var output = []
|
||||||
|
for (line in p.stdout()) {
|
||||||
|
output.add(line)
|
||||||
|
}
|
||||||
|
p.waitFor()
|
||||||
|
println(output)
|
||||||
|
assertEquals("hello lyng native", output.joinToString(" "))
|
||||||
|
output
|
||||||
|
""".trimIndent()
|
||||||
|
|
||||||
|
val script = Compiler.compile(code)
|
||||||
|
val result = script.execute(scope)
|
||||||
|
assertTrue(result.inspect(scope).contains("hello lyng native"))
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,35 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com
|
||||||
|
*
|
||||||
|
* 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
|
||||||
|
*
|
||||||
|
* http://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 net.sergeych.lyngio.process
|
||||||
|
|
||||||
|
import kotlinx.cinterop.*
|
||||||
|
import platform.posix.*
|
||||||
|
|
||||||
|
@OptIn(ExperimentalForeignApi::class)
|
||||||
|
internal actual fun getNativeKernelVersion(): String? {
|
||||||
|
return memScoped {
|
||||||
|
val u = alloc<utsname>()
|
||||||
|
if (uname(u.ptr) == 0) {
|
||||||
|
u.release.toKString()
|
||||||
|
} else null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal actual fun isNativeProcessSupported(): Boolean = true
|
||||||
|
|
||||||
|
internal actual fun getNativeProcessRunner(): LyngProcessRunner = PosixProcessRunner
|
||||||
@ -0,0 +1,152 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com
|
||||||
|
*
|
||||||
|
* 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
|
||||||
|
*
|
||||||
|
* http://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 net.sergeych.lyngio.process
|
||||||
|
|
||||||
|
import kotlinx.cinterop.*
|
||||||
|
import kotlinx.coroutines.*
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.flow
|
||||||
|
import kotlinx.coroutines.flow.flowOn
|
||||||
|
import platform.posix.*
|
||||||
|
|
||||||
|
@OptIn(ExperimentalForeignApi::class)
|
||||||
|
internal class NativeLyngProcess(
|
||||||
|
private val pid: pid_t,
|
||||||
|
private val stdoutFd: Int,
|
||||||
|
private val stderrFd: Int
|
||||||
|
) : LyngProcess {
|
||||||
|
override val stdout: Flow<String> = createPipeFlow(stdoutFd)
|
||||||
|
override val stderr: Flow<String> = createPipeFlow(stderrFd)
|
||||||
|
|
||||||
|
override suspend fun sendSignal(signal: ProcessSignal) {
|
||||||
|
val sig = when (signal) {
|
||||||
|
ProcessSignal.SIGINT -> SIGINT
|
||||||
|
ProcessSignal.SIGTERM -> SIGTERM
|
||||||
|
ProcessSignal.SIGKILL -> SIGKILL
|
||||||
|
}
|
||||||
|
if (kill(pid, sig) != 0) {
|
||||||
|
throw RuntimeException("Failed to send signal $signal to process $pid: ${strerror(errno)?.toKString()}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun waitFor(): Int = withContext(Dispatchers.Default) {
|
||||||
|
memScoped {
|
||||||
|
val status = alloc<IntVar>()
|
||||||
|
if (waitpid(pid, status.ptr, 0) == -1) {
|
||||||
|
throw RuntimeException("Failed to wait for process $pid: ${strerror(errno)?.toKString()}")
|
||||||
|
}
|
||||||
|
val s = status.value
|
||||||
|
if ((s and 0x7f) == 0) (s shr 8) and 0xff else -1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun destroy() {
|
||||||
|
kill(pid, SIGKILL)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalForeignApi::class)
|
||||||
|
private fun createPipeFlow(fd: Int): Flow<String> = flow {
|
||||||
|
val buffer = ByteArray(4096)
|
||||||
|
val lineBuffer = StringBuilder()
|
||||||
|
|
||||||
|
try {
|
||||||
|
while (true) {
|
||||||
|
val bytesRead = buffer.usePinned { pinned ->
|
||||||
|
read(fd, pinned.addressOf(0), buffer.size.toULong())
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bytesRead <= 0L) break
|
||||||
|
|
||||||
|
val text = buffer.decodeToString(endIndex = bytesRead.toInt())
|
||||||
|
lineBuffer.append(text)
|
||||||
|
|
||||||
|
var newlineIdx = lineBuffer.indexOf('\n')
|
||||||
|
while (newlineIdx != -1) {
|
||||||
|
val line = lineBuffer.substring(0, newlineIdx)
|
||||||
|
emit(line)
|
||||||
|
lineBuffer.deleteRange(0, newlineIdx + 1)
|
||||||
|
newlineIdx = lineBuffer.indexOf('\n')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (lineBuffer.isNotEmpty()) {
|
||||||
|
emit(lineBuffer.toString())
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
close(fd)
|
||||||
|
}
|
||||||
|
}.flowOn(Dispatchers.Default)
|
||||||
|
|
||||||
|
@OptIn(ExperimentalForeignApi::class)
|
||||||
|
object PosixProcessRunner : LyngProcessRunner {
|
||||||
|
override suspend fun execute(executable: String, args: List<String>): LyngProcess = withContext(Dispatchers.Default) {
|
||||||
|
memScoped {
|
||||||
|
val pipeStdout = allocArray<IntVar>(2)
|
||||||
|
val pipeStderr = allocArray<IntVar>(2)
|
||||||
|
|
||||||
|
if (pipe(pipeStdout) != 0) throw RuntimeException("Failed to create stdout pipe")
|
||||||
|
if (pipe(pipeStderr) != 0) {
|
||||||
|
close(pipeStdout[0])
|
||||||
|
close(pipeStdout[1])
|
||||||
|
throw RuntimeException("Failed to create stderr pipe")
|
||||||
|
}
|
||||||
|
|
||||||
|
val pid = fork()
|
||||||
|
if (pid == -1) {
|
||||||
|
close(pipeStdout[0])
|
||||||
|
close(pipeStdout[1])
|
||||||
|
close(pipeStderr[0])
|
||||||
|
close(pipeStderr[1])
|
||||||
|
throw RuntimeException("Failed to fork: ${strerror(errno)?.toKString()}")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pid == 0) {
|
||||||
|
// Child process
|
||||||
|
dup2(pipeStdout[1], 1)
|
||||||
|
dup2(pipeStderr[1], 2)
|
||||||
|
|
||||||
|
close(pipeStdout[0])
|
||||||
|
close(pipeStdout[1])
|
||||||
|
close(pipeStderr[0])
|
||||||
|
close(pipeStderr[1])
|
||||||
|
|
||||||
|
val argv = allocArray<CPointerVar<ByteVar>>(args.size + 2)
|
||||||
|
argv[0] = executable.cstr.ptr
|
||||||
|
for (i in args.indices) {
|
||||||
|
argv[i + 1] = args[i].cstr.ptr
|
||||||
|
}
|
||||||
|
argv[args.size + 1] = null
|
||||||
|
|
||||||
|
execvp(executable, argv)
|
||||||
|
|
||||||
|
// If we are here, exec failed
|
||||||
|
_exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parent process
|
||||||
|
close(pipeStdout[1])
|
||||||
|
close(pipeStderr[1])
|
||||||
|
|
||||||
|
NativeLyngProcess(pid, pipeStdout[0], pipeStderr[0])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun shell(command: String): LyngProcess {
|
||||||
|
return execute("/bin/sh", listOf("-c", command))
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,34 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com
|
||||||
|
*
|
||||||
|
* 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
|
||||||
|
*
|
||||||
|
* http://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 net.sergeych.lyngio.process
|
||||||
|
|
||||||
|
internal actual fun getNativeKernelVersion(): String? = null
|
||||||
|
|
||||||
|
internal actual fun isNativeProcessSupported(): Boolean = true
|
||||||
|
|
||||||
|
internal actual fun getNativeProcessRunner(): LyngProcessRunner = WindowsProcessRunner
|
||||||
|
|
||||||
|
object WindowsProcessRunner : LyngProcessRunner {
|
||||||
|
override suspend fun execute(executable: String, args: List<String>): LyngProcess {
|
||||||
|
throw UnsupportedOperationException("Windows native process execution not implemented yet")
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun shell(command: String): LyngProcess {
|
||||||
|
return execute("cmd.exe", listOf("/c", command))
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,40 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com
|
||||||
|
*
|
||||||
|
* 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
|
||||||
|
*
|
||||||
|
* http://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 net.sergeych.lyngio.process
|
||||||
|
|
||||||
|
import kotlin.experimental.ExperimentalNativeApi
|
||||||
|
|
||||||
|
@OptIn(ExperimentalNativeApi::class)
|
||||||
|
actual fun getPlatformDetails(): PlatformDetails {
|
||||||
|
return PlatformDetails(
|
||||||
|
name = Platform.osFamily.name,
|
||||||
|
version = "unknown",
|
||||||
|
arch = Platform.cpuArchitecture.name,
|
||||||
|
kernelVersion = getNativeKernelVersion()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
internal expect fun getNativeKernelVersion(): String?
|
||||||
|
|
||||||
|
actual fun isProcessSupported(): Boolean = isNativeProcessSupported()
|
||||||
|
|
||||||
|
internal expect fun isNativeProcessSupported(): Boolean
|
||||||
|
|
||||||
|
actual fun getSystemProcessRunner(): LyngProcessRunner = getNativeProcessRunner()
|
||||||
|
|
||||||
|
internal expect fun getNativeProcessRunner(): LyngProcessRunner
|
||||||
Loading…
x
Reference in New Issue
Block a user