Refactor syscall modules to use async/await patterns
- Updated `DisplaySyscall` to use `tokio::io::AsyncWriteExt` for asynchronous text output. - Refactored `FsSyscall` to read files asynchronously with offset and length handling. - Modified `MemorySyscall` to use `tokio::task::spawn_blocking` for database operations, allowing async access to SQLite. - Enhanced `NetworkSyscall` to utilize `reqwest` for HTTP requests, replacing the previous `curl` command execution. - Improved `ProcessSyscall` to manage subprocesses with async tasks for stdin, stdout, and stderr handling. - Updated `TimerSyscall` to simplify timer management. - Adjusted plugin implementations for better async support and error handling. - Added `tokio-stream` and `tracing-subscriber` dependencies to `Cargo.toml` for enhanced async stream handling and logging.
This commit is contained in:
726
Cargo.lock
generated
726
Cargo.lock
generated
@@ -8,8 +8,10 @@ version = "0.1.0"
|
||||
dependencies = [
|
||||
"agentsd-proto",
|
||||
"anyhow",
|
||||
"libc",
|
||||
"notify",
|
||||
"prost",
|
||||
"reqwest",
|
||||
"rusqlite",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -21,6 +23,76 @@ dependencies = [
|
||||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "agentsd-plugin-ai-test"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"agentsd-plugin-sdk",
|
||||
"agentsd-proto",
|
||||
"anyhow",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tokio",
|
||||
"tokio-stream",
|
||||
"tonic",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "agentsd-plugin-claudecode"
|
||||
version = "2.1.169"
|
||||
dependencies = [
|
||||
"agentsd-plugin-sdk",
|
||||
"agentsd-proto",
|
||||
"anyhow",
|
||||
"futures-core",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tokio",
|
||||
"tokio-stream",
|
||||
"tonic",
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "agentsd-plugin-echo"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"agentsd-plugin-sdk",
|
||||
"agentsd-proto",
|
||||
"anyhow",
|
||||
"tokio",
|
||||
"tonic",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "agentsd-plugin-hermes"
|
||||
version = "0.16.0"
|
||||
dependencies = [
|
||||
"agentsd-plugin-sdk",
|
||||
"agentsd-proto",
|
||||
"anyhow",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tokio",
|
||||
"tokio-stream",
|
||||
"tonic",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "agentsd-plugin-sdk"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"agentsd-proto",
|
||||
"anyhow",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tokio",
|
||||
"tokio-stream",
|
||||
"tonic",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "agentsd-proto"
|
||||
version = "0.1.0"
|
||||
@@ -195,6 +267,23 @@ version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
||||
|
||||
[[package]]
|
||||
name = "cfg_aliases"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
|
||||
|
||||
[[package]]
|
||||
name = "displaydoc"
|
||||
version = "0.2.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "either"
|
||||
version = "1.16.0"
|
||||
@@ -269,6 +358,15 @@ version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
|
||||
|
||||
[[package]]
|
||||
name = "form_urlencoded"
|
||||
version = "1.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf"
|
||||
dependencies = [
|
||||
"percent-encoding",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fsevent-sys"
|
||||
version = "4.1.0"
|
||||
@@ -324,8 +422,24 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"js-sys",
|
||||
"libc",
|
||||
"wasi",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.3.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"js-sys",
|
||||
"libc",
|
||||
"r-efi 5.3.0",
|
||||
"wasip2",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -336,7 +450,7 @@ checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"r-efi",
|
||||
"r-efi 6.0.0",
|
||||
"wasip2",
|
||||
"wasip3",
|
||||
]
|
||||
@@ -472,6 +586,22 @@ dependencies = [
|
||||
"want",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hyper-rustls"
|
||||
version = "0.27.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f"
|
||||
dependencies = [
|
||||
"http",
|
||||
"hyper",
|
||||
"hyper-util",
|
||||
"rustls",
|
||||
"tokio",
|
||||
"tokio-rustls",
|
||||
"tower-service",
|
||||
"webpki-roots",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hyper-timeout"
|
||||
version = "0.5.2"
|
||||
@@ -491,13 +621,16 @@ version = "0.1.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0"
|
||||
dependencies = [
|
||||
"base64",
|
||||
"bytes",
|
||||
"futures-channel",
|
||||
"futures-util",
|
||||
"http",
|
||||
"http-body",
|
||||
"hyper",
|
||||
"ipnet",
|
||||
"libc",
|
||||
"percent-encoding",
|
||||
"pin-project-lite",
|
||||
"socket2 0.6.4",
|
||||
"tokio",
|
||||
@@ -505,12 +638,115 @@ dependencies = [
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "icu_collections"
|
||||
version = "2.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c"
|
||||
dependencies = [
|
||||
"displaydoc",
|
||||
"potential_utf",
|
||||
"utf8_iter",
|
||||
"yoke",
|
||||
"zerofrom",
|
||||
"zerovec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "icu_locale_core"
|
||||
version = "2.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29"
|
||||
dependencies = [
|
||||
"displaydoc",
|
||||
"litemap",
|
||||
"tinystr",
|
||||
"writeable",
|
||||
"zerovec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "icu_normalizer"
|
||||
version = "2.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4"
|
||||
dependencies = [
|
||||
"icu_collections",
|
||||
"icu_normalizer_data",
|
||||
"icu_properties",
|
||||
"icu_provider",
|
||||
"smallvec",
|
||||
"zerovec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "icu_normalizer_data"
|
||||
version = "2.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38"
|
||||
|
||||
[[package]]
|
||||
name = "icu_properties"
|
||||
version = "2.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de"
|
||||
dependencies = [
|
||||
"icu_collections",
|
||||
"icu_locale_core",
|
||||
"icu_properties_data",
|
||||
"icu_provider",
|
||||
"zerotrie",
|
||||
"zerovec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "icu_properties_data"
|
||||
version = "2.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14"
|
||||
|
||||
[[package]]
|
||||
name = "icu_provider"
|
||||
version = "2.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421"
|
||||
dependencies = [
|
||||
"displaydoc",
|
||||
"icu_locale_core",
|
||||
"writeable",
|
||||
"yoke",
|
||||
"zerofrom",
|
||||
"zerotrie",
|
||||
"zerovec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "id-arena"
|
||||
version = "2.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954"
|
||||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de"
|
||||
dependencies = [
|
||||
"idna_adapter",
|
||||
"smallvec",
|
||||
"utf8_iter",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "idna_adapter"
|
||||
version = "1.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714"
|
||||
dependencies = [
|
||||
"icu_normalizer",
|
||||
"icu_properties",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "indexmap"
|
||||
version = "1.9.3"
|
||||
@@ -562,6 +798,12 @@ dependencies = [
|
||||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ipnet"
|
||||
version = "2.12.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2"
|
||||
|
||||
[[package]]
|
||||
name = "itertools"
|
||||
version = "0.14.0"
|
||||
@@ -643,6 +885,12 @@ version = "0.12.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
|
||||
|
||||
[[package]]
|
||||
name = "litemap"
|
||||
version = "0.8.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0"
|
||||
|
||||
[[package]]
|
||||
name = "lock_api"
|
||||
version = "0.4.14"
|
||||
@@ -658,6 +906,12 @@ version = "0.4.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "953f07c43838f8e6f9758cab68bf5bed85465e7587ebe0b823f1bcd81978ad3a"
|
||||
|
||||
[[package]]
|
||||
name = "lru-slab"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154"
|
||||
|
||||
[[package]]
|
||||
name = "matchers"
|
||||
version = "0.2.0"
|
||||
@@ -817,6 +1071,15 @@ version = "0.3.33"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e"
|
||||
|
||||
[[package]]
|
||||
name = "potential_utf"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564"
|
||||
dependencies = [
|
||||
"zerovec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ppv-lite86"
|
||||
version = "0.2.21"
|
||||
@@ -897,6 +1160,61 @@ dependencies = [
|
||||
"prost",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quinn"
|
||||
version = "0.11.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"cfg_aliases",
|
||||
"pin-project-lite",
|
||||
"quinn-proto",
|
||||
"quinn-udp",
|
||||
"rustc-hash",
|
||||
"rustls",
|
||||
"socket2 0.6.4",
|
||||
"thiserror",
|
||||
"tokio",
|
||||
"tracing",
|
||||
"web-time",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quinn-proto"
|
||||
version = "0.11.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"getrandom 0.3.4",
|
||||
"lru-slab",
|
||||
"rand 0.9.4",
|
||||
"ring",
|
||||
"rustc-hash",
|
||||
"rustls",
|
||||
"rustls-pki-types",
|
||||
"slab",
|
||||
"thiserror",
|
||||
"tinyvec",
|
||||
"tracing",
|
||||
"web-time",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quinn-udp"
|
||||
version = "0.5.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd"
|
||||
dependencies = [
|
||||
"cfg_aliases",
|
||||
"libc",
|
||||
"once_cell",
|
||||
"socket2 0.6.4",
|
||||
"tracing",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.45"
|
||||
@@ -906,6 +1224,12 @@ dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "r-efi"
|
||||
version = "5.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
|
||||
|
||||
[[package]]
|
||||
name = "r-efi"
|
||||
version = "6.0.0"
|
||||
@@ -919,8 +1243,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"rand_chacha",
|
||||
"rand_core",
|
||||
"rand_chacha 0.3.1",
|
||||
"rand_core 0.6.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand"
|
||||
version = "0.9.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea"
|
||||
dependencies = [
|
||||
"rand_chacha 0.9.0",
|
||||
"rand_core 0.9.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -930,7 +1264,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
|
||||
dependencies = [
|
||||
"ppv-lite86",
|
||||
"rand_core",
|
||||
"rand_core 0.6.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_chacha"
|
||||
version = "0.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
|
||||
dependencies = [
|
||||
"ppv-lite86",
|
||||
"rand_core 0.9.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -942,6 +1286,15 @@ dependencies = [
|
||||
"getrandom 0.2.17",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_core"
|
||||
version = "0.9.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c"
|
||||
dependencies = [
|
||||
"getrandom 0.3.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "redox_syscall"
|
||||
version = "0.5.18"
|
||||
@@ -980,6 +1333,58 @@ version = "0.8.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a"
|
||||
|
||||
[[package]]
|
||||
name = "reqwest"
|
||||
version = "0.12.28"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147"
|
||||
dependencies = [
|
||||
"base64",
|
||||
"bytes",
|
||||
"futures-core",
|
||||
"http",
|
||||
"http-body",
|
||||
"http-body-util",
|
||||
"hyper",
|
||||
"hyper-rustls",
|
||||
"hyper-util",
|
||||
"js-sys",
|
||||
"log",
|
||||
"percent-encoding",
|
||||
"pin-project-lite",
|
||||
"quinn",
|
||||
"rustls",
|
||||
"rustls-pki-types",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_urlencoded",
|
||||
"sync_wrapper",
|
||||
"tokio",
|
||||
"tokio-rustls",
|
||||
"tower 0.5.3",
|
||||
"tower-http",
|
||||
"tower-service",
|
||||
"url",
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-futures",
|
||||
"web-sys",
|
||||
"webpki-roots",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ring"
|
||||
version = "0.17.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"cfg-if",
|
||||
"getrandom 0.2.17",
|
||||
"libc",
|
||||
"untrusted",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rusqlite"
|
||||
version = "0.32.1"
|
||||
@@ -994,6 +1399,12 @@ dependencies = [
|
||||
"smallvec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustc-hash"
|
||||
version = "2.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe"
|
||||
|
||||
[[package]]
|
||||
name = "rustix"
|
||||
version = "1.1.4"
|
||||
@@ -1007,12 +1418,53 @@ dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls"
|
||||
version = "0.23.40"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b"
|
||||
dependencies = [
|
||||
"once_cell",
|
||||
"ring",
|
||||
"rustls-pki-types",
|
||||
"rustls-webpki",
|
||||
"subtle",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls-pki-types"
|
||||
version = "1.14.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9"
|
||||
dependencies = [
|
||||
"web-time",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls-webpki"
|
||||
version = "0.103.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e"
|
||||
dependencies = [
|
||||
"ring",
|
||||
"rustls-pki-types",
|
||||
"untrusted",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustversion"
|
||||
version = "1.0.22"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
|
||||
|
||||
[[package]]
|
||||
name = "ryu"
|
||||
version = "1.0.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"
|
||||
|
||||
[[package]]
|
||||
name = "same-file"
|
||||
version = "1.0.6"
|
||||
@@ -1077,6 +1529,18 @@ dependencies = [
|
||||
"zmij",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_urlencoded"
|
||||
version = "0.7.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd"
|
||||
dependencies = [
|
||||
"form_urlencoded",
|
||||
"itoa",
|
||||
"ryu",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sharded-slab"
|
||||
version = "0.1.7"
|
||||
@@ -1134,6 +1598,18 @@ dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "stable_deref_trait"
|
||||
version = "1.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596"
|
||||
|
||||
[[package]]
|
||||
name = "subtle"
|
||||
version = "2.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.117"
|
||||
@@ -1150,6 +1626,20 @@ name = "sync_wrapper"
|
||||
version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "synstructure"
|
||||
version = "0.13.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tempfile"
|
||||
@@ -1164,6 +1654,26 @@ dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "2.0.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4"
|
||||
dependencies = [
|
||||
"thiserror-impl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror-impl"
|
||||
version = "2.0.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thread_local"
|
||||
version = "1.1.9"
|
||||
@@ -1173,6 +1683,31 @@ dependencies = [
|
||||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tinystr"
|
||||
version = "0.8.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d"
|
||||
dependencies = [
|
||||
"displaydoc",
|
||||
"zerovec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tinyvec"
|
||||
version = "1.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3"
|
||||
dependencies = [
|
||||
"tinyvec_macros",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tinyvec_macros"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
|
||||
|
||||
[[package]]
|
||||
name = "tokio"
|
||||
version = "1.52.3"
|
||||
@@ -1201,6 +1736,16 @@ dependencies = [
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-rustls"
|
||||
version = "0.26.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61"
|
||||
dependencies = [
|
||||
"rustls",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-stream"
|
||||
version = "0.1.18"
|
||||
@@ -1280,7 +1825,7 @@ dependencies = [
|
||||
"indexmap 1.9.3",
|
||||
"pin-project",
|
||||
"pin-project-lite",
|
||||
"rand",
|
||||
"rand 0.8.6",
|
||||
"slab",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
@@ -1299,10 +1844,29 @@ dependencies = [
|
||||
"futures-util",
|
||||
"pin-project-lite",
|
||||
"sync_wrapper",
|
||||
"tokio",
|
||||
"tower-layer",
|
||||
"tower-service",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tower-http"
|
||||
version = "0.6.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840"
|
||||
dependencies = [
|
||||
"bitflags 2.13.0",
|
||||
"bytes",
|
||||
"futures-util",
|
||||
"http",
|
||||
"http-body",
|
||||
"pin-project-lite",
|
||||
"tower 0.5.3",
|
||||
"tower-layer",
|
||||
"tower-service",
|
||||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tower-layer"
|
||||
version = "0.3.3"
|
||||
@@ -1394,6 +1958,30 @@ version = "0.2.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
|
||||
|
||||
[[package]]
|
||||
name = "untrusted"
|
||||
version = "0.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
|
||||
|
||||
[[package]]
|
||||
name = "url"
|
||||
version = "2.5.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed"
|
||||
dependencies = [
|
||||
"form_urlencoded",
|
||||
"idna",
|
||||
"percent-encoding",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "utf8_iter"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
|
||||
|
||||
[[package]]
|
||||
name = "uuid"
|
||||
version = "1.23.2"
|
||||
@@ -1479,6 +2067,16 @@ dependencies = [
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-futures"
|
||||
version = "0.4.73"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "54568702fabf5d4849ce2b90fadfa64168a097eaf4b351ce9df8b687a0086aaf"
|
||||
dependencies = [
|
||||
"js-sys",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro"
|
||||
version = "0.2.123"
|
||||
@@ -1545,6 +2143,35 @@ dependencies = [
|
||||
"semver",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "web-sys"
|
||||
version = "0.3.100"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6e0871acf327f283dc6da28a1696cdc64fb355ba9f935d052021fa77f35cce69"
|
||||
dependencies = [
|
||||
"js-sys",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "web-time"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb"
|
||||
dependencies = [
|
||||
"js-sys",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "webpki-roots"
|
||||
version = "1.0.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "52f5ee44c96cf55f1b349600768e3ece3a8f26010c05265ab73f945bb1a2eb9d"
|
||||
dependencies = [
|
||||
"rustls-pki-types",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winapi-util"
|
||||
version = "0.1.11"
|
||||
@@ -1736,6 +2363,35 @@ dependencies = [
|
||||
"wasmparser",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "writeable"
|
||||
version = "0.6.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4"
|
||||
|
||||
[[package]]
|
||||
name = "yoke"
|
||||
version = "0.8.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5"
|
||||
dependencies = [
|
||||
"stable_deref_trait",
|
||||
"yoke-derive",
|
||||
"zerofrom",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "yoke-derive"
|
||||
version = "0.8.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"synstructure",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy"
|
||||
version = "0.8.50"
|
||||
@@ -1756,6 +2412,66 @@ dependencies = [
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerofrom"
|
||||
version = "0.1.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272"
|
||||
dependencies = [
|
||||
"zerofrom-derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerofrom-derive"
|
||||
version = "0.1.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"synstructure",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zeroize"
|
||||
version = "1.8.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0"
|
||||
|
||||
[[package]]
|
||||
name = "zerotrie"
|
||||
version = "0.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf"
|
||||
dependencies = [
|
||||
"displaydoc",
|
||||
"yoke",
|
||||
"zerofrom",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerovec"
|
||||
version = "0.11.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239"
|
||||
dependencies = [
|
||||
"yoke",
|
||||
"zerofrom",
|
||||
"zerovec-derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerovec-derive"
|
||||
version = "0.11.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zmij"
|
||||
version = "1.0.21"
|
||||
|
||||
@@ -25,3 +25,5 @@ anyhow = "1"
|
||||
uuid = { version = "1", features = ["v4"] }
|
||||
notify = "7"
|
||||
futures-core = "0.3"
|
||||
reqwest = { version = "0.12", default-features = false, features = ["rustls-tls"] }
|
||||
libc = "0.2"
|
||||
|
||||
@@ -21,3 +21,5 @@ tracing-subscriber = { workspace = true }
|
||||
anyhow = { workspace = true }
|
||||
uuid = { workspace = true }
|
||||
notify = { workspace = true }
|
||||
reqwest = { workspace = true }
|
||||
libc = { workspace = true }
|
||||
|
||||
@@ -14,6 +14,12 @@ pub struct CallbackBus {
|
||||
senders: Mutex<HashMap<String, mpsc::Sender<TimerEvent>>>,
|
||||
}
|
||||
|
||||
impl Default for CallbackBus {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl CallbackBus {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
@@ -35,8 +41,18 @@ impl CallbackBus {
|
||||
pub fn fire(&self, plugin_id: &str, event: TimerEvent) {
|
||||
let senders = self.senders.lock().unwrap();
|
||||
if let Some(tx) = senders.get(plugin_id) {
|
||||
// Non-blocking send; drop if channel full
|
||||
let _ = tx.try_send(event);
|
||||
match tx.try_send(event) {
|
||||
Ok(()) => {}
|
||||
Err(mpsc::error::TrySendError::Full(_)) => {
|
||||
tracing::warn!("timer event dropped: channel full for plugin={}", plugin_id);
|
||||
}
|
||||
Err(mpsc::error::TrySendError::Closed(_)) => {
|
||||
tracing::debug!(
|
||||
"timer event dropped: channel closed for plugin={}",
|
||||
plugin_id
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
mod syscall;
|
||||
pub mod callback;
|
||||
pub mod paths;
|
||||
pub mod permission;
|
||||
mod plugin;
|
||||
mod server;
|
||||
pub mod paths;
|
||||
pub mod callback;
|
||||
pub mod permission;
|
||||
mod syscall;
|
||||
|
||||
use anyhow::Result;
|
||||
use tracing_subscriber::EnvFilter;
|
||||
|
||||
@@ -4,7 +4,10 @@ use std::path::PathBuf;
|
||||
/// Creates it if it does not exist.
|
||||
pub fn data_dir() -> PathBuf {
|
||||
let exe = std::env::current_exe().unwrap_or_else(|_| PathBuf::from("."));
|
||||
let dir = exe.parent().unwrap_or_else(|| std::path::Path::new(".")).join("data");
|
||||
let dir = exe
|
||||
.parent()
|
||||
.unwrap_or_else(|| std::path::Path::new("."))
|
||||
.join("data");
|
||||
if !dir.exists() {
|
||||
std::fs::create_dir_all(&dir).ok();
|
||||
}
|
||||
|
||||
@@ -12,13 +12,14 @@ pub fn plugin_id_from_metadata(req_metadata: &tonic::metadata::MetadataMap) -> O
|
||||
}
|
||||
|
||||
/// Load a plugin and require it to be currently running.
|
||||
#[allow(clippy::result_large_err)]
|
||||
pub fn require_running_plugin(
|
||||
plugins: &Arc<PluginManager>,
|
||||
plugin_id: &str,
|
||||
) -> Result<PluginRecord, Status> {
|
||||
let record = plugins.get(plugin_id).ok_or_else(|| {
|
||||
Status::permission_denied(format!("unknown plugin: {}", plugin_id))
|
||||
})?;
|
||||
let record = plugins
|
||||
.get(plugin_id)
|
||||
.ok_or_else(|| Status::permission_denied(format!("unknown plugin: {}", plugin_id)))?;
|
||||
|
||||
if record.status != PluginStatus::Running {
|
||||
return Err(Status::permission_denied(format!(
|
||||
@@ -38,6 +39,7 @@ pub fn require_running_plugin(
|
||||
/// - `fs` grants all fs syscalls
|
||||
/// - `fs.*` grants all fs syscalls
|
||||
/// - `fs.list` grants only that method
|
||||
#[allow(clippy::result_large_err)]
|
||||
pub fn check_permission(
|
||||
plugins: &Arc<PluginManager>,
|
||||
req_metadata: &tonic::metadata::MetadataMap,
|
||||
@@ -73,6 +75,7 @@ pub fn check_permission(
|
||||
}
|
||||
|
||||
/// Helper to extract plugin_id and check permission in one call within a service method.
|
||||
#[allow(clippy::result_large_err)]
|
||||
pub fn guard<T>(
|
||||
plugins: &Arc<PluginManager>,
|
||||
req: &Request<T>,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,4 @@
|
||||
use std::io::Write as IoWrite;
|
||||
use tokio::io::AsyncWriteExt;
|
||||
|
||||
/// Display syscall: output to user.
|
||||
pub struct DisplaySyscall;
|
||||
@@ -8,32 +8,34 @@ impl DisplaySyscall {
|
||||
Self
|
||||
}
|
||||
|
||||
pub fn text(&self, content: &str) -> Result<(), String> {
|
||||
let stdout = std::io::stdout();
|
||||
let mut handle = stdout.lock();
|
||||
handle
|
||||
pub async fn text(&self, content: &str) -> Result<(), String> {
|
||||
let mut stdout = tokio::io::stdout();
|
||||
stdout
|
||||
.write_all(content.as_bytes())
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
handle.write_all(b"\n").map_err(|e| e.to_string())?;
|
||||
handle.flush().map_err(|e| e.to_string())
|
||||
stdout.write_all(b"\n").await.map_err(|e| e.to_string())?;
|
||||
stdout.flush().await.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
pub fn rich(&self, markup: &str, _format: &str) -> Result<(), String> {
|
||||
// For now, render as plain text. Rich rendering is a system plugin concern.
|
||||
self.text(markup)
|
||||
pub async fn rich(&self, markup: &str, _format: &str) -> Result<(), String> {
|
||||
self.text(markup).await
|
||||
}
|
||||
|
||||
pub fn image(&self, _data: &[u8], _format: &str) -> Result<(), String> {
|
||||
self.text("[image]")
|
||||
pub async fn image(&self, _data: &[u8], _format: &str) -> Result<(), String> {
|
||||
self.text("[image]").await
|
||||
}
|
||||
|
||||
pub fn clear(&self) -> Result<(), String> {
|
||||
// ANSI clear
|
||||
print!("\x1b[2J\x1b[H");
|
||||
std::io::stdout().flush().map_err(|e| e.to_string())
|
||||
pub async fn clear(&self) -> Result<(), String> {
|
||||
let mut stdout = tokio::io::stdout();
|
||||
stdout
|
||||
.write_all(b"\x1b[2J\x1b[H")
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
stdout.flush().await.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
pub fn notify(&self, message: &str) -> Result<(), String> {
|
||||
self.text(&format!("[notify] {}", message))
|
||||
pub async fn notify(&self, message: &str) -> Result<(), String> {
|
||||
self.text(&format!("[notify] {}", message)).await
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
use std::path::Path;
|
||||
use tokio::fs as async_fs;
|
||||
use tokio::io::{AsyncReadExt, AsyncSeekExt};
|
||||
|
||||
const MAX_READ_SIZE: usize = 64 * 1024 * 1024; // 64 MB
|
||||
|
||||
/// FS syscall: file system operations.
|
||||
pub struct FsSyscall;
|
||||
@@ -10,17 +13,25 @@ impl FsSyscall {
|
||||
}
|
||||
|
||||
pub async fn read(&self, path: &str, offset: i64, length: i64) -> Result<Vec<u8>, String> {
|
||||
let data = async_fs::read(path).await.map_err(|e| e.to_string())?;
|
||||
let start = if offset > 0 { offset as usize } else { 0 };
|
||||
let end = if length > 0 {
|
||||
std::cmp::min(start + length as usize, data.len())
|
||||
} else {
|
||||
data.len()
|
||||
};
|
||||
if start >= data.len() {
|
||||
return Ok(Vec::new());
|
||||
let mut file = tokio::fs::File::open(path)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
if offset > 0 {
|
||||
file.seek(std::io::SeekFrom::Start(offset as u64))
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
}
|
||||
Ok(data[start..end].to_vec())
|
||||
let len = if length > 0 {
|
||||
std::cmp::min(length as usize, MAX_READ_SIZE)
|
||||
} else {
|
||||
let meta = file.metadata().await.map_err(|e| e.to_string())?;
|
||||
let remaining = (meta.len() as usize).saturating_sub(offset.max(0) as usize);
|
||||
std::cmp::min(remaining, MAX_READ_SIZE)
|
||||
};
|
||||
let mut buf = vec![0u8; len];
|
||||
let n = file.read(&mut buf).await.map_err(|e| e.to_string())?;
|
||||
buf.truncate(n);
|
||||
Ok(buf)
|
||||
}
|
||||
|
||||
pub async fn write(&self, path: &str, data: &[u8], append: bool) -> Result<(), String> {
|
||||
@@ -50,9 +61,7 @@ impl FsSyscall {
|
||||
.await
|
||||
.map_err(|e| e.to_string())
|
||||
} else {
|
||||
async_fs::remove_file(path)
|
||||
.await
|
||||
.map_err(|e| e.to_string())
|
||||
async_fs::remove_file(path).await.map_err(|e| e.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
use rusqlite::{params, Connection};
|
||||
use std::sync::Mutex;
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
/// Memory syscall: structured KV storage with FTS5 full-text search.
|
||||
/// Data is persisted to `<exe_dir>/data/memory.db`.
|
||||
pub struct MemorySyscall {
|
||||
conn: Mutex<Connection>,
|
||||
conn: Arc<Mutex<Connection>>,
|
||||
}
|
||||
|
||||
impl MemorySyscall {
|
||||
@@ -12,7 +12,9 @@ impl MemorySyscall {
|
||||
let db_path = crate::paths::data_dir().join("memory.db");
|
||||
let conn = Connection::open(&db_path)?;
|
||||
conn.execute_batch(
|
||||
"CREATE TABLE IF NOT EXISTS kv (
|
||||
"PRAGMA journal_mode=WAL;
|
||||
PRAGMA busy_timeout=5000;
|
||||
CREATE TABLE IF NOT EXISTS kv (
|
||||
key TEXT PRIMARY KEY,
|
||||
value BLOB NOT NULL,
|
||||
text_value TEXT,
|
||||
@@ -34,12 +36,15 @@ impl MemorySyscall {
|
||||
)?;
|
||||
tracing::info!("memory: opened {}", db_path.display());
|
||||
Ok(Self {
|
||||
conn: Mutex::new(conn),
|
||||
conn: Arc::new(Mutex::new(conn)),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn read(&self, key: &str) -> Result<Option<Vec<u8>>, String> {
|
||||
let conn = self.conn.lock().unwrap();
|
||||
pub async fn read(&self, key: &str) -> Result<Option<Vec<u8>>, String> {
|
||||
let conn = self.conn.clone();
|
||||
let key = key.to_string();
|
||||
tokio::task::spawn_blocking(move || {
|
||||
let conn = conn.lock().unwrap();
|
||||
let mut stmt = conn
|
||||
.prepare("SELECT value FROM kv WHERE key = ?1")
|
||||
.map_err(|e| e.to_string())?;
|
||||
@@ -47,28 +52,48 @@ impl MemorySyscall {
|
||||
.query_row(params![key], |row| row.get::<_, Vec<u8>>(0))
|
||||
.ok();
|
||||
Ok(result)
|
||||
})
|
||||
.await
|
||||
.map_err(|e| e.to_string())?
|
||||
}
|
||||
|
||||
pub fn write(&self, key: &str, value: &[u8]) -> Result<(), String> {
|
||||
let conn = self.conn.lock().unwrap();
|
||||
let text_value = String::from_utf8(value.to_vec()).ok();
|
||||
pub async fn write(&self, key: &str, value: &[u8]) -> Result<(), String> {
|
||||
let conn = self.conn.clone();
|
||||
let key = key.to_string();
|
||||
let value = value.to_vec();
|
||||
tokio::task::spawn_blocking(move || {
|
||||
let conn = conn.lock().unwrap();
|
||||
let text_value: Option<String> =
|
||||
std::str::from_utf8(&value).ok().map(str::to_owned);
|
||||
conn.execute(
|
||||
"INSERT OR REPLACE INTO kv (key, value, text_value, updated_at) VALUES (?1, ?2, ?3, strftime('%s','now'))",
|
||||
params![key, value, text_value],
|
||||
)
|
||||
.map_err(|e| e.to_string())?;
|
||||
Ok(())
|
||||
})
|
||||
.await
|
||||
.map_err(|e| e.to_string())?
|
||||
}
|
||||
|
||||
pub fn delete(&self, key: &str) -> Result<(), String> {
|
||||
let conn = self.conn.lock().unwrap();
|
||||
pub async fn delete(&self, key: &str) -> Result<(), String> {
|
||||
let conn = self.conn.clone();
|
||||
let key = key.to_string();
|
||||
tokio::task::spawn_blocking(move || {
|
||||
let conn = conn.lock().unwrap();
|
||||
conn.execute("DELETE FROM kv WHERE key = ?1", params![key])
|
||||
.map_err(|e| e.to_string())?;
|
||||
Ok(())
|
||||
})
|
||||
.await
|
||||
.map_err(|e| e.to_string())?
|
||||
}
|
||||
|
||||
pub fn list(&self, prefix: &str, limit: i32) -> Result<Vec<String>, String> {
|
||||
let conn = self.conn.lock().unwrap();
|
||||
pub async fn list(&self, prefix: &str, limit: i32) -> Result<Vec<String>, String> {
|
||||
let conn = self.conn.clone();
|
||||
let prefix = prefix.to_string();
|
||||
tokio::task::spawn_blocking(move || {
|
||||
let conn = conn.lock().unwrap();
|
||||
let lim = if limit > 0 { limit } else { 1000 };
|
||||
let mut stmt = conn
|
||||
.prepare("SELECT key FROM kv WHERE key LIKE ?1 ORDER BY key LIMIT ?2")
|
||||
@@ -82,10 +107,20 @@ impl MemorySyscall {
|
||||
keys.push(row.map_err(|e| e.to_string())?);
|
||||
}
|
||||
Ok(keys)
|
||||
})
|
||||
.await
|
||||
.map_err(|e| e.to_string())?
|
||||
}
|
||||
|
||||
pub fn search(&self, query: &str, limit: i32) -> Result<Vec<(String, String, f32)>, String> {
|
||||
let conn = self.conn.lock().unwrap();
|
||||
pub async fn search(
|
||||
&self,
|
||||
query: &str,
|
||||
limit: i32,
|
||||
) -> Result<Vec<(String, String, f32)>, String> {
|
||||
let conn = self.conn.clone();
|
||||
let query = query.to_string();
|
||||
tokio::task::spawn_blocking(move || {
|
||||
let conn = conn.lock().unwrap();
|
||||
let lim = if limit > 0 { limit } else { 20 };
|
||||
let mut stmt = conn
|
||||
.prepare(
|
||||
@@ -107,5 +142,8 @@ impl MemorySyscall {
|
||||
hits.push(row.map_err(|e| e.to_string())?);
|
||||
}
|
||||
Ok(hits)
|
||||
})
|
||||
.await
|
||||
.map_err(|e| e.to_string())?
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
pub mod display;
|
||||
pub mod audio;
|
||||
pub mod display;
|
||||
pub mod fs;
|
||||
pub mod hid;
|
||||
pub mod input;
|
||||
pub mod memory;
|
||||
pub mod network;
|
||||
pub mod process;
|
||||
pub mod timer;
|
||||
pub mod hid;
|
||||
pub mod input;
|
||||
|
||||
use anyhow::Result;
|
||||
use std::sync::Arc;
|
||||
|
||||
@@ -1,60 +1,94 @@
|
||||
use std::collections::HashMap;
|
||||
use std::net::SocketAddr;
|
||||
use std::time::Duration;
|
||||
|
||||
const MAX_RESPONSE_BODY: u64 = 16 * 1024 * 1024; // 16 MB
|
||||
|
||||
/// Network syscall: remote communication.
|
||||
pub struct NetworkSyscall;
|
||||
pub struct NetworkSyscall {
|
||||
client: reqwest::Client,
|
||||
}
|
||||
|
||||
impl NetworkSyscall {
|
||||
pub fn new() -> Self {
|
||||
Self
|
||||
let client = reqwest::Client::builder()
|
||||
.connect_timeout(Duration::from_secs(5))
|
||||
.read_timeout(Duration::from_secs(30))
|
||||
.timeout(Duration::from_secs(60))
|
||||
.redirect(reqwest::redirect::Policy::limited(5))
|
||||
.build()
|
||||
.unwrap_or_else(|_| reqwest::Client::new());
|
||||
Self { client }
|
||||
}
|
||||
|
||||
pub fn available(&self) -> bool {
|
||||
// Simple connectivity check: try to resolve a known host
|
||||
std::net::TcpStream::connect_timeout(
|
||||
&std::net::SocketAddr::from(([8, 8, 8, 8], 53)),
|
||||
std::time::Duration::from_secs(2),
|
||||
pub async fn available(&self) -> bool {
|
||||
tokio::time::timeout(
|
||||
Duration::from_secs(2),
|
||||
tokio::net::TcpStream::connect(SocketAddr::from(([8, 8, 8, 8], 53))),
|
||||
)
|
||||
.is_ok()
|
||||
.await
|
||||
.map(|result| result.is_ok())
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
pub async fn request(
|
||||
&self,
|
||||
url: &str,
|
||||
method: &str,
|
||||
headers: &std::collections::HashMap<String, String>,
|
||||
headers: &HashMap<String, String>,
|
||||
body: &[u8],
|
||||
) -> Result<(i32, std::collections::HashMap<String, String>, Vec<u8>), String> {
|
||||
// Minimal HTTP client using tokio TcpStream + manual HTTP.
|
||||
// For production, use hyper or reqwest. For now, shell out to a basic impl.
|
||||
use tokio::process::Command;
|
||||
) -> Result<(i32, HashMap<String, String>, Vec<u8>), String> {
|
||||
let method = method
|
||||
.parse::<reqwest::Method>()
|
||||
.map_err(|e| format!("invalid HTTP method {method:?}: {e}"))?;
|
||||
let mut request = self.client.request(method, url);
|
||||
|
||||
let mut cmd = Command::new("curl");
|
||||
cmd.arg("-s")
|
||||
.arg("-X")
|
||||
.arg(method)
|
||||
.arg("-o")
|
||||
.arg("-")
|
||||
.arg("-w")
|
||||
.arg("\n%{http_code}");
|
||||
|
||||
for (k, v) in headers {
|
||||
cmd.arg("-H").arg(format!("{}: {}", k, v));
|
||||
for (name, value) in headers {
|
||||
let header_name = name
|
||||
.parse::<reqwest::header::HeaderName>()
|
||||
.map_err(|e| format!("invalid HTTP header name {name:?}: {e}"))?;
|
||||
let header_value = value
|
||||
.parse::<reqwest::header::HeaderValue>()
|
||||
.map_err(|e| format!("invalid HTTP header value for {name}: {e}"))?;
|
||||
request = request.header(header_name, header_value);
|
||||
}
|
||||
|
||||
if !body.is_empty() {
|
||||
cmd.arg("-d").arg(String::from_utf8_lossy(body).to_string());
|
||||
request = request.body(body.to_vec());
|
||||
}
|
||||
|
||||
cmd.arg(url);
|
||||
let response = request.send().await.map_err(|e| e.to_string())?;
|
||||
|
||||
let output = cmd.output().await.map_err(|e| e.to_string())?;
|
||||
let raw = String::from_utf8_lossy(&output.stdout);
|
||||
let lines: Vec<&str> = raw.rsplitn(2, '\n').collect();
|
||||
// Check content-length before downloading body
|
||||
if let Some(len) = response.content_length() {
|
||||
if len > MAX_RESPONSE_BODY {
|
||||
return Err(format!(
|
||||
"response body too large: {} bytes (limit {} bytes)",
|
||||
len, MAX_RESPONSE_BODY
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
let status = lines
|
||||
.first()
|
||||
.and_then(|s| s.parse::<i32>().ok())
|
||||
.unwrap_or(0);
|
||||
let response_body = lines.get(1).unwrap_or(&"").as_bytes().to_vec();
|
||||
let status = i32::from(response.status().as_u16());
|
||||
let resp_headers = response
|
||||
.headers()
|
||||
.iter()
|
||||
.map(|(name, value)| {
|
||||
(
|
||||
name.to_string(),
|
||||
value.to_str().unwrap_or_default().to_string(),
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
let resp_body = response.bytes().await.map_err(|e| e.to_string())?;
|
||||
if resp_body.len() as u64 > MAX_RESPONSE_BODY {
|
||||
return Err(format!(
|
||||
"response body too large: {} bytes (limit {} bytes)",
|
||||
resp_body.len(),
|
||||
MAX_RESPONSE_BODY
|
||||
));
|
||||
}
|
||||
|
||||
Ok((status, std::collections::HashMap::new(), response_body))
|
||||
Ok((status, resp_headers, resp_body.to_vec()))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,23 +1,31 @@
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Mutex;
|
||||
use std::sync::Arc;
|
||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||
use tokio::process::Command;
|
||||
use tokio::sync::{mpsc, Mutex, RwLock};
|
||||
|
||||
/// Per-process managed state, decoupled from the Child handle.
|
||||
struct ManagedProcess {
|
||||
cmd: String,
|
||||
status: Arc<Mutex<String>>,
|
||||
stdin_tx: Option<mpsc::Sender<Vec<u8>>>,
|
||||
stdout_rx: Arc<Mutex<mpsc::Receiver<Vec<u8>>>>,
|
||||
/// Notified when the process exits. Clone the receiver to wait.
|
||||
exit_notify: Arc<tokio::sync::Notify>,
|
||||
/// Handle to abort background tasks on kill.
|
||||
_tasks: Vec<tokio::task::JoinHandle<()>>,
|
||||
}
|
||||
|
||||
/// Process syscall: spawn and manage subprocesses.
|
||||
/// Uses the real OS PID as the identifier.
|
||||
pub struct ProcessSyscall {
|
||||
procs: Mutex<HashMap<u64, ManagedProcess>>,
|
||||
children: tokio::sync::Mutex<HashMap<u64, tokio::process::Child>>,
|
||||
procs: RwLock<HashMap<u64, Arc<ManagedProcess>>>,
|
||||
}
|
||||
|
||||
impl ProcessSyscall {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
procs: Mutex::new(HashMap::new()),
|
||||
children: tokio::sync::Mutex::new(HashMap::new()),
|
||||
procs: RwLock::new(HashMap::new()),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,86 +47,244 @@ impl ProcessSyscall {
|
||||
command
|
||||
.stdin(std::process::Stdio::piped())
|
||||
.stdout(std::process::Stdio::piped())
|
||||
.stderr(std::process::Stdio::piped());
|
||||
.stderr(std::process::Stdio::piped())
|
||||
.kill_on_drop(true);
|
||||
|
||||
let child = command.spawn().map_err(|e| e.to_string())?;
|
||||
let mut child = command.spawn().map_err(|e| e.to_string())?;
|
||||
let pid = child
|
||||
.id()
|
||||
.ok_or_else(|| "spawned process has no OS pid".to_string())? as u64;
|
||||
|
||||
self.procs.lock().unwrap().insert(
|
||||
pid,
|
||||
ManagedProcess {
|
||||
cmd: cmd.to_string(),
|
||||
},
|
||||
);
|
||||
self.children.lock().await.insert(pid, child);
|
||||
// Take ownership of stdio handles
|
||||
let child_stdin = child.stdin.take();
|
||||
let child_stdout = child.stdout.take();
|
||||
let child_stderr = child.stderr.take();
|
||||
|
||||
let status = Arc::new(Mutex::new("running".to_string()));
|
||||
let exit_notify = Arc::new(tokio::sync::Notify::new());
|
||||
|
||||
// Stdin writer task
|
||||
let (stdin_tx, mut stdin_rx) = mpsc::channel::<Vec<u8>>(64);
|
||||
let stdin_task = tokio::spawn(async move {
|
||||
if let Some(mut stdin) = child_stdin {
|
||||
while let Some(data) = stdin_rx.recv().await {
|
||||
if stdin.write_all(&data).await.is_err() {
|
||||
break;
|
||||
}
|
||||
if stdin.flush().await.is_err() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Stdout reader task
|
||||
let (stdout_tx, stdout_rx) = mpsc::channel::<Vec<u8>>(64);
|
||||
let stdout_task = tokio::spawn(async move {
|
||||
if let Some(mut stdout) = child_stdout {
|
||||
loop {
|
||||
let mut buf = vec![0u8; 4096];
|
||||
match stdout.read(&mut buf).await {
|
||||
Ok(0) => break,
|
||||
Ok(n) => {
|
||||
buf.truncate(n);
|
||||
if stdout_tx.send(buf).await.is_err() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Err(_) => break,
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Stderr drain task (prevent pipe fill-up)
|
||||
let stderr_task = tokio::spawn(async move {
|
||||
if let Some(mut stderr) = child_stderr {
|
||||
let mut buf = vec![0u8; 4096];
|
||||
loop {
|
||||
match stderr.read(&mut buf).await {
|
||||
Ok(0) | Err(_) => break,
|
||||
Ok(_) => {} // discard
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Reaper task: waits for exit and updates status
|
||||
let status_clone = status.clone();
|
||||
let exit_notify_clone = exit_notify.clone();
|
||||
let reaper_task = tokio::spawn(async move {
|
||||
let exit_status = child.wait().await;
|
||||
let code = exit_status.map(|s| s.code().unwrap_or(-1)).unwrap_or(-1);
|
||||
*status_clone.lock().await = format!("exited({})", code);
|
||||
exit_notify_clone.notify_waiters();
|
||||
});
|
||||
|
||||
let managed = Arc::new(ManagedProcess {
|
||||
cmd: cmd.to_string(),
|
||||
status,
|
||||
stdin_tx: Some(stdin_tx),
|
||||
stdout_rx: Arc::new(Mutex::new(stdout_rx)),
|
||||
exit_notify,
|
||||
_tasks: vec![stdin_task, stdout_task, stderr_task, reaper_task],
|
||||
});
|
||||
|
||||
self.procs.write().await.insert(pid, managed);
|
||||
tracing::info!("process spawned: pid={} cmd={}", pid, cmd);
|
||||
Ok(pid)
|
||||
}
|
||||
|
||||
pub async fn kill(&self, pid: u64) -> Result<(), String> {
|
||||
let mut child = {
|
||||
let mut children = self.children.lock().await;
|
||||
children.remove(&pid).ok_or_else(|| format!("pid {} not found", pid))?
|
||||
};
|
||||
let procs = self.procs.read().await;
|
||||
let proc = procs
|
||||
.get(&pid)
|
||||
.ok_or_else(|| format!("pid {} not found", pid))?
|
||||
.clone();
|
||||
drop(procs);
|
||||
|
||||
child.kill().await.map_err(|e| e.to_string())?;
|
||||
self.procs.lock().unwrap().remove(&pid);
|
||||
// Abort all background tasks (stdin writer, stdout reader, stderr drain, reaper)
|
||||
for task in proc._tasks.iter() {
|
||||
task.abort();
|
||||
}
|
||||
*proc.status.lock().await = "killed".to_string();
|
||||
proc.exit_notify.notify_waiters();
|
||||
|
||||
// Also send OS kill
|
||||
#[cfg(unix)]
|
||||
{
|
||||
let os_pid = pid as i32;
|
||||
unsafe { libc::kill(os_pid as libc::pid_t, 9) };
|
||||
}
|
||||
#[cfg(not(unix))]
|
||||
{
|
||||
// On Windows, aborting the reaper task triggers kill_on_drop on the Child
|
||||
}
|
||||
|
||||
self.procs.write().await.remove(&pid);
|
||||
tracing::info!("process killed: pid={}", pid);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn wait(&self, pid: u64) -> Result<i32, String> {
|
||||
let mut child = {
|
||||
let mut children = self.children.lock().await;
|
||||
children.remove(&pid).ok_or_else(|| format!("pid {} not found", pid))?
|
||||
};
|
||||
self.procs.lock().unwrap().remove(&pid);
|
||||
let status = child.wait().await.map_err(|e| e.to_string())?;
|
||||
Ok(status.code().unwrap_or(-1))
|
||||
let procs = self.procs.read().await;
|
||||
let proc = procs
|
||||
.get(&pid)
|
||||
.ok_or_else(|| format!("pid {} not found", pid))?
|
||||
.clone();
|
||||
drop(procs);
|
||||
|
||||
// Check if already exited before waiting
|
||||
{
|
||||
let status = proc.status.lock().await;
|
||||
if status.starts_with("exited") || *status == "killed" {
|
||||
let s = status.clone();
|
||||
drop(status);
|
||||
self.procs.write().await.remove(&pid);
|
||||
return parse_exit_code(&s);
|
||||
}
|
||||
}
|
||||
|
||||
// Wait for exit notification
|
||||
proc.exit_notify.notified().await;
|
||||
let status = proc.status.lock().await.clone();
|
||||
|
||||
// Clean up from map
|
||||
self.procs.write().await.remove(&pid);
|
||||
parse_exit_code(&status)
|
||||
}
|
||||
|
||||
pub async fn write_stdin(&self, pid: u64, data: &[u8]) -> Result<(), String> {
|
||||
use tokio::io::AsyncWriteExt;
|
||||
let mut children = self.children.lock().await;
|
||||
if let Some(child) = children.get_mut(&pid) {
|
||||
if let Some(stdin) = child.stdin.as_mut() {
|
||||
stdin.write_all(data).await.map_err(|e| e.to_string())?;
|
||||
stdin.flush().await.map_err(|e| e.to_string())?;
|
||||
Ok(())
|
||||
let procs = self.procs.read().await;
|
||||
let proc = procs
|
||||
.get(&pid)
|
||||
.ok_or_else(|| format!("pid {} not found", pid))?
|
||||
.clone();
|
||||
drop(procs);
|
||||
|
||||
if let Some(ref tx) = proc.stdin_tx {
|
||||
tx.send(data.to_vec())
|
||||
.await
|
||||
.map_err(|_| "stdin channel closed".to_string())
|
||||
} else {
|
||||
Err("stdin not available".to_string())
|
||||
}
|
||||
} else {
|
||||
Err(format!("pid {} not found", pid))
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn read_stdout(&self, pid: u64, buf: &mut [u8]) -> Result<usize, String> {
|
||||
use tokio::io::AsyncReadExt;
|
||||
let mut children = self.children.lock().await;
|
||||
if let Some(child) = children.get_mut(&pid) {
|
||||
if let Some(stdout) = child.stdout.as_mut() {
|
||||
stdout.read(buf).await.map_err(|e| e.to_string())
|
||||
} else {
|
||||
Err("stdout not available".to_string())
|
||||
let procs = self.procs.read().await;
|
||||
let proc = procs
|
||||
.get(&pid)
|
||||
.ok_or_else(|| format!("pid {} not found", pid))?
|
||||
.clone();
|
||||
drop(procs);
|
||||
|
||||
let mut rx = proc.stdout_rx.lock().await;
|
||||
match rx.recv().await {
|
||||
Some(data) => {
|
||||
let n = std::cmp::min(data.len(), buf.len());
|
||||
buf[..n].copy_from_slice(&data[..n]);
|
||||
Ok(n)
|
||||
}
|
||||
} else {
|
||||
Err(format!("pid {} not found", pid))
|
||||
None => Ok(0), // EOF
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn signal(&self, pid: u64, _signal: i32) -> Result<(), String> {
|
||||
self.kill(pid).await
|
||||
pub async fn signal(&self, pid: u64, signal: i32) -> Result<(), String> {
|
||||
if signal == 9 {
|
||||
return self.kill(pid).await;
|
||||
}
|
||||
|
||||
pub fn list(&self) -> Vec<(u64, String, String)> {
|
||||
let procs = self.procs.lock().unwrap();
|
||||
procs
|
||||
.iter()
|
||||
.map(|(pid, mp)| (*pid, mp.cmd.clone(), "running".to_string()))
|
||||
.collect()
|
||||
let procs = self.procs.read().await;
|
||||
if !procs.contains_key(&pid) {
|
||||
return Err(format!("pid {} not found", pid));
|
||||
}
|
||||
drop(procs);
|
||||
|
||||
#[cfg(unix)]
|
||||
{
|
||||
let os_pid = i32::try_from(pid)
|
||||
.map_err(|_| format!("pid {} out of range for platform signal API", pid))?;
|
||||
let result = unsafe { libc::kill(os_pid as libc::pid_t, signal as libc::c_int) };
|
||||
if result == 0 {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(format!(
|
||||
"signal {} to pid {} failed: {}",
|
||||
signal,
|
||||
pid,
|
||||
std::io::Error::last_os_error()
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(unix))]
|
||||
{
|
||||
Err(format!(
|
||||
"process.signal only supports signal 9 on this platform, got {}",
|
||||
signal
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn list(&self) -> Vec<(u64, String, String)> {
|
||||
let procs = self.procs.read().await;
|
||||
let mut result = Vec::with_capacity(procs.len());
|
||||
for (pid, mp) in procs.iter() {
|
||||
let status = mp.status.lock().await.clone();
|
||||
result.push((*pid, mp.cmd.clone(), status));
|
||||
}
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_exit_code(status: &str) -> Result<i32, String> {
|
||||
if let Some(code_str) = status
|
||||
.strip_prefix("exited(")
|
||||
.and_then(|s| s.strip_suffix(')'))
|
||||
{
|
||||
code_str.parse::<i32>().map_err(|e| e.to_string())
|
||||
} else {
|
||||
Ok(-1)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,12 +52,10 @@ impl TimerSyscall {
|
||||
timers.lock().unwrap().remove(&tid);
|
||||
});
|
||||
|
||||
self.timers.lock().unwrap().insert(
|
||||
timer_id.clone(),
|
||||
TimerEntry {
|
||||
handle,
|
||||
},
|
||||
);
|
||||
self.timers
|
||||
.lock()
|
||||
.unwrap()
|
||||
.insert(timer_id.clone(), TimerEntry { handle });
|
||||
timer_id
|
||||
}
|
||||
|
||||
|
||||
@@ -304,7 +304,7 @@ impl CheckRunner {
|
||||
.into_inner();
|
||||
let names: Vec<&str> = list.entries.iter().map(|e| e.name.as_str()).collect();
|
||||
ensure!(
|
||||
list.ok && names.iter().any(|name| *name == "sample.txt"),
|
||||
list.ok && names.contains(&"sample.txt"),
|
||||
"list mismatch ok={} names={:?} error={:?}",
|
||||
list.ok,
|
||||
names,
|
||||
@@ -341,12 +341,13 @@ impl CheckRunner {
|
||||
|
||||
async fn process_roundtrip(&mut self) -> Result<String> {
|
||||
let mut process = ProcessClient::new(self.channel.clone());
|
||||
let (cmd, args) = process_command();
|
||||
let resp = process
|
||||
.spawn(request_with_plugin(
|
||||
PLUGIN_ID,
|
||||
SpawnRequest {
|
||||
cmd: "cmd".to_string(),
|
||||
args: vec!["/C".to_string(), format!("echo {PROCESS_OK}")],
|
||||
cmd,
|
||||
args,
|
||||
env: HashMap::new(),
|
||||
cwd: executable_dir()
|
||||
.map(|path| path_string(&path))
|
||||
@@ -504,6 +505,20 @@ fn executable_data_dir() -> PathBuf {
|
||||
.join("data")
|
||||
}
|
||||
|
||||
fn process_command() -> (String, Vec<String>) {
|
||||
if cfg!(windows) {
|
||||
(
|
||||
"cmd".to_string(),
|
||||
vec!["/C".to_string(), format!("echo {PROCESS_OK}")],
|
||||
)
|
||||
} else {
|
||||
(
|
||||
"sh".to_string(),
|
||||
vec!["-c".to_string(), format!("printf %s {PROCESS_OK}")],
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn path_string(path: &std::path::Path) -> String {
|
||||
path.to_string_lossy().to_string()
|
||||
}
|
||||
|
||||
@@ -14,6 +14,8 @@ anyhow = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
tokio = { workspace = true }
|
||||
tokio-stream = { workspace = true }
|
||||
tonic = { workspace = true }
|
||||
futures-core = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
tracing-subscriber = { workspace = true }
|
||||
|
||||
@@ -12,7 +12,7 @@ use tonic::{transport::Server, Request, Response, Status};
|
||||
const PLUGIN_ID: &str = "claudecode";
|
||||
const DEFAULT_PORT: u16 = 50053;
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[derive(Debug, Default, Deserialize)]
|
||||
struct CallArgs {
|
||||
#[serde(default)]
|
||||
prompt: String,
|
||||
@@ -32,26 +32,22 @@ fn find_claude_bin() -> Option<String> {
|
||||
return Some(bin);
|
||||
}
|
||||
|
||||
let candidates = vec![
|
||||
"claude",
|
||||
"claude.cmd",
|
||||
&std::env::var("APPDATA")
|
||||
.map(|p| format!("{}\\npm\\claude.cmd", p))
|
||||
.unwrap_or_default(),
|
||||
];
|
||||
let appdata_claude = std::env::var("APPDATA")
|
||||
.ok()
|
||||
.map(|p| format!("{}\\npm\\claude.cmd", p));
|
||||
let mut candidates = vec!["claude".to_string(), "claude.cmd".to_string()];
|
||||
if let Some(path) = appdata_claude {
|
||||
candidates.push(path);
|
||||
}
|
||||
|
||||
for candidate in candidates {
|
||||
if std::process::Command::new(candidate)
|
||||
candidates.into_iter().find(|candidate| {
|
||||
std::process::Command::new(candidate)
|
||||
.arg("--version")
|
||||
.stdout(Stdio::null())
|
||||
.stderr(Stdio::null())
|
||||
.status()
|
||||
.is_ok()
|
||||
{
|
||||
return Some(candidate.to_string());
|
||||
}
|
||||
}
|
||||
None
|
||||
})
|
||||
}
|
||||
|
||||
async fn call_claude(
|
||||
@@ -97,7 +93,10 @@ async fn call_claude(
|
||||
drop(stdin);
|
||||
}
|
||||
|
||||
let output = tokio::time::timeout(std::time::Duration::from_secs(300), child.wait_with_output())
|
||||
let output = tokio::time::timeout(
|
||||
std::time::Duration::from_secs(300),
|
||||
child.wait_with_output(),
|
||||
)
|
||||
.await
|
||||
.context("claude timeout (300s)")??;
|
||||
|
||||
@@ -148,8 +147,10 @@ impl ClaudeCodePlugin {
|
||||
.await
|
||||
}
|
||||
"review" => {
|
||||
let review_prompt =
|
||||
format!("Review the following code or changes. Be concise and actionable:\n\n{}", prompt);
|
||||
let review_prompt = format!(
|
||||
"Review the following code or changes. Be concise and actionable:\n\n{}",
|
||||
prompt
|
||||
);
|
||||
call_claude(
|
||||
&self.claude_bin,
|
||||
&review_prompt,
|
||||
@@ -184,7 +185,10 @@ impl ClaudeCodePlugin {
|
||||
|
||||
#[tonic::async_trait]
|
||||
impl PluginBridge for ClaudeCodePlugin {
|
||||
async fn register(&self, _req: Request<PluginInfo>) -> Result<Response<RegisterResponse>, Status> {
|
||||
async fn register(
|
||||
&self,
|
||||
_req: Request<PluginInfo>,
|
||||
) -> Result<Response<RegisterResponse>, Status> {
|
||||
Err(Status::unimplemented("not a bridge"))
|
||||
}
|
||||
|
||||
@@ -195,13 +199,19 @@ impl PluginBridge for ClaudeCodePlugin {
|
||||
Ok(Response::new(response))
|
||||
}
|
||||
|
||||
type StreamCallStream = futures_core::stream::Empty<Result<CallResponse, Status>>;
|
||||
type StreamCallStream = tokio_stream::Empty<Result<CallResponse, Status>>;
|
||||
|
||||
async fn stream_call(&self, _req: Request<CallRequest>) -> Result<Response<Self::StreamCallStream>, Status> {
|
||||
async fn stream_call(
|
||||
&self,
|
||||
_req: Request<CallRequest>,
|
||||
) -> Result<Response<Self::StreamCallStream>, Status> {
|
||||
Err(Status::unimplemented("stream_call not supported"))
|
||||
}
|
||||
|
||||
async fn heartbeat(&self, _req: Request<HeartbeatRequest>) -> Result<Response<HeartbeatResponse>, Status> {
|
||||
async fn heartbeat(
|
||||
&self,
|
||||
_req: Request<HeartbeatRequest>,
|
||||
) -> Result<Response<HeartbeatResponse>, Status> {
|
||||
Ok(Response::new(HeartbeatResponse { ok: true }))
|
||||
}
|
||||
}
|
||||
@@ -210,7 +220,8 @@ impl PluginBridge for ClaudeCodePlugin {
|
||||
async fn main() -> Result<()> {
|
||||
tracing_subscriber::fmt::init();
|
||||
|
||||
let claude_bin = find_claude_bin().context("Claude Code CLI not found. Install: npm install -g @anthropic-ai/claude-code")?;
|
||||
let claude_bin = find_claude_bin()
|
||||
.context("Claude Code CLI not found. Install: npm install -g @anthropic-ai/claude-code")?;
|
||||
tracing::info!("Found Claude Code CLI: {}", claude_bin);
|
||||
|
||||
let endpoint = agentsd_plugin_sdk::plugin_endpoint_from_env(DEFAULT_PORT);
|
||||
@@ -224,21 +235,42 @@ async fn main() -> Result<()> {
|
||||
"Claude Code",
|
||||
env!("CARGO_PKG_VERSION"),
|
||||
"standard",
|
||||
&["process.spawn", "display.text", "fs.read", "fs.write", "network.request"],
|
||||
&[
|
||||
"process.spawn",
|
||||
"display.text",
|
||||
"fs.read",
|
||||
"fs.write",
|
||||
"network.request",
|
||||
],
|
||||
&["hermes"],
|
||||
vec![
|
||||
export("chat", "Send prompt to Claude Code, get text response (no tools)"),
|
||||
export("code", "Generate or edit code with Claude Code (Bash/Read/Edit/Write)"),
|
||||
export(
|
||||
"chat",
|
||||
"Send prompt to Claude Code, get text response (no tools)",
|
||||
),
|
||||
export(
|
||||
"code",
|
||||
"Generate or edit code with Claude Code (Bash/Read/Edit/Write)",
|
||||
),
|
||||
export("review", "Review code changes (Read/Glob/Grep only)"),
|
||||
export("ask", "Ask about the codebase (Read/Glob/Grep, max 3 turns)"),
|
||||
export("run", "Full Claude Code access (all tools, unlimited turns)"),
|
||||
export(
|
||||
"ask",
|
||||
"Ask about the codebase (Read/Glob/Grep, max 3 turns)",
|
||||
),
|
||||
export(
|
||||
"run",
|
||||
"Full Claude Code access (all tools, unlimited turns)",
|
||||
),
|
||||
],
|
||||
&endpoint,
|
||||
),
|
||||
)
|
||||
.await?;
|
||||
|
||||
tracing::info!("Registered with agentsd: session={}", register_resp.session_id);
|
||||
tracing::info!(
|
||||
"Registered with agentsd: session={}",
|
||||
register_resp.session_id
|
||||
);
|
||||
|
||||
let plugin = ClaudeCodePlugin::new(claude_bin);
|
||||
let addr = listen_addr.parse()?;
|
||||
|
||||
@@ -26,7 +26,10 @@ async fn main() -> Result<()> {
|
||||
.await?
|
||||
.into_inner();
|
||||
ensure!(register.ok, "register failed: {}", register.error);
|
||||
println!("Register: ok={} session={}", register.ok, register.session_id);
|
||||
println!(
|
||||
"Register: ok={} session={}",
|
||||
register.ok, register.session_id
|
||||
);
|
||||
|
||||
let mut display = DisplayClient::new(channel.clone());
|
||||
let resp = display
|
||||
@@ -84,7 +87,11 @@ async fn main() -> Result<()> {
|
||||
ensure!(resp.ok, "FS.List failed: {}", resp.error);
|
||||
println!("FS.List: ok={} entries={}", resp.ok, resp.entries.len());
|
||||
for entry in resp.entries.iter().take(5) {
|
||||
println!(" {} {}", entry.name, if entry.is_dir { "[dir]" } else { "" });
|
||||
println!(
|
||||
" {} {}",
|
||||
entry.name,
|
||||
if entry.is_dir { "[dir]" } else { "" }
|
||||
);
|
||||
}
|
||||
|
||||
println!("\nAll syscalls verified OK!");
|
||||
|
||||
@@ -44,7 +44,9 @@ impl PluginBridge for HermesPlugin {
|
||||
&self,
|
||||
_req: Request<PluginInfo>,
|
||||
) -> std::result::Result<Response<RegisterResponse>, Status> {
|
||||
Err(Status::unimplemented("hermes plugin is not an agentsd bridge"))
|
||||
Err(Status::unimplemented(
|
||||
"hermes plugin is not an agentsd bridge",
|
||||
))
|
||||
}
|
||||
|
||||
async fn call(
|
||||
@@ -62,13 +64,16 @@ impl PluginBridge for HermesPlugin {
|
||||
Ok(Response::new(response))
|
||||
}
|
||||
|
||||
type StreamCallStream = tokio_stream::wrappers::ReceiverStream<std::result::Result<CallResponse, Status>>;
|
||||
type StreamCallStream =
|
||||
tokio_stream::wrappers::ReceiverStream<std::result::Result<CallResponse, Status>>;
|
||||
|
||||
async fn stream_call(
|
||||
&self,
|
||||
_req: Request<CallRequest>,
|
||||
) -> std::result::Result<Response<Self::StreamCallStream>, Status> {
|
||||
Err(Status::unimplemented("stream_call is not implemented for hermes"))
|
||||
Err(Status::unimplemented(
|
||||
"stream_call is not implemented for hermes",
|
||||
))
|
||||
}
|
||||
|
||||
async fn heartbeat(
|
||||
@@ -110,7 +115,7 @@ async fn main() -> Result<()> {
|
||||
println!("Hermes PluginBridge server listening on {}", listen_addr);
|
||||
|
||||
Server::builder()
|
||||
.add_service(PluginBridgeServer::new(HermesPlugin::default()))
|
||||
.add_service(PluginBridgeServer::new(HermesPlugin))
|
||||
.serve_with_incoming(incoming)
|
||||
.await?;
|
||||
Ok(())
|
||||
|
||||
@@ -59,6 +59,7 @@ pub fn plugin_info(
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn plugin_info_with_endpoint(
|
||||
id: &str,
|
||||
name: &str,
|
||||
@@ -95,7 +96,11 @@ pub fn encode_json<T: Serialize>(value: &T) -> Result<Vec<u8>> {
|
||||
serde_json::to_vec(value).context("encode JSON")
|
||||
}
|
||||
|
||||
pub fn call_response_json<T: Serialize>(value: &T, ok: bool, error: impl Into<String>) -> CallResponse {
|
||||
pub fn call_response_json<T: Serialize>(
|
||||
value: &T,
|
||||
ok: bool,
|
||||
error: impl Into<String>,
|
||||
) -> CallResponse {
|
||||
match encode_json(value) {
|
||||
Ok(result) => CallResponse {
|
||||
result,
|
||||
|
||||
Reference in New Issue
Block a user