From a5f6dfa9723dcf783d155655550114abafd34c3d Mon Sep 17 00:00:00 2001 From: kjuulh Date: Wed, 7 Jan 2026 14:12:49 +0100 Subject: [PATCH] feat: add basic process manager Signed-off-by: kjuulh --- .drone.yml | 2 + .gitignore | 2 + Cargo.lock | 634 ++++++++++++++++++++++++++++++++++++ Cargo.toml | 11 + crates/noprocess/Cargo.toml | 12 + crates/noprocess/src/lib.rs | 373 +++++++++++++++++++++ cuddle.yaml | 17 + 7 files changed, 1051 insertions(+) create mode 100644 .drone.yml create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 crates/noprocess/Cargo.toml create mode 100644 crates/noprocess/src/lib.rs create mode 100644 cuddle.yaml diff --git a/.drone.yml b/.drone.yml new file mode 100644 index 0000000..7d36638 --- /dev/null +++ b/.drone.yml @@ -0,0 +1,2 @@ +kind: template +load: cuddle-rust-lib-plan.yaml diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9c4c004 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +target/ +.cuddle/ diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..29e24d3 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,634 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "anyhow" +version = "1.0.71" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c7d0618f0e0b7e8ff11427422b64564d5fb0be1940354bfe2e0529b18a9d9b8" + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bumpalo" +version = "3.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" + +[[package]] +name = "bytes" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-sink" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", +] + +[[package]] +name = "js-sys" +version = "0.3.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "464a3709c7f55f1f721e5389aa6ea4e3bc6aba669353300af094b29ffbdde1d8" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "libc" +version = "0.2.179" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5a2d376baa530d1238d133232d15e239abad80d05838b4b59354e5268af431f" + +[[package]] +name = "lock_api" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1cc9717a20b1bb222f333e6a92fd32f7d8a18ddc5a3191a11af45dcbf4dcd16" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b06a4cde4c0f271a446782e3eff8de789548ce57dbc8eca9292c27f4a42004b4" + +[[package]] +name = "mio" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "noprocess" +version = "0.1.0" +dependencies = [ + "anyhow", + "thiserror", + "tokio", + "tokio-util", + "tracing", + "uuid", +] + +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "once_cell" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" + +[[package]] +name = "parking_lot" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93f00c865fe7cabf650081affecd3871070f26767e7b2070a3ffae14c654b447" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets 0.48.0", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "proc-macro2" +version = "1.0.105" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "535d180e0ecab6268a3e718bb9fd44db66bbbc256257165fc699dadf70d16fe7" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc74d9a594b72ae6656596548f56f667211f8a97b3d4c3d467150794690dc40a" +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 = "redox_syscall" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29" +dependencies = [ + "bitflags", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "scopeguard" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" +dependencies = [ + "libc", +] + +[[package]] +name = "simple" +version = "0.1.0" +dependencies = [ + "anyhow", + "noprocess", + "tokio", + "tokio-util", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "smallvec" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0" + +[[package]] +name = "socket2" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881" +dependencies = [ + "libc", + "windows-sys 0.60.2", +] + +[[package]] +name = "syn" +version = "2.0.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "thiserror" +version = "2.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thread_local" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "tokio" +version = "1.49.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86" +dependencies = [ + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio-util" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tracing" +version = "0.1.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8" +dependencies = [ + "cfg-if", + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f57e3ca2a01450b1a921183a9c9cbfda207fd822cef4ccb00a65402cbba7a74" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e" +dependencies = [ + "nu-ansi-term", + "sharded-slab", + "smallvec", + "thread_local", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "unicode-ident" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15811caf2415fb889178633e7724bad2509101cde276048e013b9def5e51fa0" + +[[package]] +name = "uuid" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2e054861b4bd027cd373e18e8d8d8e6548085000e41290d95ce0c373a654b4a" +dependencies = [ + "getrandom", + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasip2" +version = "1.0.1+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d759f433fa64a2d763d1340820e46e111a7a5ab75f993d1852d70b03dbb80fd" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48cb0d2638f8baedbc542ed444afc0644a29166f1595371af4fecf8ce1e7eeb3" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cefb59d5cd5f92d9dcf80e4683949f15ca4b511f4ac0a6e14d4e1ac60c6ecd40" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbc538057e648b67f72a982e708d485b2efa771e1ac05fec311f9f63e5800db4" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b1eb6f0cd7c80c79759c929114ef071b87354ce476d9d94271031c0497adfd5" +dependencies = [ + "windows_aarch64_gnullvm 0.48.0", + "windows_aarch64_msvc 0.48.0", + "windows_i686_gnu 0.48.0", + "windows_i686_msvc 0.48.0", + "windows_x86_64_gnu 0.48.0", + "windows_x86_64_gnullvm 0.48.0", + "windows_x86_64_msvc 0.48.0", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + +[[package]] +name = "wit-bindgen" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..a9b813e --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,11 @@ +[workspace] +members = ["crates/*", "examples/*"] +resolver = "2" + +[workspace.dependencies] +noprocess = { path = "crates/noprocess" } + +anyhow = { version = "1.0.71" } +tokio = { version = "1", features = ["full"] } +tracing = { version = "0.1", features = ["log"] } +tokio-util = "0.7.18" diff --git a/crates/noprocess/Cargo.toml b/crates/noprocess/Cargo.toml new file mode 100644 index 0000000..c9596b2 --- /dev/null +++ b/crates/noprocess/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "noprocess" +version = "0.1.0" +edition = "2024" + +[dependencies] +anyhow.workspace = true +thiserror = "2.0.17" +tokio.workspace = true +tokio-util.workspace = true +tracing.workspace = true +uuid = { version = "1.19.0", features = ["v4", "v7"] } diff --git a/crates/noprocess/src/lib.rs b/crates/noprocess/src/lib.rs new file mode 100644 index 0000000..5f71a62 --- /dev/null +++ b/crates/noprocess/src/lib.rs @@ -0,0 +1,373 @@ +use std::{collections::BTreeMap, fmt::Display, pin::Pin, sync::Arc, time::Duration}; + +use tokio::{ + sync::{Mutex, mpsc, oneshot, watch}, + task::JoinHandle, +}; +use tokio_util::sync::CancellationToken; + +pub struct ProcessManager { + inner: Arc>, +} + +#[derive(Default)] +pub struct InnerProcessManager { + handles: BTreeMap, +} + +impl InnerProcessManager { + pub fn new() -> Self { + Self { + handles: BTreeMap::default(), + } + } + + pub fn add_process(&mut self, process: Process) -> &mut Self { + self.handles.insert(process.handle_id.clone(), process); + + self + } + + pub async fn start_process(&mut self, handle_id: &HandleID) -> anyhow::Result> { + let Some(handle) = self.handles.get_mut(handle_id) else { + return Ok(None); + }; + + handle.start().await?; + + Ok(None) + } + + pub async fn restart_process(&mut self, handle_id: &HandleID) -> anyhow::Result> { + let Some(handle) = self.handles.get_mut(handle_id) else { + return Ok(None); + }; + + handle.restart().await?; + + Ok(None) + } + + pub async fn stop_process(&mut self, handle_id: &HandleID) -> anyhow::Result> { + let Some(handle) = self.handles.get_mut(handle_id) else { + return Ok(None); + }; + + handle.stop().await?; + + Ok(None) + } +} + +pub struct Process { + handle_id: HandleID, + command_tx: mpsc::Sender, + state: watch::Receiver, + task: tokio::task::JoinHandle<()>, +} + +impl Process { + pub fn new(process: impl IntoProcess) -> Self { + Self::new_with_handle(HandleID::new_random(), process) + } + + pub fn new_with_handle(handle_id: impl Into, process: impl IntoProcess) -> Self { + let handle_id = handle_id.into(); + let process = process.into_process(); + let shutdown_config = ShutdownConfig::default(); + let (command_tx, command_rx) = mpsc::channel(1); + let (event_tx, event_rx) = watch::channel(ProcessState::Pending); + + let handle = tokio::spawn(Self::run_loop( + handle_id.clone(), + process, + command_rx, + shutdown_config, + event_tx, + )); + + Self { + handle_id, + state: event_rx, + command_tx, + + task: handle, + } + } + + async fn run_loop( + handle_id: HandleID, + process: SharedProcess, + mut commands: mpsc::Receiver, + shutdown_config: ShutdownConfig, + state: watch::Sender, + ) { + let mut current_task: Option = None; + + loop { + match current_task.take() { + Some(mut task) => { + tokio::select! { + biased; + Some(cmd) = commands.recv() => { + current_task = Self::handle_command_while_running( + cmd, + task, + &state, + &shutdown_config + ).await; + } + + result = &mut task.handle => { + Self::handle_task_completion(result, process.clone(), &state, &shutdown_config, &mut current_task).await; + } + } + } + None => match commands.recv().await { + Some(ProcessCommand::Start) => { + if matches!( + *state.borrow(), + ProcessState::Pending | ProcessState::Stopped | ProcessState::Errored + ) { + current_task = Some(RunningTask::spawn(process.clone())); + let _ = state.send(ProcessState::Running); + } + } + Some(ProcessCommand::Stop { response } | ProcessCommand::Kill { response }) => { + let _ = response.send(()); + } + None => break, + }, + } + } + } + + pub async fn start(&mut self) -> anyhow::Result<()> { + self.command_tx + .send(ProcessCommand::Start) + .await + .map_err(|_| anyhow::anyhow!("runner terminated")) + } + + pub async fn stop(&mut self) -> anyhow::Result<()> { + let (resp_tx, resp_rx) = oneshot::channel(); + + self.command_tx + .send(ProcessCommand::Stop { response: resp_tx }) + .await + .map_err(|_| anyhow::anyhow!("runner terminated"))?; + + resp_rx + .await + .map_err(|_| anyhow::anyhow!("runner terminated")) + } + + pub async fn kill(&mut self) -> anyhow::Result<()> { + let (resp_tx, resp_rx) = oneshot::channel(); + + self.command_tx + .send(ProcessCommand::Kill { response: resp_tx }) + .await + .map_err(|_| anyhow::anyhow!("runner terminated"))?; + + resp_rx + .await + .map_err(|_| anyhow::anyhow!("runner terminated")) + } + + pub async fn restart(&mut self) -> anyhow::Result<()> { + self.stop().await?; + self.start().await + } + + pub(crate) async fn wait_for(&self, desired: ProcessState) { + let mut rx = self.state.clone(); + while *rx.borrow_and_update() != desired { + if rx.changed().await.is_err() { + break; + } + } + } + + pub fn handle_id(&self) -> &HandleID { + &self.handle_id + } + + async fn handle_command_while_running( + cmd: ProcessCommand, + task: RunningTask, + state: &watch::Sender, + config: &ShutdownConfig, + ) -> Option { + match cmd { + ProcessCommand::Start => Some(task), + ProcessCommand::Stop { response } => { + task.shutdown_gracefully(config.graceful_timeout).await; + let _ = state.send(ProcessState::Stopped); + let _ = response.send(()); + None + } + ProcessCommand::Kill { response } => { + task.handle.abort(); + let _ = task.handle.await; + let _ = state.send(ProcessState::Stopped); + let _ = response.send(()); + None + } + } + } + + async fn handle_task_completion( + result: Result, tokio::task::JoinError>, + process: SharedProcess, + state: &watch::Sender, + config: &ShutdownConfig, + current_task: &mut Option, + ) { + match result { + Ok(Ok(())) => { + if config.restart_on_success { + tokio::time::sleep(config.restart_delay).await; + *current_task = Some(RunningTask::spawn(process)); + } else { + let _ = state.send(ProcessState::Stopped); + } + } + Ok(Err(e)) => { + tracing::error!("process error: {e:#}"); + let _ = state.send(ProcessState::Errored); + } + Err(e) if e.is_cancelled() => { + // Aborted - state already set by kill handler + } + Err(e) => { + tracing::error!("process panicked: {e}"); + let _ = state.send(ProcessState::Errored); + } + } + } +} + +impl Drop for Process { + fn drop(&mut self) { + self.task.abort(); + } +} + +struct RunningTask { + handle: JoinHandle>, + cancellation: CancellationToken, +} + +impl RunningTask { + pub fn spawn(process: SharedProcess) -> Self { + let cancellation = CancellationToken::new(); + let handle = tokio::spawn({ + let cancellation = cancellation.child_token(); + async move { process.process.call_async(cancellation).await } + }); + + Self { + handle, + cancellation, + } + } + + async fn shutdown_gracefully(mut self, timeout: Duration) { + self.cancellation.cancel(); + match tokio::time::timeout(timeout, &mut self.handle).await { + Ok(_) => { + tracing::debug!("process stopped") + } + Err(_) => { + tracing::error!("graceful shutdown timed out, process will be dropped"); + self.handle.abort(); + } + } + } +} + +enum ProcessCommand { + Start, + Stop { response: oneshot::Sender<()> }, + Kill { response: oneshot::Sender<()> }, +} + +pub struct ShutdownConfig { + pub graceful_timeout: Duration, + pub restart_on_success: bool, + pub restart_delay: Duration, +} + +impl Default for ShutdownConfig { + fn default() -> Self { + Self { + graceful_timeout: Duration::from_secs(5), + restart_on_success: true, + restart_delay: Duration::ZERO, + } + } +} + +#[derive(PartialEq)] +pub(crate) enum ProcessState { + Pending, + Running, + Errored, + Stopped, +} + +#[derive(Debug, Clone, Ord, Eq, PartialEq, PartialOrd)] +pub struct HandleID(String); +impl HandleID { + fn new_random() -> Self { + Self(uuid::Uuid::now_v7().to_string()) + } +} + +impl Display for HandleID { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(&self.0) + } +} + +#[allow(async_fn_in_trait)] +pub trait ProcessHandler: Send + Sync + 'static { + fn call( + &self, + cancellation: CancellationToken, + ) -> impl Future> + Send; +} + +trait AsyncProcess: Send + Sync { + fn call_async( + &self, + cancellation: CancellationToken, + ) -> Pin> + Send + '_>>; +} + +impl AsyncProcess for E { + fn call_async( + &self, + cancellation: CancellationToken, + ) -> Pin> + Send + '_>> { + Box::pin(self.call(cancellation)) + } +} + +#[derive(Clone)] +pub struct SharedProcess { + process: Arc, +} + +pub trait IntoProcess { + fn into_process(self) -> SharedProcess; +} + +impl IntoProcess for E { + fn into_process(self) -> SharedProcess { + SharedProcess { + process: Arc::new(self), + } + } +} diff --git a/cuddle.yaml b/cuddle.yaml new file mode 100644 index 0000000..2fa5433 --- /dev/null +++ b/cuddle.yaml @@ -0,0 +1,17 @@ +# yaml-language-server: $schema=https://git.kjuulh.io/kjuulh/cuddle/raw/branch/main/schemas/base.json + +base: "git@git.kjuulh.io:kjuulh/cuddle-rust-lib-plan.git" + +vars: + service: "noprocess" + registry: kasperhermansen + +please: + project: + owner: kjuulh + repository: "noprocess" + branch: main + settings: + api_url: "https://git.kjuulh.io" + actions: + rust: