initial: per-user process supervisor (rss)
- start/stop/restart/status/logs/enable/disable/list/daemon - PID-file based state, no daemon, no root - config.toml with placeholders, restart policies, env vars - log tailing with -n/-f and stderr colorization - supervisor per service with SIGTERM handling
This commit is contained in:
commit
653b8d1612
12 changed files with 1600 additions and 0 deletions
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
/target
|
||||||
|
config.toml
|
||||||
|
*.pid
|
||||||
|
*.log
|
||||||
601
Cargo.lock
generated
Normal file
601
Cargo.lock
generated
Normal file
|
|
@ -0,0 +1,601 @@
|
||||||
|
# This file is automatically @generated by Cargo.
|
||||||
|
# It is not intended for manual editing.
|
||||||
|
version = 4
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "android_system_properties"
|
||||||
|
version = "0.1.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "anstream"
|
||||||
|
version = "1.0.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d"
|
||||||
|
dependencies = [
|
||||||
|
"anstyle",
|
||||||
|
"anstyle-parse",
|
||||||
|
"anstyle-query",
|
||||||
|
"anstyle-wincon",
|
||||||
|
"colorchoice",
|
||||||
|
"is_terminal_polyfill",
|
||||||
|
"utf8parse",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "anstyle"
|
||||||
|
version = "1.0.14"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "anstyle-parse"
|
||||||
|
version = "1.0.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e"
|
||||||
|
dependencies = [
|
||||||
|
"utf8parse",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "anstyle-query"
|
||||||
|
version = "1.1.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"
|
||||||
|
dependencies = [
|
||||||
|
"windows-sys",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "anstyle-wincon"
|
||||||
|
version = "3.0.11"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"
|
||||||
|
dependencies = [
|
||||||
|
"anstyle",
|
||||||
|
"once_cell_polyfill",
|
||||||
|
"windows-sys",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "autocfg"
|
||||||
|
version = "1.5.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bitflags"
|
||||||
|
version = "2.13.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bumpalo"
|
||||||
|
version = "3.20.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cc"
|
||||||
|
version = "1.2.65"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e228eec9be7c17ccb640b59b36a5cd805ea2a564a4c5e162c2f659fea30d3b96"
|
||||||
|
dependencies = [
|
||||||
|
"find-msvc-tools",
|
||||||
|
"shlex",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cfg-if"
|
||||||
|
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 = "chrono"
|
||||||
|
version = "0.4.45"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327"
|
||||||
|
dependencies = [
|
||||||
|
"iana-time-zone",
|
||||||
|
"js-sys",
|
||||||
|
"num-traits",
|
||||||
|
"wasm-bindgen",
|
||||||
|
"windows-link",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "clap"
|
||||||
|
version = "4.6.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51"
|
||||||
|
dependencies = [
|
||||||
|
"clap_builder",
|
||||||
|
"clap_derive",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "clap_builder"
|
||||||
|
version = "4.6.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f"
|
||||||
|
dependencies = [
|
||||||
|
"anstream",
|
||||||
|
"anstyle",
|
||||||
|
"clap_lex",
|
||||||
|
"strsim",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "clap_derive"
|
||||||
|
version = "4.6.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9"
|
||||||
|
dependencies = [
|
||||||
|
"heck",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "clap_lex"
|
||||||
|
version = "1.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "colorchoice"
|
||||||
|
version = "1.0.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "core-foundation-sys"
|
||||||
|
version = "0.8.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "equivalent"
|
||||||
|
version = "1.0.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "find-msvc-tools"
|
||||||
|
version = "0.1.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "futures-core"
|
||||||
|
version = "0.3.32"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "futures-task"
|
||||||
|
version = "0.3.32"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "futures-util"
|
||||||
|
version = "0.3.32"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6"
|
||||||
|
dependencies = [
|
||||||
|
"futures-core",
|
||||||
|
"futures-task",
|
||||||
|
"pin-project-lite",
|
||||||
|
"slab",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hashbrown"
|
||||||
|
version = "0.17.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "heck"
|
||||||
|
version = "0.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "iana-time-zone"
|
||||||
|
version = "0.1.65"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470"
|
||||||
|
dependencies = [
|
||||||
|
"android_system_properties",
|
||||||
|
"core-foundation-sys",
|
||||||
|
"iana-time-zone-haiku",
|
||||||
|
"js-sys",
|
||||||
|
"log",
|
||||||
|
"wasm-bindgen",
|
||||||
|
"windows-core",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "iana-time-zone-haiku"
|
||||||
|
version = "0.1.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
|
||||||
|
dependencies = [
|
||||||
|
"cc",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "indexmap"
|
||||||
|
version = "2.14.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9"
|
||||||
|
dependencies = [
|
||||||
|
"equivalent",
|
||||||
|
"hashbrown",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "is_terminal_polyfill"
|
||||||
|
version = "1.70.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "js-sys"
|
||||||
|
version = "0.3.103"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"futures-util",
|
||||||
|
"wasm-bindgen",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "libc"
|
||||||
|
version = "0.2.186"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "log"
|
||||||
|
version = "0.4.33"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "memchr"
|
||||||
|
version = "2.8.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "nix"
|
||||||
|
version = "0.29.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags",
|
||||||
|
"cfg-if",
|
||||||
|
"cfg_aliases",
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "num-traits"
|
||||||
|
version = "0.2.19"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
|
||||||
|
dependencies = [
|
||||||
|
"autocfg",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "once_cell"
|
||||||
|
version = "1.21.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "once_cell_polyfill"
|
||||||
|
version = "1.70.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pin-project-lite"
|
||||||
|
version = "0.2.17"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "proc-macro2"
|
||||||
|
version = "1.0.106"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
|
||||||
|
dependencies = [
|
||||||
|
"unicode-ident",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "quote"
|
||||||
|
version = "1.0.46"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rss"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"chrono",
|
||||||
|
"clap",
|
||||||
|
"nix",
|
||||||
|
"serde",
|
||||||
|
"toml",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rustversion"
|
||||||
|
version = "1.0.22"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde"
|
||||||
|
version = "1.0.228"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
|
||||||
|
dependencies = [
|
||||||
|
"serde_core",
|
||||||
|
"serde_derive",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_core"
|
||||||
|
version = "1.0.228"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
|
||||||
|
dependencies = [
|
||||||
|
"serde_derive",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_derive"
|
||||||
|
version = "1.0.228"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_spanned"
|
||||||
|
version = "0.6.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3"
|
||||||
|
dependencies = [
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "shlex"
|
||||||
|
version = "2.0.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "slab"
|
||||||
|
version = "0.4.12"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "strsim"
|
||||||
|
version = "0.11.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "syn"
|
||||||
|
version = "2.0.118"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"unicode-ident",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "toml"
|
||||||
|
version = "0.8.23"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362"
|
||||||
|
dependencies = [
|
||||||
|
"serde",
|
||||||
|
"serde_spanned",
|
||||||
|
"toml_datetime",
|
||||||
|
"toml_edit",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "toml_datetime"
|
||||||
|
version = "0.6.11"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c"
|
||||||
|
dependencies = [
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "toml_edit"
|
||||||
|
version = "0.22.27"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a"
|
||||||
|
dependencies = [
|
||||||
|
"indexmap",
|
||||||
|
"serde",
|
||||||
|
"serde_spanned",
|
||||||
|
"toml_datetime",
|
||||||
|
"toml_write",
|
||||||
|
"winnow",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "toml_write"
|
||||||
|
version = "0.1.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unicode-ident"
|
||||||
|
version = "1.0.24"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "utf8parse"
|
||||||
|
version = "0.2.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasm-bindgen"
|
||||||
|
version = "0.2.126"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4b067c0c11094aef6b7a801c1e34a26affafdf3d051dba08456b868789aaf9a4"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"once_cell",
|
||||||
|
"rustversion",
|
||||||
|
"wasm-bindgen-macro",
|
||||||
|
"wasm-bindgen-shared",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasm-bindgen-macro"
|
||||||
|
version = "0.2.126"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "167ce5e579f6bcf889c4f7175a8a5a585de84e8ff93976ce393efa5f2837aab1"
|
||||||
|
dependencies = [
|
||||||
|
"quote",
|
||||||
|
"wasm-bindgen-macro-support",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasm-bindgen-macro-support"
|
||||||
|
version = "0.2.126"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f3997c7839262f4ef12cf90b818d6340c18e80f263f1a94bf157d0ec4420380e"
|
||||||
|
dependencies = [
|
||||||
|
"bumpalo",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
"wasm-bindgen-shared",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasm-bindgen-shared"
|
||||||
|
version = "0.2.126"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "dc1b4cb0cc549fcf58d7dfc081778139b3d283a081644e833e84682ad71cea24"
|
||||||
|
dependencies = [
|
||||||
|
"unicode-ident",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-core"
|
||||||
|
version = "0.62.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb"
|
||||||
|
dependencies = [
|
||||||
|
"windows-implement",
|
||||||
|
"windows-interface",
|
||||||
|
"windows-link",
|
||||||
|
"windows-result",
|
||||||
|
"windows-strings",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-implement"
|
||||||
|
version = "0.60.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-interface"
|
||||||
|
version = "0.59.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-link"
|
||||||
|
version = "0.2.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-result"
|
||||||
|
version = "0.4.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5"
|
||||||
|
dependencies = [
|
||||||
|
"windows-link",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-strings"
|
||||||
|
version = "0.5.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091"
|
||||||
|
dependencies = [
|
||||||
|
"windows-link",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[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 = "winnow"
|
||||||
|
version = "0.7.15"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945"
|
||||||
|
dependencies = [
|
||||||
|
"memchr",
|
||||||
|
]
|
||||||
11
Cargo.toml
Normal file
11
Cargo.toml
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
[package]
|
||||||
|
name = "rss"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
clap = { version = "4", features = ["derive"] }
|
||||||
|
serde = { version = "1", features = ["derive"] }
|
||||||
|
toml = "0.8"
|
||||||
|
chrono = "0.4"
|
||||||
|
nix = { version = "0.29", default-features = false, features = ["signal", "fs"] }
|
||||||
21
LICENSE
Normal file
21
LICENSE
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2026 niko
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
57
README.md
Normal file
57
README.md
Normal file
|
|
@ -0,0 +1,57 @@
|
||||||
|
# rss — RuSt Supervisor
|
||||||
|
|
||||||
|
Lightweight, per-user process supervisor. No daemon, no root, no virtualization.
|
||||||
|
|
||||||
|
## Philosophy
|
||||||
|
|
||||||
|
- **KISS** — one binary, one job. Each service gets its own supervisor process.
|
||||||
|
- **UNIX way** — PID files for state, no IPC, no sockets.
|
||||||
|
- **User-space** — everything under `~/.config/rss/` and `~/.local/share/rss/`.
|
||||||
|
- **No root** — runs entirely as your user.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```
|
||||||
|
rss start <name> Start a service
|
||||||
|
rss stop <name> Stop a service
|
||||||
|
rss restart <name> Restart a service
|
||||||
|
rss status [name] Show service status
|
||||||
|
rss logs <name> [-n N] [-f] View logs (last N, follow)
|
||||||
|
rss enable <name> [--now] Enable auto-start
|
||||||
|
rss disable <name> Disable auto-start
|
||||||
|
rss list List all services
|
||||||
|
rss daemon Start all enabled services
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
Place `config.toml` in `~/.config/rss/config.toml`.
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[service.NAME]
|
||||||
|
workdir = "/path/to/project"
|
||||||
|
command = "your command here"
|
||||||
|
stop_command = "optional stop command"
|
||||||
|
timeout_start = 15
|
||||||
|
timeout_stop = 10
|
||||||
|
restart = "on-failure" # no, always, on-failure
|
||||||
|
restart_delay = 3
|
||||||
|
restart_max = 5 # 0 = unlimited
|
||||||
|
oneshot = false
|
||||||
|
stop_signal = "TERM"
|
||||||
|
env = { KEY = "value" }
|
||||||
|
```
|
||||||
|
|
||||||
|
Placeholders in `command` / `stop_command`: `{name}`, `{workdir}`, `{pid}`, `{log_file}`, plus any env var.
|
||||||
|
|
||||||
|
See `config.example.toml` for a full example.
|
||||||
|
|
||||||
|
## Install
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cargo install --path .
|
||||||
|
```
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT
|
||||||
30
config.example.toml
Normal file
30
config.example.toml
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
[service.web]
|
||||||
|
workdir = "/home/user/projects/web"
|
||||||
|
command = "python3 -m http.server {port}"
|
||||||
|
stop_command = "curl -s -X POST http://localhost:{port}/shutdown"
|
||||||
|
timeout_start = 15
|
||||||
|
timeout_stop = 10
|
||||||
|
restart = "on-failure"
|
||||||
|
restart_delay = 3
|
||||||
|
restart_max = 5
|
||||||
|
oneshot = false
|
||||||
|
stop_signal = "TERM"
|
||||||
|
env = { port = "8080", HOST = "0.0.0.0" }
|
||||||
|
|
||||||
|
[service.backup]
|
||||||
|
workdir = "/home/user"
|
||||||
|
command = "tar czf /tmp/backup.tar.gz ."
|
||||||
|
oneshot = true
|
||||||
|
restart = "no"
|
||||||
|
|
||||||
|
[service.app]
|
||||||
|
workdir = "/home/user/app"
|
||||||
|
command = "node server.js --port {PORT}"
|
||||||
|
stop_command = "kill -INT {pid}"
|
||||||
|
restart = "always"
|
||||||
|
restart_delay = 2
|
||||||
|
restart_max = 10
|
||||||
|
timeout_start = 30
|
||||||
|
timeout_stop = 15
|
||||||
|
stop_signal = "INT"
|
||||||
|
env = { PORT = "3000", NODE_ENV = "production" }
|
||||||
58
src/cli.rs
Normal file
58
src/cli.rs
Normal file
|
|
@ -0,0 +1,58 @@
|
||||||
|
use clap::Parser;
|
||||||
|
|
||||||
|
#[derive(Parser)]
|
||||||
|
#[command(name = "rss", version = "0.1.0", about = "Lightweight process supervisor")]
|
||||||
|
pub struct Cli {
|
||||||
|
#[arg(short = 'C', long, global = true, help = "Path to config file")]
|
||||||
|
pub config: Option<String>,
|
||||||
|
|
||||||
|
#[command(subcommand)]
|
||||||
|
pub command: Commands,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(clap::Subcommand)]
|
||||||
|
pub enum Commands {
|
||||||
|
/// Start a service
|
||||||
|
Start {
|
||||||
|
name: String,
|
||||||
|
},
|
||||||
|
/// Stop a service
|
||||||
|
Stop {
|
||||||
|
name: String,
|
||||||
|
},
|
||||||
|
/// Restart a service
|
||||||
|
Restart {
|
||||||
|
name: String,
|
||||||
|
},
|
||||||
|
/// Show service status
|
||||||
|
Status {
|
||||||
|
name: Option<String>,
|
||||||
|
},
|
||||||
|
/// Show service logs
|
||||||
|
Logs {
|
||||||
|
name: String,
|
||||||
|
#[arg(short = 'n', value_name = "N", help = "Show last N lines")]
|
||||||
|
lines: Option<usize>,
|
||||||
|
#[arg(short = 'f', help = "Follow new log entries")]
|
||||||
|
follow: bool,
|
||||||
|
},
|
||||||
|
/// Enable auto-start for a service
|
||||||
|
Enable {
|
||||||
|
name: String,
|
||||||
|
#[arg(long, help = "Start the service immediately")]
|
||||||
|
now: bool,
|
||||||
|
},
|
||||||
|
/// Disable auto-start for a service
|
||||||
|
Disable {
|
||||||
|
name: String,
|
||||||
|
},
|
||||||
|
/// List all configured services
|
||||||
|
List,
|
||||||
|
/// Start all enabled services
|
||||||
|
Daemon,
|
||||||
|
/// Internal: run supervisor for a service
|
||||||
|
#[command(hide = true)]
|
||||||
|
Supervise {
|
||||||
|
name: String,
|
||||||
|
},
|
||||||
|
}
|
||||||
99
src/config.rs
Normal file
99
src/config.rs
Normal file
|
|
@ -0,0 +1,99 @@
|
||||||
|
use serde::Deserialize;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
pub struct Config {
|
||||||
|
pub service: HashMap<String, ServiceConfig>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, Clone)]
|
||||||
|
pub struct ServiceConfig {
|
||||||
|
pub workdir: String,
|
||||||
|
pub command: String,
|
||||||
|
#[serde(default)]
|
||||||
|
pub stop_command: Option<String>,
|
||||||
|
#[serde(default = "default_timeout_start")]
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub timeout_start: u64,
|
||||||
|
#[serde(default = "default_timeout_stop")]
|
||||||
|
pub timeout_stop: u64,
|
||||||
|
#[serde(default = "default_restart")]
|
||||||
|
pub restart: RestartPolicy,
|
||||||
|
#[serde(default = "default_restart_delay")]
|
||||||
|
pub restart_delay: u64,
|
||||||
|
#[serde(default = "default_restart_max")]
|
||||||
|
pub restart_max: u32,
|
||||||
|
#[serde(default)]
|
||||||
|
pub oneshot: bool,
|
||||||
|
#[serde(default = "default_stop_signal")]
|
||||||
|
pub stop_signal: String,
|
||||||
|
#[serde(default)]
|
||||||
|
pub env: HashMap<String, String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn default_timeout_start() -> u64 {
|
||||||
|
15
|
||||||
|
}
|
||||||
|
fn default_timeout_stop() -> u64 {
|
||||||
|
10
|
||||||
|
}
|
||||||
|
fn default_restart() -> RestartPolicy {
|
||||||
|
RestartPolicy::OnFailure
|
||||||
|
}
|
||||||
|
fn default_restart_delay() -> u64 {
|
||||||
|
3
|
||||||
|
}
|
||||||
|
fn default_restart_max() -> u32 {
|
||||||
|
5
|
||||||
|
}
|
||||||
|
fn default_stop_signal() -> String {
|
||||||
|
"TERM".to_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, Clone, PartialEq)]
|
||||||
|
#[serde(rename_all = "kebab-case")]
|
||||||
|
pub enum RestartPolicy {
|
||||||
|
No,
|
||||||
|
Always,
|
||||||
|
OnFailure,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Config {
|
||||||
|
pub fn load(path: &str) -> Result<Self, Box<dyn std::error::Error>> {
|
||||||
|
let content = std::fs::read_to_string(path)?;
|
||||||
|
let config: Config = toml::from_str(&content)?;
|
||||||
|
if config.service.is_empty() {
|
||||||
|
return Err("no services defined in config".into());
|
||||||
|
}
|
||||||
|
Ok(config)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ServiceConfig {
|
||||||
|
pub fn resolve_command(&self, name: &str, log_file: &str) -> String {
|
||||||
|
let mut cmd = self
|
||||||
|
.command
|
||||||
|
.replace("{name}", name)
|
||||||
|
.replace("{workdir}", &self.workdir)
|
||||||
|
.replace("{log_file}", log_file);
|
||||||
|
for (k, v) in &self.env {
|
||||||
|
cmd = cmd.replace(&format!("{{{}}}", k), v);
|
||||||
|
}
|
||||||
|
cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn resolve_stop_command(&self, name: &str, pid: u32, log_file: &str) -> Option<String> {
|
||||||
|
let pid_str = pid.to_string();
|
||||||
|
self.stop_command.as_ref().map(|s| {
|
||||||
|
let mut s = s
|
||||||
|
.replace("{name}", name)
|
||||||
|
.replace("{workdir}", &self.workdir)
|
||||||
|
.replace("{pid}", &pid_str)
|
||||||
|
.replace("{log_file}", log_file);
|
||||||
|
for (k, v) in &self.env {
|
||||||
|
s = s.replace(&format!("{{{}}}", k), v);
|
||||||
|
}
|
||||||
|
s
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
120
src/log.rs
Normal file
120
src/log.rs
Normal file
|
|
@ -0,0 +1,120 @@
|
||||||
|
use std::fs::File;
|
||||||
|
use std::io::{BufRead, BufReader, Read, Seek, SeekFrom, Write};
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
const RED: &str = "\x1b[31m";
|
||||||
|
const RESET: &str = "\x1b[0m";
|
||||||
|
|
||||||
|
pub fn show_logs(path: &str, lines: Option<usize>, follow: bool) -> std::io::Result<()> {
|
||||||
|
let color = use_color();
|
||||||
|
|
||||||
|
if let Some(n) = lines {
|
||||||
|
print_last_n(path, n, color)?;
|
||||||
|
} else {
|
||||||
|
let content = std::fs::read_to_string(path)?;
|
||||||
|
for line in content.lines() {
|
||||||
|
println!("{}", colorize(line, color));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if follow {
|
||||||
|
follow_logs(path, lines.unwrap_or(0), color)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn print_last_n(path: &str, n: usize, color: bool) -> std::io::Result<()> {
|
||||||
|
let mut file = File::open(path)?;
|
||||||
|
let file_size = file.seek(SeekFrom::End(0))?;
|
||||||
|
if file_size == 0 {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut buf = Vec::new();
|
||||||
|
let mut pos = file_size;
|
||||||
|
let block = 4096.min(file_size as usize);
|
||||||
|
let mut newline_count = 0;
|
||||||
|
|
||||||
|
while pos > 0 {
|
||||||
|
let read_size = block.min(pos as usize);
|
||||||
|
pos -= read_size as u64;
|
||||||
|
file.seek(SeekFrom::Start(pos))?;
|
||||||
|
let mut chunk = vec![0u8; read_size];
|
||||||
|
file.read_exact(&mut chunk)?;
|
||||||
|
buf.splice(0..0, chunk);
|
||||||
|
|
||||||
|
newline_count = buf.iter().filter(|&&b| b == b'\n').count();
|
||||||
|
if newline_count > n {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let start = if newline_count > n {
|
||||||
|
let mut skip = newline_count - n;
|
||||||
|
let mut idx = 0;
|
||||||
|
for (i, &b) in buf.iter().enumerate() {
|
||||||
|
if b == b'\n' {
|
||||||
|
skip -= 1;
|
||||||
|
if skip == 0 {
|
||||||
|
idx = i + 1;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
idx
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Ok(s) = std::str::from_utf8(&buf[start..]) {
|
||||||
|
for line in s.lines() {
|
||||||
|
println!("{}", colorize(line, color));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn follow_logs(path: &str, _printed: usize, color: bool) -> std::io::Result<()> {
|
||||||
|
let mut file = File::open(path)?;
|
||||||
|
file.seek(SeekFrom::End(0))?;
|
||||||
|
let mut reader = BufReader::new(file);
|
||||||
|
|
||||||
|
loop {
|
||||||
|
let mut line = String::new();
|
||||||
|
match reader.read_line(&mut line) {
|
||||||
|
Ok(0) => {
|
||||||
|
std::thread::sleep(Duration::from_millis(100));
|
||||||
|
}
|
||||||
|
Ok(_) => {
|
||||||
|
let trimmed = line.trim_end_matches('\n').trim_end_matches('\r');
|
||||||
|
if !trimmed.is_empty() {
|
||||||
|
println!("{}", colorize(trimmed, color));
|
||||||
|
std::io::stdout().flush().ok();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(_) => break,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn colorize(line: &str, color: bool) -> String {
|
||||||
|
if !color {
|
||||||
|
return line.to_string();
|
||||||
|
}
|
||||||
|
if line.contains("E: ") || line.starts_with("E:") {
|
||||||
|
format!("{}{}{}", RED, line, RESET)
|
||||||
|
} else {
|
||||||
|
line.to_string()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn use_color() -> bool {
|
||||||
|
if std::env::var("NO_COLOR").is_ok() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
nix::unistd::isatty(1).unwrap_or(false)
|
||||||
|
}
|
||||||
314
src/main.rs
Normal file
314
src/main.rs
Normal file
|
|
@ -0,0 +1,314 @@
|
||||||
|
mod cli;
|
||||||
|
mod config;
|
||||||
|
mod log;
|
||||||
|
mod state;
|
||||||
|
mod supervisor;
|
||||||
|
|
||||||
|
use std::process::{Command, Stdio};
|
||||||
|
|
||||||
|
use clap::Parser;
|
||||||
|
use cli::{Cli, Commands};
|
||||||
|
use config::Config;
|
||||||
|
use state::State;
|
||||||
|
use supervisor::{parse_signal, process_running, wait_for_exit};
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let cli = Cli::parse();
|
||||||
|
|
||||||
|
// Internal supervise subcommand — must run before config loading?
|
||||||
|
if let Commands::Supervise { ref name } = cli.command {
|
||||||
|
let config_path = resolve_config_path(cli.config.as_deref());
|
||||||
|
let config = Config::load(&config_path).unwrap_or_else(|e| {
|
||||||
|
eprintln!("error: config: {}", e);
|
||||||
|
std::process::exit(1);
|
||||||
|
});
|
||||||
|
let cfg = match config.service.get(name) {
|
||||||
|
Some(c) => c,
|
||||||
|
None => {
|
||||||
|
eprintln!("error: service '{}' not found in config", name);
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
supervisor::run_supervisor(name, cfg);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let config_path = resolve_config_path(cli.config.as_deref());
|
||||||
|
let config = Config::load(&config_path).unwrap_or_else(|e| {
|
||||||
|
eprintln!("error: config: {}", e);
|
||||||
|
std::process::exit(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
let state = State::new();
|
||||||
|
|
||||||
|
match cli.command {
|
||||||
|
Commands::Start { name } => cmd_start(&name, &config, &config_path),
|
||||||
|
Commands::Stop { name } => cmd_stop(&name, &config, &state),
|
||||||
|
Commands::Restart { name } => {
|
||||||
|
cmd_stop(&name, &config, &state);
|
||||||
|
cmd_start(&name, &config, &config_path);
|
||||||
|
}
|
||||||
|
Commands::Status { name } => cmd_status(name.as_deref(), &config, &state),
|
||||||
|
Commands::Logs { name, lines, follow } => cmd_logs(&name, lines, follow),
|
||||||
|
Commands::Enable { name, now } => cmd_enable(&name, now, &config, &state, &config_path),
|
||||||
|
Commands::Disable { name } => cmd_disable(&name, &state),
|
||||||
|
Commands::List => cmd_list(&config, &state),
|
||||||
|
Commands::Daemon => cmd_daemon(&config, &state, &config_path),
|
||||||
|
Commands::Supervise { .. } => unreachable!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn resolve_config_path(custom: Option<&str>) -> String {
|
||||||
|
if let Some(path) = custom {
|
||||||
|
return path.to_string();
|
||||||
|
}
|
||||||
|
if let Ok(path) = std::env::var("RSS_CONFIG") {
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
let home = std::env::var("HOME").expect("HOME not set");
|
||||||
|
format!("{}/.config/rss/config.toml", home)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cmd_start(name: &str, config: &Config, config_path: &str) {
|
||||||
|
if !config.service.contains_key(name) {
|
||||||
|
eprintln!("error: service '{}' not found in config", name);
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
let self_exe = std::env::current_exe().unwrap_or_else(|_| {
|
||||||
|
eprintln!("error: cannot determine binary path");
|
||||||
|
std::process::exit(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
let child = Command::new(&self_exe)
|
||||||
|
.arg("supervise")
|
||||||
|
.arg(name)
|
||||||
|
.env("RSS_CONFIG", config_path)
|
||||||
|
.stdout(Stdio::null())
|
||||||
|
.stderr(Stdio::null())
|
||||||
|
.stdin(Stdio::null())
|
||||||
|
.spawn()
|
||||||
|
.unwrap_or_else(|e| {
|
||||||
|
eprintln!("error: failed to start supervisor: {}", e);
|
||||||
|
std::process::exit(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
println!("{}", child.id());
|
||||||
|
// Detach — forget the child so it becomes orphaned and adopted by init
|
||||||
|
std::mem::forget(child);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cmd_stop(name: &str, config: &Config, state: &State) {
|
||||||
|
let cfg = match config.service.get(name) {
|
||||||
|
Some(c) => c,
|
||||||
|
None => {
|
||||||
|
eprintln!("error: service '{}' not found", name);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let svc_pid_path = state.service_pid_path(name);
|
||||||
|
let sup_pid_path = state.supervise_pid_path(name);
|
||||||
|
let log_path = state.log_path(name);
|
||||||
|
let log_str = log_path.to_str().unwrap_or("");
|
||||||
|
|
||||||
|
// Tell supervisor to stop (sets TERMINATE flag) first
|
||||||
|
if let Some(pid) = State::read_pid(&sup_pid_path) {
|
||||||
|
if process_running(pid) {
|
||||||
|
let _ = nix::sys::signal::kill(
|
||||||
|
nix::unistd::Pid::from_raw(pid as i32),
|
||||||
|
nix::sys::signal::Signal::SIGTERM,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop child process
|
||||||
|
if let Some(pid) = State::read_pid(&svc_pid_path) {
|
||||||
|
if process_running(pid) {
|
||||||
|
// Run stop_command if configured
|
||||||
|
if let Some(stop_cmd) = cfg.resolve_stop_command(name, pid, log_str) {
|
||||||
|
let _ = Command::new("sh").arg("-c").arg(&stop_cmd).current_dir(&cfg.workdir).output();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send configured signal
|
||||||
|
let sig = parse_signal(&cfg.stop_signal);
|
||||||
|
let _ = nix::sys::signal::kill(nix::unistd::Pid::from_raw(pid as i32), sig);
|
||||||
|
|
||||||
|
// Wait for graceful exit
|
||||||
|
wait_for_exit(pid, cfg.timeout_stop);
|
||||||
|
|
||||||
|
// Force kill if still alive
|
||||||
|
if process_running(pid) {
|
||||||
|
let _ = nix::sys::signal::kill(
|
||||||
|
nix::unistd::Pid::from_raw(pid as i32),
|
||||||
|
nix::sys::signal::Signal::SIGKILL,
|
||||||
|
);
|
||||||
|
wait_for_exit(pid, 3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for supervisor to exit (it sees TERMINATE + child dead)
|
||||||
|
if let Some(pid) = State::read_pid(&sup_pid_path) {
|
||||||
|
if process_running(pid) {
|
||||||
|
wait_for_exit(pid, 3);
|
||||||
|
if process_running(pid) {
|
||||||
|
let _ = nix::sys::signal::kill(
|
||||||
|
nix::unistd::Pid::from_raw(pid as i32),
|
||||||
|
nix::sys::signal::Signal::SIGKILL,
|
||||||
|
);
|
||||||
|
wait_for_exit(pid, 3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cleanup PID files
|
||||||
|
State::remove_pid(&svc_pid_path);
|
||||||
|
State::remove_pid(&sup_pid_path);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cmd_status(name: Option<&str>, config: &Config, state: &State) {
|
||||||
|
if let Some(name) = name {
|
||||||
|
print_service_status(name, config, state);
|
||||||
|
} else {
|
||||||
|
let mut names: Vec<&String> = config.service.keys().collect();
|
||||||
|
names.sort();
|
||||||
|
for name in names {
|
||||||
|
print_service_status(name, config, state);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn print_service_status(name: &str, _config: &Config, state: &State) {
|
||||||
|
let svc_pid_path = state.service_pid_path(name);
|
||||||
|
let sup_pid_path = state.supervise_pid_path(name);
|
||||||
|
let enabled = state.is_enabled(name);
|
||||||
|
|
||||||
|
let svc_pid = State::read_pid(&svc_pid_path);
|
||||||
|
let sup_pid = State::read_pid(&sup_pid_path);
|
||||||
|
|
||||||
|
let svc_running = svc_pid.map_or(false, process_running);
|
||||||
|
let sup_running = sup_pid.map_or(false, process_running);
|
||||||
|
|
||||||
|
let status = if svc_running {
|
||||||
|
"running"
|
||||||
|
} else if sup_running {
|
||||||
|
"restarting"
|
||||||
|
} else {
|
||||||
|
"stopped"
|
||||||
|
};
|
||||||
|
|
||||||
|
let enabled_str = if enabled { "+" } else { "-" };
|
||||||
|
println!(
|
||||||
|
"{} {} pid={} supervisor={} enabled={}",
|
||||||
|
status,
|
||||||
|
name,
|
||||||
|
svc_pid.map_or(0, |p| p),
|
||||||
|
sup_pid.map_or(0, |p| p),
|
||||||
|
enabled_str,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cmd_logs(name: &str, mut lines: Option<usize>, follow: bool) {
|
||||||
|
if lines.is_none() && follow {
|
||||||
|
lines = Some(10);
|
||||||
|
}
|
||||||
|
let state = State::new();
|
||||||
|
let log_path = state.log_path(name);
|
||||||
|
let log_str = log_path.to_str().unwrap_or("");
|
||||||
|
|
||||||
|
if !log_path.exists() {
|
||||||
|
eprintln!("error: no logs for '{}'", name);
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Err(e) = log::show_logs(log_str, lines, follow) {
|
||||||
|
eprintln!("error: reading logs: {}", e);
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cmd_enable(name: &str, now: bool, config: &Config, state: &State, config_path: &str) {
|
||||||
|
if !config.service.contains_key(name) {
|
||||||
|
eprintln!("error: service '{}' not found in config", name);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Err(e) = state.enable(name) {
|
||||||
|
eprintln!("error: enabling '{}': {}", name, e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("enabled {}", name);
|
||||||
|
|
||||||
|
if now {
|
||||||
|
cmd_start(name, config, config_path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cmd_disable(name: &str, state: &State) {
|
||||||
|
state.disable(name);
|
||||||
|
println!("disabled {}", name);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cmd_list(config: &Config, state: &State) {
|
||||||
|
let mut names: Vec<&String> = config.service.keys().collect();
|
||||||
|
names.sort();
|
||||||
|
|
||||||
|
if names.is_empty() {
|
||||||
|
println!("no services configured");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("{:<12} {:<7} {:>6} {:>6} {}", "status", "name", "pid", "super", "enabled");
|
||||||
|
println!("{}", "-".repeat(50));
|
||||||
|
for name in names {
|
||||||
|
let svc_pid_path = state.service_pid_path(name);
|
||||||
|
let sup_pid_path = state.supervise_pid_path(name);
|
||||||
|
let enabled = state.is_enabled(name);
|
||||||
|
|
||||||
|
let svc_pid = State::read_pid(&svc_pid_path);
|
||||||
|
let sup_pid = State::read_pid(&sup_pid_path);
|
||||||
|
|
||||||
|
let svc_running = svc_pid.map_or(false, process_running);
|
||||||
|
let sup_running = sup_pid.map_or(false, process_running);
|
||||||
|
|
||||||
|
let status = if svc_running {
|
||||||
|
"running"
|
||||||
|
} else if sup_running {
|
||||||
|
"restart"
|
||||||
|
} else {
|
||||||
|
"stopped"
|
||||||
|
};
|
||||||
|
|
||||||
|
let enabled_str = if enabled { "yes" } else { "no" };
|
||||||
|
println!(
|
||||||
|
"{:<12} {:<7} {:>6} {:>6} {}",
|
||||||
|
status,
|
||||||
|
name,
|
||||||
|
svc_pid.map_or(0, |p| p),
|
||||||
|
sup_pid.map_or(0, |p| p),
|
||||||
|
enabled_str,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cmd_daemon(config: &Config, state: &State, config_path: &str) {
|
||||||
|
let enabled = state.list_enabled().unwrap_or_default();
|
||||||
|
if enabled.is_empty() {
|
||||||
|
println!("no enabled services");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for name in &enabled {
|
||||||
|
if !config.service.contains_key(name) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let svc_pid_path = state.service_pid_path(name);
|
||||||
|
let already = State::read_pid(&svc_pid_path).map_or(false, |p| process_running(p));
|
||||||
|
if already {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
println!("starting {}...", name);
|
||||||
|
cmd_start(name, config, config_path);
|
||||||
|
}
|
||||||
|
}
|
||||||
89
src/state.rs
Normal file
89
src/state.rs
Normal file
|
|
@ -0,0 +1,89 @@
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
pub struct State {
|
||||||
|
state_dir: PathBuf,
|
||||||
|
config_dir: PathBuf,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl State {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
let home = std::env::var("HOME").expect("HOME not set");
|
||||||
|
State {
|
||||||
|
state_dir: PathBuf::from(format!("{}/.local/share/rss", home)),
|
||||||
|
config_dir: PathBuf::from(format!("{}/.config/rss", home)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn supervise_pid_path(&self, name: &str) -> PathBuf {
|
||||||
|
self.state_dir.join("supervise").join(format!("{}.pid", name))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn service_pid_path(&self, name: &str) -> PathBuf {
|
||||||
|
self.state_dir.join("service").join(format!("{}.pid", name))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn log_path(&self, name: &str) -> PathBuf {
|
||||||
|
self.state_dir.join("logs").join(format!("{}.log", name))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn enabled_path(&self, name: &str) -> PathBuf {
|
||||||
|
self.config_dir.join("enabled").join(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn write_pid(path: &PathBuf) -> std::io::Result<()> {
|
||||||
|
Self::write_pid_with(path, std::process::id())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn write_pid_with(path: &PathBuf, pid: u32) -> std::io::Result<()> {
|
||||||
|
if let Some(parent) = path.parent() {
|
||||||
|
std::fs::create_dir_all(parent)?;
|
||||||
|
}
|
||||||
|
std::fs::write(path, format!("{}", pid))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn read_pid(path: &PathBuf) -> Option<u32> {
|
||||||
|
let content = std::fs::read_to_string(path).ok()?;
|
||||||
|
content.trim().parse().ok()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn remove_pid(path: &PathBuf) {
|
||||||
|
let _ = std::fs::remove_file(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn process_exists(pid: u32) -> bool {
|
||||||
|
std::path::Path::new(&format!("/proc/{}", pid)).exists()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_enabled(&self, name: &str) -> bool {
|
||||||
|
self.enabled_path(name).exists()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn enable(&self, name: &str) -> std::io::Result<()> {
|
||||||
|
let path = self.enabled_path(name);
|
||||||
|
if let Some(parent) = path.parent() {
|
||||||
|
std::fs::create_dir_all(parent)?;
|
||||||
|
}
|
||||||
|
std::fs::write(&path, "")?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn disable(&self, name: &str) {
|
||||||
|
let _ = std::fs::remove_file(self.enabled_path(name));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn list_enabled(&self) -> std::io::Result<Vec<String>> {
|
||||||
|
let dir = self.config_dir.join("enabled");
|
||||||
|
if !dir.exists() {
|
||||||
|
return Ok(Vec::new());
|
||||||
|
}
|
||||||
|
let mut names = Vec::new();
|
||||||
|
for entry in std::fs::read_dir(dir)? {
|
||||||
|
let entry = entry?;
|
||||||
|
if let Some(name) = entry.file_name().to_str() {
|
||||||
|
names.push(name.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
names.sort();
|
||||||
|
Ok(names)
|
||||||
|
}
|
||||||
|
}
|
||||||
196
src/supervisor.rs
Normal file
196
src/supervisor.rs
Normal file
|
|
@ -0,0 +1,196 @@
|
||||||
|
use std::io::{BufRead, Write};
|
||||||
|
use std::process::{Child, Command, Stdio};
|
||||||
|
use std::sync::atomic::{AtomicBool, Ordering};
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use chrono::Local;
|
||||||
|
use nix::sys::signal::{self, SigAction, SigHandler, Signal};
|
||||||
|
|
||||||
|
use crate::config::{RestartPolicy, ServiceConfig};
|
||||||
|
use crate::state::State;
|
||||||
|
|
||||||
|
pub static TERMINATE: AtomicBool = AtomicBool::new(false);
|
||||||
|
|
||||||
|
extern "C" fn sigterm_handler(_: i32) {
|
||||||
|
TERMINATE.store(true, Ordering::Relaxed);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn setup_signal_handler() {
|
||||||
|
let handler = SigHandler::Handler(sigterm_handler);
|
||||||
|
let action = SigAction::new(handler, nix::sys::signal::SaFlags::SA_RESTART, signal::SigSet::empty());
|
||||||
|
unsafe {
|
||||||
|
let _ = signal::sigaction(Signal::SIGTERM, &action);
|
||||||
|
let _ = signal::sigaction(Signal::SIGINT, &action);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn run_supervisor(name: &str, cfg: &ServiceConfig) {
|
||||||
|
setup_signal_handler();
|
||||||
|
|
||||||
|
let state = State::new();
|
||||||
|
let supervise_pid = state.supervise_pid_path(name);
|
||||||
|
let service_pid = state.service_pid_path(name);
|
||||||
|
let log_path = state.log_path(name);
|
||||||
|
|
||||||
|
State::write_pid(&supervise_pid).ok();
|
||||||
|
|
||||||
|
if let Some(parent) = log_path.parent() {
|
||||||
|
std::fs::create_dir_all(parent).ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut restarts: u32 = 0;
|
||||||
|
let max_restarts = if cfg.restart_max == 0 { u32::MAX } else { cfg.restart_max };
|
||||||
|
|
||||||
|
loop {
|
||||||
|
let cmd = cfg.resolve_command(name, log_path.to_str().unwrap_or(""));
|
||||||
|
let log_file = log_path.to_str().unwrap_or("").to_string();
|
||||||
|
|
||||||
|
let mut child = match spawn_child(name, cfg, &cmd, &log_file) {
|
||||||
|
Ok(c) => c,
|
||||||
|
Err(e) => {
|
||||||
|
append_log(&log_file, &format!("E: failed to spawn: {}", e));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let child_pid = child.id();
|
||||||
|
State::write_pid_with(&service_pid, child_pid).ok();
|
||||||
|
|
||||||
|
append_log(&log_file, &format!("O: started (pid {})", child_pid));
|
||||||
|
|
||||||
|
let status = child.wait();
|
||||||
|
|
||||||
|
match status {
|
||||||
|
Ok(status) => {
|
||||||
|
let code = status.code().map(|c| format!("code {}", c)).unwrap_or_else(|| "signal".to_string());
|
||||||
|
append_log(&log_file, &format!("O: exited with {}", code));
|
||||||
|
|
||||||
|
if TERMINATE.load(Ordering::Relaxed) {
|
||||||
|
append_log(&log_file, "O: supervisor received stop signal, exiting");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if cfg.oneshot {
|
||||||
|
append_log(&log_file, "O: oneshot service completed, exiting");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
let should_restart = match cfg.restart {
|
||||||
|
RestartPolicy::No => false,
|
||||||
|
RestartPolicy::Always => true,
|
||||||
|
RestartPolicy::OnFailure => !status.success(),
|
||||||
|
};
|
||||||
|
|
||||||
|
if should_restart && restarts < max_restarts {
|
||||||
|
restarts += 1;
|
||||||
|
let max_str = if cfg.restart_max == 0 {
|
||||||
|
"unlimited".to_string()
|
||||||
|
} else {
|
||||||
|
cfg.restart_max.to_string()
|
||||||
|
};
|
||||||
|
append_log(
|
||||||
|
&log_file,
|
||||||
|
&format!("O: restarting in {}s (attempt {}/{})", cfg.restart_delay, restarts, max_str),
|
||||||
|
);
|
||||||
|
std::thread::sleep(Duration::from_secs(cfg.restart_delay));
|
||||||
|
} else {
|
||||||
|
if cfg.restart != RestartPolicy::No {
|
||||||
|
append_log(&log_file, "O: max restarts reached, exiting");
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
append_log(&log_file, &format!("E: wait error: {}", e));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
State::remove_pid(&service_pid);
|
||||||
|
State::remove_pid(&supervise_pid);
|
||||||
|
|
||||||
|
// Give pipe-reader threads time to flush
|
||||||
|
std::thread::sleep(Duration::from_millis(200));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn spawn_child(
|
||||||
|
_name: &str,
|
||||||
|
cfg: &ServiceConfig,
|
||||||
|
cmd: &str,
|
||||||
|
log_file: &str,
|
||||||
|
) -> std::io::Result<Child> {
|
||||||
|
let mut child = Command::new("sh")
|
||||||
|
.arg("-c")
|
||||||
|
.arg(cmd)
|
||||||
|
.current_dir(&cfg.workdir)
|
||||||
|
.envs(&cfg.env)
|
||||||
|
.stdout(Stdio::piped())
|
||||||
|
.stderr(Stdio::piped())
|
||||||
|
.stdin(Stdio::null())
|
||||||
|
.spawn()?;
|
||||||
|
|
||||||
|
let stdout = child.stdout.take().unwrap();
|
||||||
|
let stderr = child.stderr.take().unwrap();
|
||||||
|
|
||||||
|
let log_out = log_file.to_string();
|
||||||
|
std::thread::spawn(move || {
|
||||||
|
let reader = std::io::BufReader::new(stdout);
|
||||||
|
for line in reader.lines() {
|
||||||
|
if let Ok(line) = line {
|
||||||
|
append_log(&log_out, &format!("O: {}", line));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let log_err = log_file.to_string();
|
||||||
|
std::thread::spawn(move || {
|
||||||
|
let reader = std::io::BufReader::new(stderr);
|
||||||
|
for line in reader.lines() {
|
||||||
|
if let Ok(line) = line {
|
||||||
|
append_log(&log_err, &format!("E: {}", line));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Ok(child)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn append_log(path: &str, msg: &str) {
|
||||||
|
if let Ok(mut file) = std::fs::OpenOptions::new()
|
||||||
|
.create(true)
|
||||||
|
.append(true)
|
||||||
|
.open(path)
|
||||||
|
{
|
||||||
|
let ts = Local::now().format("%Y-%m-%d %H:%M:%S");
|
||||||
|
let _ = writeln!(file, "[{}] {}", ts, msg);
|
||||||
|
let _ = file.flush();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parse_signal(name: &str) -> Signal {
|
||||||
|
match name.to_uppercase().as_str() {
|
||||||
|
"TERM" => Signal::SIGTERM,
|
||||||
|
"INT" => Signal::SIGINT,
|
||||||
|
"QUIT" => Signal::SIGQUIT,
|
||||||
|
"KILL" => Signal::SIGKILL,
|
||||||
|
"HUP" => Signal::SIGHUP,
|
||||||
|
"USR1" => Signal::SIGUSR1,
|
||||||
|
"USR2" => Signal::SIGUSR2,
|
||||||
|
_ => Signal::SIGTERM,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn process_running(pid: u32) -> bool {
|
||||||
|
State::process_exists(pid)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn wait_for_exit(pid: u32, timeout_secs: u64) {
|
||||||
|
let deadline = std::time::Instant::now() + Duration::from_secs(timeout_secs);
|
||||||
|
while std::time::Instant::now() < deadline {
|
||||||
|
if !process_running(pid) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
std::thread::sleep(Duration::from_millis(100));
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Reference in a new issue