├── .gitignore ├── CHANGES.md ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── Makefile ├── README.md ├── assets ├── vsv-add-service.jpg ├── vsv-arguments.jpg ├── vsv-down.jpg ├── vsv-filter.jpg ├── vsv-log-tree.jpg ├── vsv-log.jpg ├── vsv-restart.jpg ├── vsv-status.jpg └── vsv-tree.jpg ├── benchmarks ├── .gitignore └── make-test-dirs ├── man ├── vsv.8 └── vsv.md ├── rustfmt.toml ├── src ├── arguments.rs ├── commands │ ├── enable_disable.rs │ ├── external.rs │ ├── mod.rs │ └── status.rs ├── config.rs ├── die.rs ├── main.rs ├── runit.rs ├── service.rs └── utils.rs └── tests ├── basic.rs ├── common └── mod.rs └── integration.rs /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /CHANGES.md: -------------------------------------------------------------------------------- 1 | vsv Changes 2 | =========== 3 | 4 | Not Yet Released 5 | ---------------- 6 | 7 | (nothing yet) 8 | 9 | `v2.0.0` 10 | -------- 11 | 12 | - Rewritten to Rust from bash. Archived code is available here: https://github.com/bahamas10/bash-vsv/blob/master/CHANGES.md 13 | 14 | `v1.3.5` 15 | -------- 16 | 17 | - [#16](https://github.com/bahamas10/vsv/pull/16) loosen-up defensive service name validation logic to allow "@" chars 18 | 19 | `v1.3.4` 20 | -------- 21 | 22 | - [#8](https://github.com/bahamas10/vsv/issues/8) better detect terminals with color support 23 | 24 | `v1.3.3` 25 | -------- 26 | 27 | - [#4](https://github.com/bahamas10/vsv/pull/4) Truncate command output to not overflow 17 characters (PR by @zdykstra) 28 | - [#6](https://github.com/bahamas10/vsv/issues/6) race condition when checking if a service is disabled 29 | 30 | `v1.3.2` 31 | -------- 32 | 33 | - Add license file (MIT) 34 | 35 | `v1.3.1` 36 | -------- 37 | 38 | - Update manpage 39 | 40 | `v1.3.0` 41 | -------- 42 | 43 | - [#1](https://github.com/bahamas10/vsv/pull/1) fix a typo (PR by @pltrz) 44 | - [#2](https://github.com/bahamas10/vsv/pull/2) Add support to enable/disable service (issue by @illiliti) 45 | 46 | `v1.2.1` 47 | -------- 48 | 49 | - Add manpage 50 | 51 | `v1.2.0` 52 | -------- 53 | 54 | - Add `-l` to status to show log processes 55 | - Add `COMMAND` to output (first arg of `cmdline`) 56 | 57 | `v1.1.0` 58 | -------- 59 | 60 | - Add `-V` to print version number 61 | 62 | `v1.0.0` 63 | -------- 64 | 65 | - Initial Commit 66 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "anyhow" 7 | version = "1.0.58" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "bb07d2053ccdbe10e2af2995a2f116c1330396493dc1269f6a91d0ae82e19704" 10 | 11 | [[package]] 12 | name = "assert_cmd" 13 | version = "2.0.4" 14 | source = "registry+https://github.com/rust-lang/crates.io-index" 15 | checksum = "93ae1ddd39efd67689deb1979d80bad3bf7f2b09c6e6117c8d1f2443b5e2f83e" 16 | dependencies = [ 17 | "bstr", 18 | "doc-comment", 19 | "predicates", 20 | "predicates-core", 21 | "predicates-tree", 22 | "wait-timeout", 23 | ] 24 | 25 | [[package]] 26 | name = "atty" 27 | version = "0.2.14" 28 | source = "registry+https://github.com/rust-lang/crates.io-index" 29 | checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" 30 | dependencies = [ 31 | "hermit-abi", 32 | "libc", 33 | "winapi", 34 | ] 35 | 36 | [[package]] 37 | name = "autocfg" 38 | version = "1.1.0" 39 | source = "registry+https://github.com/rust-lang/crates.io-index" 40 | checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" 41 | 42 | [[package]] 43 | name = "bitflags" 44 | version = "1.3.2" 45 | source = "registry+https://github.com/rust-lang/crates.io-index" 46 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 47 | 48 | [[package]] 49 | name = "bstr" 50 | version = "0.2.17" 51 | source = "registry+https://github.com/rust-lang/crates.io-index" 52 | checksum = "ba3569f383e8f1598449f1a423e72e99569137b47740b1da11ef19af3d5c3223" 53 | dependencies = [ 54 | "lazy_static", 55 | "memchr", 56 | "regex-automata", 57 | ] 58 | 59 | [[package]] 60 | name = "cfg-if" 61 | version = "1.0.0" 62 | source = "registry+https://github.com/rust-lang/crates.io-index" 63 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 64 | 65 | [[package]] 66 | name = "clap" 67 | version = "3.2.7" 68 | source = "registry+https://github.com/rust-lang/crates.io-index" 69 | checksum = "5b7b16274bb247b45177db843202209b12191b631a14a9d06e41b3777d6ecf14" 70 | dependencies = [ 71 | "atty", 72 | "bitflags", 73 | "clap_derive", 74 | "clap_lex", 75 | "indexmap", 76 | "once_cell", 77 | "strsim", 78 | "termcolor", 79 | "textwrap", 80 | ] 81 | 82 | [[package]] 83 | name = "clap_derive" 84 | version = "3.2.7" 85 | source = "registry+https://github.com/rust-lang/crates.io-index" 86 | checksum = "759bf187376e1afa7b85b959e6a664a3e7a95203415dba952ad19139e798f902" 87 | dependencies = [ 88 | "heck", 89 | "proc-macro-error", 90 | "proc-macro2", 91 | "quote", 92 | "syn", 93 | ] 94 | 95 | [[package]] 96 | name = "clap_lex" 97 | version = "0.2.4" 98 | source = "registry+https://github.com/rust-lang/crates.io-index" 99 | checksum = "2850f2f5a82cbf437dd5af4d49848fbdfc27c157c3d010345776f952765261c5" 100 | dependencies = [ 101 | "os_str_bytes", 102 | ] 103 | 104 | [[package]] 105 | name = "crossbeam-channel" 106 | version = "0.5.5" 107 | source = "registry+https://github.com/rust-lang/crates.io-index" 108 | checksum = "4c02a4d71819009c192cf4872265391563fd6a84c81ff2c0f2a7026ca4c1d85c" 109 | dependencies = [ 110 | "cfg-if", 111 | "crossbeam-utils", 112 | ] 113 | 114 | [[package]] 115 | name = "crossbeam-deque" 116 | version = "0.8.1" 117 | source = "registry+https://github.com/rust-lang/crates.io-index" 118 | checksum = "6455c0ca19f0d2fbf751b908d5c55c1f5cbc65e03c4225427254b46890bdde1e" 119 | dependencies = [ 120 | "cfg-if", 121 | "crossbeam-epoch", 122 | "crossbeam-utils", 123 | ] 124 | 125 | [[package]] 126 | name = "crossbeam-epoch" 127 | version = "0.9.9" 128 | source = "registry+https://github.com/rust-lang/crates.io-index" 129 | checksum = "07db9d94cbd326813772c968ccd25999e5f8ae22f4f8d1b11effa37ef6ce281d" 130 | dependencies = [ 131 | "autocfg", 132 | "cfg-if", 133 | "crossbeam-utils", 134 | "memoffset", 135 | "once_cell", 136 | "scopeguard", 137 | ] 138 | 139 | [[package]] 140 | name = "crossbeam-utils" 141 | version = "0.8.10" 142 | source = "registry+https://github.com/rust-lang/crates.io-index" 143 | checksum = "7d82ee10ce34d7bc12c2122495e7593a9c41347ecdd64185af4ecf72cb1a7f83" 144 | dependencies = [ 145 | "cfg-if", 146 | "once_cell", 147 | ] 148 | 149 | [[package]] 150 | name = "difflib" 151 | version = "0.4.0" 152 | source = "registry+https://github.com/rust-lang/crates.io-index" 153 | checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8" 154 | 155 | [[package]] 156 | name = "dirs" 157 | version = "4.0.0" 158 | source = "registry+https://github.com/rust-lang/crates.io-index" 159 | checksum = "ca3aa72a6f96ea37bbc5aa912f6788242832f75369bdfdadcb0e38423f100059" 160 | dependencies = [ 161 | "dirs-sys", 162 | ] 163 | 164 | [[package]] 165 | name = "dirs-sys" 166 | version = "0.3.7" 167 | source = "registry+https://github.com/rust-lang/crates.io-index" 168 | checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6" 169 | dependencies = [ 170 | "libc", 171 | "redox_users", 172 | "winapi", 173 | ] 174 | 175 | [[package]] 176 | name = "doc-comment" 177 | version = "0.3.3" 178 | source = "registry+https://github.com/rust-lang/crates.io-index" 179 | checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" 180 | 181 | [[package]] 182 | name = "either" 183 | version = "1.6.1" 184 | source = "registry+https://github.com/rust-lang/crates.io-index" 185 | checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457" 186 | 187 | [[package]] 188 | name = "getrandom" 189 | version = "0.2.7" 190 | source = "registry+https://github.com/rust-lang/crates.io-index" 191 | checksum = "4eb1a864a501629691edf6c15a593b7a51eebaa1e8468e9ddc623de7c9b58ec6" 192 | dependencies = [ 193 | "cfg-if", 194 | "libc", 195 | "wasi", 196 | ] 197 | 198 | [[package]] 199 | name = "hashbrown" 200 | version = "0.12.1" 201 | source = "registry+https://github.com/rust-lang/crates.io-index" 202 | checksum = "db0d4cf898abf0081f964436dc980e96670a0f36863e4b83aaacdb65c9d7ccc3" 203 | 204 | [[package]] 205 | name = "heck" 206 | version = "0.4.0" 207 | source = "registry+https://github.com/rust-lang/crates.io-index" 208 | checksum = "2540771e65fc8cb83cd6e8a237f70c319bd5c29f78ed1084ba5d50eeac86f7f9" 209 | 210 | [[package]] 211 | name = "hermit-abi" 212 | version = "0.1.19" 213 | source = "registry+https://github.com/rust-lang/crates.io-index" 214 | checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" 215 | dependencies = [ 216 | "libc", 217 | ] 218 | 219 | [[package]] 220 | name = "indexmap" 221 | version = "1.9.1" 222 | source = "registry+https://github.com/rust-lang/crates.io-index" 223 | checksum = "10a35a97730320ffe8e2d410b5d3b69279b98d2c14bdb8b70ea89ecf7888d41e" 224 | dependencies = [ 225 | "autocfg", 226 | "hashbrown", 227 | ] 228 | 229 | [[package]] 230 | name = "itertools" 231 | version = "0.10.3" 232 | source = "registry+https://github.com/rust-lang/crates.io-index" 233 | checksum = "a9a9d19fa1e79b6215ff29b9d6880b706147f16e9b1dbb1e4e5947b5b02bc5e3" 234 | dependencies = [ 235 | "either", 236 | ] 237 | 238 | [[package]] 239 | name = "lazy_static" 240 | version = "1.4.0" 241 | source = "registry+https://github.com/rust-lang/crates.io-index" 242 | checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" 243 | 244 | [[package]] 245 | name = "libc" 246 | version = "0.2.126" 247 | source = "registry+https://github.com/rust-lang/crates.io-index" 248 | checksum = "349d5a591cd28b49e1d1037471617a32ddcda5731b99419008085f72d5a53836" 249 | 250 | [[package]] 251 | name = "memchr" 252 | version = "2.5.0" 253 | source = "registry+https://github.com/rust-lang/crates.io-index" 254 | checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" 255 | 256 | [[package]] 257 | name = "memoffset" 258 | version = "0.6.5" 259 | source = "registry+https://github.com/rust-lang/crates.io-index" 260 | checksum = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce" 261 | dependencies = [ 262 | "autocfg", 263 | ] 264 | 265 | [[package]] 266 | name = "num_cpus" 267 | version = "1.13.1" 268 | source = "registry+https://github.com/rust-lang/crates.io-index" 269 | checksum = "19e64526ebdee182341572e50e9ad03965aa510cd94427a4549448f285e957a1" 270 | dependencies = [ 271 | "hermit-abi", 272 | "libc", 273 | ] 274 | 275 | [[package]] 276 | name = "once_cell" 277 | version = "1.12.0" 278 | source = "registry+https://github.com/rust-lang/crates.io-index" 279 | checksum = "7709cef83f0c1f58f666e746a08b21e0085f7440fa6a29cc194d68aac97a4225" 280 | 281 | [[package]] 282 | name = "os_str_bytes" 283 | version = "6.1.0" 284 | source = "registry+https://github.com/rust-lang/crates.io-index" 285 | checksum = "21326818e99cfe6ce1e524c2a805c189a99b5ae555a35d19f9a284b427d86afa" 286 | 287 | [[package]] 288 | name = "predicates" 289 | version = "2.1.1" 290 | source = "registry+https://github.com/rust-lang/crates.io-index" 291 | checksum = "a5aab5be6e4732b473071984b3164dbbfb7a3674d30ea5ff44410b6bcd960c3c" 292 | dependencies = [ 293 | "difflib", 294 | "itertools", 295 | "predicates-core", 296 | ] 297 | 298 | [[package]] 299 | name = "predicates-core" 300 | version = "1.0.3" 301 | source = "registry+https://github.com/rust-lang/crates.io-index" 302 | checksum = "da1c2388b1513e1b605fcec39a95e0a9e8ef088f71443ef37099fa9ae6673fcb" 303 | 304 | [[package]] 305 | name = "predicates-tree" 306 | version = "1.0.5" 307 | source = "registry+https://github.com/rust-lang/crates.io-index" 308 | checksum = "4d86de6de25020a36c6d3643a86d9a6a9f552107c0559c60ea03551b5e16c032" 309 | dependencies = [ 310 | "predicates-core", 311 | "termtree", 312 | ] 313 | 314 | [[package]] 315 | name = "proc-macro-error" 316 | version = "1.0.4" 317 | source = "registry+https://github.com/rust-lang/crates.io-index" 318 | checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" 319 | dependencies = [ 320 | "proc-macro-error-attr", 321 | "proc-macro2", 322 | "quote", 323 | "syn", 324 | "version_check", 325 | ] 326 | 327 | [[package]] 328 | name = "proc-macro-error-attr" 329 | version = "1.0.4" 330 | source = "registry+https://github.com/rust-lang/crates.io-index" 331 | checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" 332 | dependencies = [ 333 | "proc-macro2", 334 | "quote", 335 | "version_check", 336 | ] 337 | 338 | [[package]] 339 | name = "proc-macro2" 340 | version = "1.0.40" 341 | source = "registry+https://github.com/rust-lang/crates.io-index" 342 | checksum = "dd96a1e8ed2596c337f8eae5f24924ec83f5ad5ab21ea8e455d3566c69fbcaf7" 343 | dependencies = [ 344 | "unicode-ident", 345 | ] 346 | 347 | [[package]] 348 | name = "quote" 349 | version = "1.0.20" 350 | source = "registry+https://github.com/rust-lang/crates.io-index" 351 | checksum = "3bcdf212e9776fbcb2d23ab029360416bb1706b1aea2d1a5ba002727cbcab804" 352 | dependencies = [ 353 | "proc-macro2", 354 | ] 355 | 356 | [[package]] 357 | name = "rayon" 358 | version = "1.5.3" 359 | source = "registry+https://github.com/rust-lang/crates.io-index" 360 | checksum = "bd99e5772ead8baa5215278c9b15bf92087709e9c1b2d1f97cdb5a183c933a7d" 361 | dependencies = [ 362 | "autocfg", 363 | "crossbeam-deque", 364 | "either", 365 | "rayon-core", 366 | ] 367 | 368 | [[package]] 369 | name = "rayon-core" 370 | version = "1.9.3" 371 | source = "registry+https://github.com/rust-lang/crates.io-index" 372 | checksum = "258bcdb5ac6dad48491bb2992db6b7cf74878b0384908af124823d118c99683f" 373 | dependencies = [ 374 | "crossbeam-channel", 375 | "crossbeam-deque", 376 | "crossbeam-utils", 377 | "num_cpus", 378 | ] 379 | 380 | [[package]] 381 | name = "redox_syscall" 382 | version = "0.2.13" 383 | source = "registry+https://github.com/rust-lang/crates.io-index" 384 | checksum = "62f25bc4c7e55e0b0b7a1d43fb893f4fa1361d0abe38b9ce4f323c2adfe6ef42" 385 | dependencies = [ 386 | "bitflags", 387 | ] 388 | 389 | [[package]] 390 | name = "redox_users" 391 | version = "0.4.3" 392 | source = "registry+https://github.com/rust-lang/crates.io-index" 393 | checksum = "b033d837a7cf162d7993aded9304e30a83213c648b6e389db233191f891e5c2b" 394 | dependencies = [ 395 | "getrandom", 396 | "redox_syscall", 397 | "thiserror", 398 | ] 399 | 400 | [[package]] 401 | name = "regex-automata" 402 | version = "0.1.10" 403 | source = "registry+https://github.com/rust-lang/crates.io-index" 404 | checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" 405 | 406 | [[package]] 407 | name = "scopeguard" 408 | version = "1.1.0" 409 | source = "registry+https://github.com/rust-lang/crates.io-index" 410 | checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" 411 | 412 | [[package]] 413 | name = "strsim" 414 | version = "0.10.0" 415 | source = "registry+https://github.com/rust-lang/crates.io-index" 416 | checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" 417 | 418 | [[package]] 419 | name = "syn" 420 | version = "1.0.98" 421 | source = "registry+https://github.com/rust-lang/crates.io-index" 422 | checksum = "c50aef8a904de4c23c788f104b7dddc7d6f79c647c7c8ce4cc8f73eb0ca773dd" 423 | dependencies = [ 424 | "proc-macro2", 425 | "quote", 426 | "unicode-ident", 427 | ] 428 | 429 | [[package]] 430 | name = "termcolor" 431 | version = "1.1.3" 432 | source = "registry+https://github.com/rust-lang/crates.io-index" 433 | checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755" 434 | dependencies = [ 435 | "winapi-util", 436 | ] 437 | 438 | [[package]] 439 | name = "termtree" 440 | version = "0.2.4" 441 | source = "registry+https://github.com/rust-lang/crates.io-index" 442 | checksum = "507e9898683b6c43a9aa55b64259b721b52ba226e0f3779137e50ad114a4c90b" 443 | 444 | [[package]] 445 | name = "textwrap" 446 | version = "0.15.0" 447 | source = "registry+https://github.com/rust-lang/crates.io-index" 448 | checksum = "b1141d4d61095b28419e22cb0bbf02755f5e54e0526f97f1e3d1d160e60885fb" 449 | 450 | [[package]] 451 | name = "thiserror" 452 | version = "1.0.31" 453 | source = "registry+https://github.com/rust-lang/crates.io-index" 454 | checksum = "bd829fe32373d27f76265620b5309d0340cb8550f523c1dda251d6298069069a" 455 | dependencies = [ 456 | "thiserror-impl", 457 | ] 458 | 459 | [[package]] 460 | name = "thiserror-impl" 461 | version = "1.0.31" 462 | source = "registry+https://github.com/rust-lang/crates.io-index" 463 | checksum = "0396bc89e626244658bef819e22d0cc459e795a5ebe878e6ec336d1674a8d79a" 464 | dependencies = [ 465 | "proc-macro2", 466 | "quote", 467 | "syn", 468 | ] 469 | 470 | [[package]] 471 | name = "unicode-ident" 472 | version = "1.0.1" 473 | source = "registry+https://github.com/rust-lang/crates.io-index" 474 | checksum = "5bd2fe26506023ed7b5e1e315add59d6f584c621d037f9368fea9cfb988f368c" 475 | 476 | [[package]] 477 | name = "version_check" 478 | version = "0.9.4" 479 | source = "registry+https://github.com/rust-lang/crates.io-index" 480 | checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" 481 | 482 | [[package]] 483 | name = "vsv" 484 | version = "2.0.0" 485 | dependencies = [ 486 | "anyhow", 487 | "assert_cmd", 488 | "clap", 489 | "dirs", 490 | "libc", 491 | "rayon", 492 | "yansi", 493 | ] 494 | 495 | [[package]] 496 | name = "wait-timeout" 497 | version = "0.2.0" 498 | source = "registry+https://github.com/rust-lang/crates.io-index" 499 | checksum = "9f200f5b12eb75f8c1ed65abd4b2db8a6e1b138a20de009dacee265a2498f3f6" 500 | dependencies = [ 501 | "libc", 502 | ] 503 | 504 | [[package]] 505 | name = "wasi" 506 | version = "0.11.0+wasi-snapshot-preview1" 507 | source = "registry+https://github.com/rust-lang/crates.io-index" 508 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 509 | 510 | [[package]] 511 | name = "winapi" 512 | version = "0.3.9" 513 | source = "registry+https://github.com/rust-lang/crates.io-index" 514 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 515 | dependencies = [ 516 | "winapi-i686-pc-windows-gnu", 517 | "winapi-x86_64-pc-windows-gnu", 518 | ] 519 | 520 | [[package]] 521 | name = "winapi-i686-pc-windows-gnu" 522 | version = "0.4.0" 523 | source = "registry+https://github.com/rust-lang/crates.io-index" 524 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 525 | 526 | [[package]] 527 | name = "winapi-util" 528 | version = "0.1.5" 529 | source = "registry+https://github.com/rust-lang/crates.io-index" 530 | checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" 531 | dependencies = [ 532 | "winapi", 533 | ] 534 | 535 | [[package]] 536 | name = "winapi-x86_64-pc-windows-gnu" 537 | version = "0.4.0" 538 | source = "registry+https://github.com/rust-lang/crates.io-index" 539 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 540 | 541 | [[package]] 542 | name = "yansi" 543 | version = "0.5.1" 544 | source = "registry+https://github.com/rust-lang/crates.io-index" 545 | checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec" 546 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "vsv" 3 | version = "2.0.0" 4 | description = "Runit service manager CLI" 5 | edition = "2021" 6 | repository = "https://github.com/bahamas10/rust-vsv" 7 | license = "MIT" 8 | exclude = ["/assets"] 9 | keywords = ["runit", "void", "void-linux"] 10 | categories = ["command-line-utilities"] 11 | 12 | [dependencies] 13 | anyhow = "1.0.55" 14 | clap = { version = "3.1.1", features = ["cargo", "derive"] } 15 | dirs = "4.0.0" 16 | libc = "0.2.119" 17 | rayon = "1.5.1" 18 | yansi = "0.5.0" 19 | 20 | [dev-dependencies] 21 | assert_cmd = "2.0.4" 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2022 Dave Eddy (https://www.daveeddy.com) 2 | 3 | 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: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | 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. 8 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: all 2 | all: 3 | @echo 'nothing to do' 4 | 5 | .PHONY: man 6 | man: man/vsv.8 7 | man/vsv.8: man/vsv.md 8 | md2man-roff $^ > $@ 9 | 10 | .PHONY: clean 11 | clean: 12 | rm -f man/vsv.8 13 | 14 | .PHONY: check 15 | check: 16 | cargo check 17 | cargo clippy 18 | 19 | .PHONY: fmt 20 | fmt: 21 | cargo fmt 22 | 23 | .PHONY: test 24 | test: 25 | cargo test 26 | 27 | .PHONY: test-basic 28 | test-basic: 29 | cargo test --test basic 30 | 31 | .PHONY: test-integration 32 | test-integration: 33 | cargo test --test integration 34 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | `vsv` - Void Service Manager 2 | ============================ 3 | 4 | Manage and view runit services. 5 | 6 | **Note:** This is a rewrite in Rust of [`vsv`][vsv] which was originally written 7 | in Bash. 8 | 9 | `vsv` was inspired by [`vpm`][vpm]. `vsv` is to `sv` as `vpm` is to the 10 | `xbps-*` commands. 11 | 12 | Installation 13 | ------------ 14 | 15 | ### On Void 16 | 17 | xbps-install -S vsv 18 | 19 | ### Using `crates` 20 | 21 | cargo install vsv 22 | 23 | ### Compile yourself 24 | 25 | git clone git@github.com:bahamas10/vsv.git 26 | cd vsv 27 | cargo build 28 | 29 | Examples 30 | -------- 31 | 32 | Run `vsv` without any arguments to get process status. This is equivalent to 33 | running `vsv status`: 34 | 35 | vsv-status 36 | 37 | **Note:** `sudo` or escalated privileges are required to determine service state 38 | because of the strict permissions on each service's `supervise` directory. 39 | 40 | `vsv` scans the `/var/service` directory by default, which can be overridden by 41 | setting the `$SVDIR` environmental variable or passing in a `-d ` argument. 42 | Any service that has been in a state for less than 5 seconds will be marked 43 | in red and any less than 30 seconds will be marked in yellow, making new or 44 | failing services easy to spot: 45 | 46 | vsv-add-service.jpg 47 | 48 | We can isolate the service by passing it as a "filter" to `status`. 49 | 50 | vsv-filter.jpg 51 | 52 | A string can be passed as the first argument after `status` to filter for 53 | services that contain that string in their name. Also, `-t` can be supplied to 54 | `status` to print the process tree of the pid for that process: 55 | 56 | vsv-arguments.jpg 57 | 58 | Any command other than `status` will be passed directly to the `sv` command. 59 | Restarting a service is as easy as `vsv restart `: 60 | 61 | vsv-restart.jpg 62 | 63 | To stop a service, `vsv down ` or `vsv stop ` can be used: 64 | 65 | vsv-down.jpg 66 | 67 | A full service tree can be generated with `vsv -t`. This command is equivalent 68 | to running `vsv status -t`: 69 | 70 | vsv-tree.jpg 71 | 72 | `-l` can be specified to view log services for each service as well. This 73 | command is equivalent to running `vsv status -l virt`: 74 | 75 | vsv-log.jpg 76 | 77 | `-t` can be specified with `-l` to view log services as a tree for each service 78 | as well as normal services. This command is equivalent to running `vsv status 79 | -tl virt`: 80 | 81 | vsv-log-tree.jpg 82 | 83 | Usage 84 | ----- 85 | 86 | Quick Examples: 87 | 88 | - `vsv` - show all services 89 | - `vsv status` - same as above 90 | - `vsv stop ` - stop a service 91 | - `vsv start ` - start a service 92 | - `vsv restart ` - restart a service 93 | - `vsv enable ` - enable a service (autostart at boot) 94 | - `vsv disable ` - disable a service (no autostart at boot) 95 | - `vsv hup ` - refresh a service (`SIGHUP`) 96 | 97 | Status: 98 | 99 | The `status` subcommand has the following fields: 100 | 101 | - `SERVICE` - the service (directory) name. 102 | - `STATE` - the service state: output from `.../$service/supervise/stat`. 103 | - `ENABLED` - if the service is enabled (lacks the `.../$service/down` file). 104 | - `PID` - the pid of the process being monitored. 105 | - `COMMAND` - arg0 from the pid being monitored (first field of `/proc/$pid/cmdline`. 106 | - `TIME` - time the service has been in whatever state it is in. 107 | 108 | Command Usage: 109 | 110 | $ vsv -h 111 | __ _______ __ 112 | \ \ / / __\ \ / / Void Service Manager 113 | \ V /\__ \\ V / Source: https://github.com/bahamas10/vsv 114 | \_/ |___/ \_/ MIT License 115 | ------------- 116 | Manage and view runit services 117 | Made specifically for Void Linux but should work anywhere 118 | Author: Dave Eddy (bahamas10) 119 | 120 | vsv 2.0.0 121 | Runit service manager CLI 122 | 123 | USAGE: 124 | vsv [OPTIONS] [SUBCOMMAND] 125 | 126 | OPTIONS: 127 | -c, --color Enable or disable color output 128 | -d, --dir Directory to look into, defaults to env SVDIR or /var/service if 129 | unset 130 | -h, --help Print help information 131 | -l, --log Show log processes, this is a shortcut for `status -l` 132 | -t, --tree Tree view, this is a shortcut for `status -t` 133 | -u, --user User mode, this is a shortcut for `-d ~/runit/service` 134 | -v, --verbose Increase Verbosity 135 | -V, --version Print version information 136 | 137 | SUBCOMMANDS: 138 | disable Disable service(s) 139 | enable Enable service(s) 140 | help Print this message or the help of the given subcommand(s) 141 | status Show process status 142 | 143 | Any other subcommand gets passed directly to the 'sv' command, see sv(1) for 144 | the full list of subcommands and information about what each does specifically. 145 | Common subcommands: 146 | 147 | start Start the service 148 | stop Stop the service 149 | restart Restart the service 150 | reload Reload the service (send SIGHUP) 151 | 152 | Environmental Variables: 153 | 154 | - `SVDIR`: The directory to use, passed to the `sv` command, can be overridden 155 | with `-d `. 156 | - `PROC_DIR`: A Linux procfs directory to use for command name lookups, defaults 157 | to `/proc`. 158 | - `SV_PROG`: The command to use for any "external" subcommand given to `vsv`, 159 | defaults to `sv`. 160 | - `PSTREE_PROG`: The command to use to get a process tree for a given pid, 161 | defaults to `pstree`. 162 | - `NO_COLOR`: Set this environmental variable to disable color output. 163 | 164 | Syntax 165 | ------ 166 | 167 | All source code should be clean of `cargo clippy` and `cargo fmt`. You can use 168 | `make` to ensure this: 169 | 170 | ``` 171 | $ make check 172 | cargo check 173 | Finished dev [unoptimized + debuginfo] target(s) in 0.01s 174 | cargo clippy 175 | Finished dev [unoptimized + debuginfo] target(s) in 0.12s 176 | $ make fmt 177 | cargo fmt 178 | ``` 179 | 180 | License 181 | ------- 182 | 183 | MIT License 184 | 185 | [vpm]: https://github.com/netzverweigerer/vpm 186 | [vsv]: https://github.com/bahamas10/bash-vsv 187 | -------------------------------------------------------------------------------- /assets/vsv-add-service.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bahamas10/vsv/0103e67a9fd271692b75dd5d54b8e30b39ba7144/assets/vsv-add-service.jpg -------------------------------------------------------------------------------- /assets/vsv-arguments.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bahamas10/vsv/0103e67a9fd271692b75dd5d54b8e30b39ba7144/assets/vsv-arguments.jpg -------------------------------------------------------------------------------- /assets/vsv-down.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bahamas10/vsv/0103e67a9fd271692b75dd5d54b8e30b39ba7144/assets/vsv-down.jpg -------------------------------------------------------------------------------- /assets/vsv-filter.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bahamas10/vsv/0103e67a9fd271692b75dd5d54b8e30b39ba7144/assets/vsv-filter.jpg -------------------------------------------------------------------------------- /assets/vsv-log-tree.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bahamas10/vsv/0103e67a9fd271692b75dd5d54b8e30b39ba7144/assets/vsv-log-tree.jpg -------------------------------------------------------------------------------- /assets/vsv-log.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bahamas10/vsv/0103e67a9fd271692b75dd5d54b8e30b39ba7144/assets/vsv-log.jpg -------------------------------------------------------------------------------- /assets/vsv-restart.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bahamas10/vsv/0103e67a9fd271692b75dd5d54b8e30b39ba7144/assets/vsv-restart.jpg -------------------------------------------------------------------------------- /assets/vsv-status.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bahamas10/vsv/0103e67a9fd271692b75dd5d54b8e30b39ba7144/assets/vsv-status.jpg -------------------------------------------------------------------------------- /assets/vsv-tree.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bahamas10/vsv/0103e67a9fd271692b75dd5d54b8e30b39ba7144/assets/vsv-tree.jpg -------------------------------------------------------------------------------- /benchmarks/.gitignore: -------------------------------------------------------------------------------- 1 | proc 2 | service 3 | -------------------------------------------------------------------------------- /benchmarks/make-test-dirs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | servicedir='./service' 4 | procdir='./proc' 5 | 6 | mkdir -p "$servicedir" 7 | mkdir -p "$procdir" 8 | 9 | make-service() { 10 | local name=$1 11 | local state=$2 12 | local pid=$3 13 | 14 | local dir=$servicedir/$name 15 | 16 | echo "creating $dir: (pid '$pid' state '$state')" 17 | 18 | mkdir -p "$dir" 19 | mkdir -p "$dir/supervise" 20 | echo "$state" > "$dir/supervise/stat" 21 | echo "$pid" > "$dir/supervise/pid" 22 | 23 | local proc=$procdir/$pid 24 | echo "creating $proc" 25 | mkdir -p "$proc" 26 | printf '%s-command\0arg1\0arg2\0' "$name" > "$proc/cmdline" 27 | } 28 | 29 | pid=1 30 | for i in {1..1000}; do 31 | make-service "$i" 'run' "$pid" 32 | ((pid++)) 33 | make-service "$i/log" 'run' "$pid" 34 | ((pid++)) 35 | done 36 | 37 | true 38 | -------------------------------------------------------------------------------- /man/vsv.8: -------------------------------------------------------------------------------- 1 | .TH VSV 8 "FEB 2022" "System Manager's Utilities" 2 | .SH NAME 3 | .PP 4 | \fB\fCvsv\fR \- manage and view runit services 5 | .SH SYNOPSIS 6 | .PP 7 | \fB\fCvsv [OPTIONS] [SUBCOMMAND] []\fR 8 | .PP 9 | \fB\fCvsv [\-u] [\-d ] [\-h] [\-t] [SUBCOMMAND] [...]\fR 10 | .SH DESCRIPTION 11 | .PP 12 | \fB\fCvsv\fR is a wrapper for the \fB\fCsv\fR command that can be used to query and manage 13 | services under runit. It was made specifically for Void Linux but should 14 | theoretically work on any system using runit to manage services. 15 | .SH OPTIONS 16 | .TP 17 | \fB\fC\-c \fR 18 | Enable/disable color output, defaults to auto. 19 | .TP 20 | \fB\fC\-d\fR \fIdir\fP 21 | Directory to look into, defaults to env \fB\fCSVDIR\fR or \fB\fC/var/service\fR if unset. 22 | .TP 23 | \fB\fC\-h\fR 24 | Print this message and exit. 25 | .TP 26 | \fB\fC\-l\fR 27 | Show log processes, this is a shortcut for \fB\fCvsv status \-l\fR\&. 28 | .TP 29 | \fB\fC\-t\fR 30 | Tree view, this is a shortcut for \fB\fCvsv status \-t\fR\&. 31 | .TP 32 | \fB\fC\-u\fR 33 | User mode, this is a shortcut for \fB\fCvsv \-d ~/runit/service\fR\&. 34 | .TP 35 | \fB\fC\-v\fR 36 | Increase verbosity. 37 | .TP 38 | \fB\fC\-V\fR 39 | Print the version number and exit. 40 | .SH ENVIRONMENT 41 | .TP 42 | \fB\fCSVDIR\fR 43 | The directory to use, passed to the \fB\fCsv\fR command, can be overridden with \fB\fC\-d 44 | \fR\&. 45 | .TP 46 | \fB\fCPROC_DIR\fR 47 | A Linux procfs directory to use for command name lookups, defaults to \fB\fC/proc\fR\&. 48 | .TP 49 | \fB\fCSV_PROG\fR 50 | The command to use for any "external" subcommand given to \fB\fCvsv\fR, defaults to 51 | \fB\fCsv\fR\&. 52 | .TP 53 | \fB\fCPSTREE_PROG\fR 54 | The command to use to get a process tree for a given pid, defaults to 55 | \fB\fCpstree\fR\&. 56 | .TP 57 | \fB\fCNO_COLOR\fR 58 | Set this environmental variable to disable color output. 59 | .SH SUBCOMMANDS 60 | .PP 61 | \fB\fCstatus\fR 62 | .PP 63 | \fB\fCvsv status [\-lt] [filter]\fR 64 | .PP 65 | Default subcommand, show process status 66 | .TP 67 | \fB\fC\-t\fR 68 | Enables tree mode (process tree) 69 | .TP 70 | \fB\fC\-l\fR 71 | Enables log mode (show log processes) 72 | .TP 73 | \fB\fCfilter\fR 74 | An optional string to match service names against 75 | .PP 76 | Any other subcommand gets passed directly to the \fB\fCsv\fR command, see \fB\fCsv(1)\fR for 77 | the full list of subcommands and information about what each does specifically. 78 | Common subcommands: 79 | .PP 80 | \fB\fCstart \fR 81 | .IP 82 | Start the service 83 | .PP 84 | \fB\fCstop \fR 85 | .IP 86 | Stop the service 87 | .PP 88 | \fB\fCrestart \fR 89 | .IP 90 | Restart the service 91 | .PP 92 | \fB\fCreload \fR 93 | .IP 94 | Reload the service (send \fB\fCSIGHUP\fR) 95 | .PP 96 | \fB\fCenable \fR 97 | .PP 98 | Enable the service (remove the "down" file, does not start service) 99 | .PP 100 | \fB\fCdisable \fR 101 | .PP 102 | Disable the service (create the "down" file, does not stop service) 103 | .SH EXAMPLES 104 | .PP 105 | \fB\fCvsv\fR 106 | .IP 107 | Show service status in \fB\fC/var/service\fR 108 | .PP 109 | \fB\fCvsv status\fR 110 | .IP 111 | Same as above 112 | .PP 113 | \fB\fCvsv \-t\fR 114 | .IP 115 | Show service status + \fB\fCpstree\fR output 116 | .PP 117 | \fB\fCvsv status \-t\fR 118 | .IP 119 | Same as above 120 | .PP 121 | \fB\fCvsv status tty\fR 122 | .IP 123 | Show service status for any service that matches \fB\fCtty\fR 124 | .PP 125 | \fB\fCvsv check uuidd\fR 126 | .IP 127 | Check the uuidd svc, wrapper for \fB\fCsv check uuidd\fR 128 | .PP 129 | \fB\fCvsv restart sshd\fR 130 | .IP 131 | Restart sshd, wrapper for \fB\fCsv restart sshd\fR 132 | .PP 133 | \fB\fCvsv \-u\fR 134 | .IP 135 | Show service status in \fB\fC~/runit/service\fR 136 | .PP 137 | \fB\fCvsv \-u restart ssh\-agent\fR 138 | .IP 139 | Restart ssh\-agent in \fB\fC~/runit/service/ssh\-agent\fR 140 | .SH BUGS 141 | .PP 142 | \[la]https://github.com/bahamas10/rust-vsv\[ra] 143 | .SH AUTHOR 144 | .PP 145 | \fB\fCDave Eddy (https://www.daveeddy.com)\fR 146 | .SH SEE ALSO 147 | .PP 148 | .BR sv (8), 149 | .BR runsvdir (8) 150 | .SH LICENSE 151 | .PP 152 | MIT License 153 | -------------------------------------------------------------------------------- /man/vsv.md: -------------------------------------------------------------------------------- 1 | VSV 8 "FEB 2022" "System Manager's Utilities" 2 | ============================================= 3 | 4 | NAME 5 | ---- 6 | 7 | `vsv` - manage and view runit services 8 | 9 | SYNOPSIS 10 | -------- 11 | 12 | `vsv [OPTIONS] [SUBCOMMAND] []` 13 | 14 | `vsv [-u] [-d ] [-h] [-t] [SUBCOMMAND] [...]` 15 | 16 | DESCRIPTION 17 | ----------- 18 | 19 | `vsv` is a wrapper for the `sv` command that can be used to query and manage 20 | services under runit. It was made specifically for Void Linux but should 21 | theoretically work on any system using runit to manage services. 22 | 23 | OPTIONS 24 | ------- 25 | 26 | `-c ` 27 | Enable/disable color output, defaults to auto. 28 | 29 | `-d` *dir* 30 | Directory to look into, defaults to env `SVDIR` or `/var/service` if unset. 31 | 32 | `-h` 33 | Print this message and exit. 34 | 35 | `-l` 36 | Show log processes, this is a shortcut for `vsv status -l`. 37 | 38 | `-t` 39 | Tree view, this is a shortcut for `vsv status -t`. 40 | 41 | `-u` 42 | User mode, this is a shortcut for `vsv -d ~/runit/service`. 43 | 44 | `-v` 45 | Increase verbosity. 46 | 47 | `-V` 48 | Print the version number and exit. 49 | 50 | ENVIRONMENT 51 | ----------- 52 | 53 | `SVDIR` 54 | The directory to use, passed to the `sv` command, can be overridden with `-d 55 | `. 56 | 57 | `PROC_DIR` 58 | A Linux procfs directory to use for command name lookups, defaults to `/proc`. 59 | 60 | `SV_PROG` 61 | The command to use for any "external" subcommand given to `vsv`, defaults to 62 | `sv`. 63 | 64 | `PSTREE_PROG` 65 | The command to use to get a process tree for a given pid, defaults to 66 | `pstree`. 67 | 68 | `NO_COLOR` 69 | Set this environmental variable to disable color output. 70 | 71 | SUBCOMMANDS 72 | ----------- 73 | 74 | `status` 75 | 76 | `vsv status [-lt] [filter]` 77 | 78 | Default subcommand, show process status 79 | 80 | `-t` 81 | Enables tree mode (process tree) 82 | 83 | `-l` 84 | Enables log mode (show log processes) 85 | 86 | `filter` 87 | An optional string to match service names against 88 | 89 | Any other subcommand gets passed directly to the `sv` command, see `sv(1)` for 90 | the full list of subcommands and information about what each does specifically. 91 | Common subcommands: 92 | 93 | `start ` 94 | 95 | Start the service 96 | 97 | `stop ` 98 | 99 | Stop the service 100 | 101 | `restart ` 102 | 103 | Restart the service 104 | 105 | `reload ` 106 | 107 | Reload the service (send `SIGHUP`) 108 | 109 | `enable ` 110 | 111 | Enable the service (remove the "down" file, does not start service) 112 | 113 | `disable ` 114 | 115 | Disable the service (create the "down" file, does not stop service) 116 | 117 | EXAMPLES 118 | -------- 119 | 120 | `vsv` 121 | 122 | Show service status in `/var/service` 123 | 124 | `vsv status` 125 | 126 | Same as above 127 | 128 | `vsv -t` 129 | 130 | Show service status + `pstree` output 131 | 132 | `vsv status -t` 133 | 134 | Same as above 135 | 136 | `vsv status tty` 137 | 138 | Show service status for any service that matches `tty` 139 | 140 | `vsv check uuidd` 141 | 142 | Check the uuidd svc, wrapper for `sv check uuidd` 143 | 144 | `vsv restart sshd` 145 | 146 | Restart sshd, wrapper for `sv restart sshd` 147 | 148 | `vsv -u` 149 | 150 | Show service status in `~/runit/service` 151 | 152 | `vsv -u restart ssh-agent` 153 | 154 | Restart ssh-agent in `~/runit/service/ssh-agent` 155 | 156 | BUGS 157 | ---- 158 | 159 | https://github.com/bahamas10/rust-vsv 160 | 161 | AUTHOR 162 | ------ 163 | 164 | `Dave Eddy (https://www.daveeddy.com)` 165 | 166 | SEE ALSO 167 | -------- 168 | 169 | sv(8), runsvdir(8) 170 | 171 | LICENSE 172 | ------- 173 | 174 | MIT License 175 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | max_width = 80 2 | use_small_heuristics = "max" 3 | -------------------------------------------------------------------------------- /src/arguments.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * Author: Dave Eddy 3 | * Date: January 25, 2022 4 | * License: MIT 5 | */ 6 | 7 | //! Argument parsing logic (via `clap`) for vsv. 8 | 9 | use std::path; 10 | 11 | use clap::{Parser, Subcommand}; 12 | 13 | #[derive(Debug, Parser)] 14 | #[clap(author, version, about, verbatim_doc_comment, long_about = None)] 15 | #[clap(before_help = r" __ _______ __ 16 | \ \ / / __\ \ / / Void Service Manager 17 | \ V /\__ \\ V / Source: https://github.com/bahamas10/vsv 18 | \_/ |___/ \_/ MIT License 19 | ------------- 20 | Manage and view runit services 21 | Made specifically for Void Linux but should work anywhere 22 | Author: Dave Eddy (bahamas10)")] 23 | #[clap( 24 | after_help = "Any other subcommand gets passed directly to the 'sv' command, see sv(1) for 25 | the full list of subcommands and information about what each does specifically. 26 | Common subcommands: 27 | 28 | start Start the service 29 | stop Stop the service 30 | restart Restart the service 31 | reload Reload the service (send SIGHUP) 32 | " 33 | )] 34 | pub struct Args { 35 | /// Enable or disable color output. 36 | #[clap(short, long, value_name = "yes|no|auto")] 37 | pub color: Option, 38 | 39 | /// Directory to look into, defaults to env SVDIR or /var/service if unset. 40 | #[clap(short, long, parse(from_os_str), value_name = "dir")] 41 | pub dir: Option, 42 | 43 | /// Show log processes, this is a shortcut for `status -l`. 44 | #[clap(short, long)] 45 | pub log: bool, 46 | 47 | /// Tree view, this is a shortcut for `status -t`. 48 | #[clap(short, long)] 49 | pub tree: bool, 50 | 51 | /// User mode, this is a shortcut for `-d ~/runit/service`. 52 | #[clap(short, long)] 53 | pub user: bool, 54 | 55 | /// Increase Verbosity. 56 | #[clap(short, long, parse(from_occurrences))] 57 | pub verbose: usize, 58 | 59 | /// Subcommand. 60 | #[clap(subcommand)] 61 | pub command: Option, 62 | } 63 | 64 | #[derive(Debug, Subcommand)] 65 | pub enum Commands { 66 | /// Show process status. 67 | Status { 68 | /// Show associated log processes. 69 | #[clap(short, long)] 70 | log: bool, 71 | 72 | /// Tree view (calls pstree(1) on PIDs found). 73 | #[clap(short, long)] 74 | tree: bool, 75 | 76 | filter: Vec, 77 | }, 78 | 79 | /// Enable service(s). 80 | Enable { services: Vec }, 81 | 82 | /// Disable service(s). 83 | Disable { services: Vec }, 84 | 85 | /// Pass arguments directly to `sv`. 86 | #[clap(external_subcommand)] 87 | External(Vec), 88 | } 89 | 90 | pub fn parse() -> Args { 91 | Args::parse() 92 | } 93 | -------------------------------------------------------------------------------- /src/commands/enable_disable.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * Author: Dave Eddy 3 | * Date: February 15, 2022 4 | * License: MIT 5 | */ 6 | 7 | //! `vsv enable` and `vsv disable`. 8 | 9 | use anyhow::{ensure, Result}; 10 | use yansi::{Color, Style}; 11 | 12 | use crate::config; 13 | use crate::config::Config; 14 | use crate::runit::RunitService; 15 | 16 | /// Handle `vsv enable`. 17 | pub fn do_enable(cfg: &Config) -> Result<()> { 18 | _do_enable_disable(cfg) 19 | } 20 | 21 | /// Handle `vsv enable`. 22 | pub fn do_disable(cfg: &Config) -> Result<()> { 23 | _do_enable_disable(cfg) 24 | } 25 | 26 | /// Handle `vsv enable` and `vsv disable`. 27 | fn _do_enable_disable(cfg: &Config) -> Result<()> { 28 | ensure!(!cfg.operands.is_empty(), "at least one (1) service required"); 29 | 30 | let mut had_error = false; 31 | 32 | for name in &cfg.operands { 33 | let p = cfg.svdir.join(name); 34 | let svc = RunitService::new(name, &p); 35 | print!( 36 | "{} service {}... ", 37 | cfg.mode, 38 | Style::default().bold().paint(name) 39 | ); 40 | 41 | if !svc.valid() { 42 | println!("{}", Color::Red.paint("failed! service not valid")); 43 | had_error = true; 44 | continue; 45 | } 46 | 47 | let ret = match cfg.mode { 48 | config::ProgramMode::Enable => svc.enable(), 49 | config::ProgramMode::Disable => svc.disable(), 50 | _ => unreachable!(), 51 | }; 52 | 53 | match ret { 54 | Err(err) => { 55 | had_error = true; 56 | println!("{}", Color::Red.paint(format!("failed! {}", err))); 57 | } 58 | Ok(()) => println!("{}.", Color::Green.paint("done")), 59 | }; 60 | } 61 | 62 | ensure!(!had_error, "failed to modify service(s)"); 63 | 64 | Ok(()) 65 | } 66 | -------------------------------------------------------------------------------- /src/commands/external.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * Author: Dave Eddy 3 | * Date: January 25, 2022 4 | * License: MIT 5 | */ 6 | 7 | //! `vsv `. 8 | 9 | use std::env; 10 | 11 | use anyhow::{bail, ensure, Context, Result}; 12 | use clap::crate_name; 13 | use yansi::Color; 14 | 15 | use crate::utils; 16 | use crate::{config, config::Config}; 17 | 18 | /// Handle `vsv `. 19 | pub fn do_external(cfg: &Config) -> Result<()> { 20 | assert!(!cfg.operands.is_empty()); 21 | 22 | let sv = cfg.sv_prog.to_owned(); 23 | 24 | ensure!( 25 | cfg.operands.len() >= 2, 26 | "argument expected for '{} {}'", 27 | sv, 28 | cfg.operands[0] 29 | ); 30 | 31 | // format arguments 32 | let args_s = cfg.operands.join(" "); 33 | 34 | // set SVDIR env to match what user wanted 35 | env::set_var(config::ENV_SVDIR, &cfg.svdir); 36 | 37 | println!( 38 | "[{}] {}", 39 | crate_name!(), 40 | Color::Cyan.paint(format!( 41 | "Running {} command ({}={:?} {} {})", 42 | sv, 43 | config::ENV_SVDIR, 44 | &cfg.svdir, 45 | sv, 46 | &args_s 47 | )) 48 | ); 49 | 50 | // run the actual program 51 | let status = utils::run_program_get_status(&sv, &cfg.operands) 52 | .with_context(|| format!("failed to execute {}", sv))?; 53 | 54 | // check the process status 55 | let code = status.code().unwrap_or(-1); 56 | let color = match code { 57 | 0 => Color::Green, 58 | _ => Color::Red, 59 | }; 60 | 61 | // print exit code 62 | println!( 63 | "[{}] {}", 64 | crate_name!(), 65 | color.paint(format!("[{} {}] exit code {}", sv, &args_s, code)) 66 | ); 67 | 68 | match code { 69 | 0 => Ok(()), 70 | _ => bail!("call to {} failed", sv), 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/commands/mod.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * Author: Dave Eddy 3 | * Date: February 15, 2022 4 | * License: MIT 5 | */ 6 | 7 | //! Subcommands for `vsv`. 8 | 9 | pub mod enable_disable; 10 | pub mod external; 11 | pub mod status; 12 | -------------------------------------------------------------------------------- /src/commands/status.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * Author: Dave Eddy 3 | * Date: February 15, 2022 4 | * License: MIT 5 | */ 6 | 7 | //! `vsv status` subcommand. 8 | 9 | use anyhow::{Context, Result}; 10 | use rayon::prelude::*; 11 | use yansi::Style; 12 | 13 | use crate::config::Config; 14 | use crate::runit; 15 | use crate::service::Service; 16 | use crate::{utils, utils::verbose}; 17 | 18 | /// Handle `vsv status` or `vsv` without a subcommand given. 19 | pub fn do_status(cfg: &Config) -> Result<()> { 20 | // may or may not be set (option) 21 | let filter = cfg.operands.get(0); 22 | 23 | // find all services 24 | let services = runit::get_services(&cfg.svdir, cfg.log, filter) 25 | .with_context(|| { 26 | format!("failed to list services in {:?}", cfg.svdir) 27 | })?; 28 | 29 | // loop each service found (just gather data here, can be done in parallel) 30 | let services: Vec<(Service, Vec)> = services 31 | .par_iter() 32 | .map(|service| { 33 | Service::from_runit_service( 34 | service, 35 | cfg.tree, 36 | &cfg.proc_path, 37 | &cfg.pstree_prog, 38 | ) 39 | }) 40 | .collect(); 41 | 42 | // print gathared data 43 | let style = Style::default(); 44 | 45 | verbose!(cfg, "found {} services in {:?}", services.len(), cfg.svdir); 46 | println!(); 47 | println!( 48 | "{}", 49 | utils::format_status_line( 50 | ("", style.bold()), 51 | ("SERVICE", style.bold()), 52 | ("STATE", style.bold()), 53 | ("ENABLED", style.bold()), 54 | ("PID", style.bold()), 55 | ("COMMAND", style.bold()), 56 | ("TIME", style.bold()), 57 | ) 58 | ); 59 | 60 | // print each service found 61 | for (service, messages) in services { 62 | println!("{}", service); 63 | 64 | // print pstree if applicable 65 | if cfg.tree { 66 | let (tree_s, style) = service.format_pstree(); 67 | println!("{}", style.paint(tree_s)); 68 | } 69 | 70 | // print any verbose messages/warnings generated by the service 71 | for message in messages { 72 | verbose!(cfg, "{}", message); 73 | } 74 | } 75 | 76 | if !cfg.tree { 77 | // add a newline to the end of the output if no tree was printed 78 | println!(); 79 | } 80 | 81 | Ok(()) 82 | } 83 | -------------------------------------------------------------------------------- /src/config.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * Author: Dave Eddy 3 | * Date: January 25, 2022 4 | * License: MIT 5 | */ 6 | 7 | /*! 8 | * Config context variable and various constants for vsv. 9 | * 10 | * The main idea here is that after CLI arguments are parsed by `clap` the args 11 | * object will be given to the config constructor via `::from_args(&args)` and 12 | * from that + ENV variables a config object will be created. 13 | */ 14 | 15 | use std::env; 16 | use std::ffi::OsString; 17 | use std::fmt; 18 | use std::path::PathBuf; 19 | 20 | use anyhow::{bail, Context, Result}; 21 | 22 | use crate::arguments::{Args, Commands}; 23 | use crate::config; 24 | use crate::utils; 25 | 26 | // default values 27 | pub const DEFAULT_SVDIR: &str = "/var/service"; 28 | pub const DEFAULT_PROC_DIR: &str = "/proc"; 29 | pub const DEFAULT_SV_PROG: &str = "sv"; 30 | pub const DEFAULT_PSTREE_PROG: &str = "pstree"; 31 | pub const DEFAULT_USER_DIR: &str = "runit/service"; 32 | 33 | // env var name 34 | pub const ENV_NO_COLOR: &str = "NO_COLOR"; 35 | pub const ENV_SVDIR: &str = "SVDIR"; 36 | pub const ENV_PROC_DIR: &str = "PROC_DIR"; 37 | pub const ENV_SV_PROG: &str = "SV_PROG"; 38 | pub const ENV_PSTREE_PROG: &str = "PSTREE_PROG"; 39 | 40 | /// vsv execution modes (subcommands). 41 | #[derive(Debug)] 42 | pub enum ProgramMode { 43 | Status, 44 | Enable, 45 | Disable, 46 | External, 47 | } 48 | 49 | impl fmt::Display for ProgramMode { 50 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 51 | let s = match self { 52 | ProgramMode::Status => "status", 53 | ProgramMode::Enable => "enable", 54 | ProgramMode::Disable => "disable", 55 | ProgramMode::External => "", 56 | }; 57 | 58 | s.fmt(f) 59 | } 60 | } 61 | 62 | /** 63 | * Configuration options derived from the environment and CLI arguments. 64 | * 65 | * This struct holds all configuration data for the invocation of `vsv` derived 66 | * from both env variables and CLI arguments. This object can be passed around 67 | * and thought of as a "context" variable. 68 | */ 69 | #[derive(Debug)] 70 | pub struct Config { 71 | // env vars only 72 | pub proc_path: PathBuf, 73 | pub sv_prog: String, 74 | pub pstree_prog: String, 75 | 76 | // env vars or CLI options 77 | pub colorize: bool, 78 | pub svdir: PathBuf, 79 | 80 | // CLI options only 81 | pub tree: bool, 82 | pub log: bool, 83 | pub verbose: usize, 84 | pub operands: Vec, 85 | pub mode: ProgramMode, 86 | } 87 | 88 | impl Config { 89 | /// Create a `Config` struct from a clap `Args` struct. 90 | pub fn from_args(args: &Args) -> Result { 91 | let mut tree = args.tree; 92 | let mut log = args.log; 93 | 94 | let proc_path: PathBuf = env::var_os(config::ENV_PROC_DIR) 95 | .unwrap_or_else(|| OsString::from(DEFAULT_PROC_DIR)) 96 | .into(); 97 | let sv_prog = env::var(config::ENV_SV_PROG) 98 | .unwrap_or_else(|_| DEFAULT_SV_PROG.to_string()); 99 | let pstree_prog = env::var(config::ENV_PSTREE_PROG) 100 | .unwrap_or_else(|_| DEFAULT_PSTREE_PROG.to_string()); 101 | 102 | let colorize = should_colorize_output(&args.color)?; 103 | let svdir = get_svdir(&args.dir, args.user)?; 104 | let verbose = args.verbose; 105 | 106 | // let arguments after `vsv status` work as well. 107 | if let Some(Commands::Status { tree: _tree, log: _log, filter: _ }) = 108 | &args.command 109 | { 110 | if *_tree { 111 | tree = true; 112 | } 113 | if *_log { 114 | log = true; 115 | } 116 | }; 117 | 118 | // figure out subcommand to run 119 | let (mode, operands) = match &args.command { 120 | // `vsv` (no subcommand) 121 | None => { 122 | let v: Vec = vec![]; 123 | (ProgramMode::Status, v) 124 | } 125 | // `vsv status` 126 | Some(Commands::Status { tree: _, log: _, filter: operands }) => { 127 | (ProgramMode::Status, operands.to_vec()) 128 | } 129 | // `vsv enable ...` 130 | Some(Commands::Enable { services }) => { 131 | (ProgramMode::Enable, services.to_vec()) 132 | } 133 | // `vsv disable ...` 134 | Some(Commands::Disable { services }) => { 135 | (ProgramMode::Disable, services.to_vec()) 136 | } 137 | // `vsv ...` 138 | Some(Commands::External(args)) => { 139 | // -t or -l will put the program into status mode 140 | let mode = if tree || log { 141 | ProgramMode::Status 142 | } else { 143 | ProgramMode::External 144 | }; 145 | (mode, args.to_vec()) 146 | } 147 | }; 148 | 149 | let o = Self { 150 | proc_path, 151 | sv_prog, 152 | pstree_prog, 153 | colorize, 154 | svdir, 155 | tree, 156 | log, 157 | verbose, 158 | operands, 159 | mode, 160 | }; 161 | 162 | Ok(o) 163 | } 164 | } 165 | 166 | /** 167 | * Check if the output should be colorized. 168 | * 169 | * Coloring output goes in order from highest priority to lowest priority 170 | * -highest priority (first in this list) wins: 171 | * 172 | * 1. CLI option (`-c`) given. 173 | * 2. env `NO_COLOR` given. 174 | * 3. stdout is a tty. 175 | */ 176 | fn should_colorize_output(color_arg: &Option) -> Result { 177 | // check CLI option first 178 | if let Some(s) = color_arg { 179 | match s.as_str() { 180 | "yes" | "on" | "always" => return Ok(true), 181 | "no" | "off" | "never" => return Ok(false), 182 | "auto" => (), // fall through 183 | _ => bail!("unknown color option: '{}'", s), 184 | } 185 | } 186 | 187 | // check env var next 188 | if env::var_os(config::ENV_NO_COLOR).is_some() { 189 | return Ok(false); 190 | } 191 | 192 | // lastly check if stdout is a tty 193 | let isatty = utils::isatty(1); 194 | 195 | Ok(isatty) 196 | } 197 | 198 | /** 199 | * Determine the `SVDIR` the user wants. 200 | * 201 | * Check svdir in this order: 202 | * 203 | * 1. CLI option (`-d`) given 204 | * 2. CLI option (`-u`) given 205 | * 3. env `SVDIR` given 206 | * 4. use `DEFAULT_SVDIR` (`"/var/service"`) 207 | */ 208 | fn get_svdir(dir_arg: &Option, user_arg: bool) -> Result { 209 | // `-d ` 210 | if let Some(dir) = dir_arg { 211 | return Ok(dir.to_path_buf()); 212 | } 213 | 214 | // `-u` 215 | if user_arg { 216 | let home_dir = dirs::home_dir() 217 | .context("failed to determine users home directory")?; 218 | let buf = home_dir.join(DEFAULT_USER_DIR); 219 | return Ok(buf); 220 | } 221 | 222 | // env or default 223 | let svdir = env::var_os(config::ENV_SVDIR) 224 | .unwrap_or_else(|| OsString::from(config::DEFAULT_SVDIR)); 225 | let buf = PathBuf::from(&svdir); 226 | 227 | Ok(buf) 228 | } 229 | -------------------------------------------------------------------------------- /src/die.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * Author: Dave Eddy 3 | * Date: January 26, 2022 4 | * License: MIT 5 | */ 6 | 7 | /*! 8 | * Contains the `die!()` convenience macro for exiting a program with a code and 9 | * message. 10 | */ 11 | 12 | /** 13 | * Exit the current program with a code and optional message. 14 | * 15 | * # Usage 16 | * 17 | * Exit the program successfully with no message: 18 | * 19 | * ``` 20 | * die!(0); 21 | * ``` 22 | * 23 | * Exit the program with code 1 and a message: 24 | * 25 | * ``` 26 | * die!(1, "uh oh"); 27 | * ``` 28 | * 29 | * Exit the program with code 57 and an error message: 30 | * 31 | * ``` 32 | * let bad_num: u32 = "foo".parse(); 33 | * 34 | * if Err(err) = bad_num { 35 | * die!(57, "number parsing failed: {:?}", err); 36 | * } 37 | * ``` 38 | */ 39 | macro_rules! die { 40 | () => { 41 | ::std::process::exit(1); 42 | }; 43 | 44 | ($code:expr $(,)?) => { 45 | ::std::process::exit($code); 46 | }; 47 | 48 | ($code:expr, $fmt:expr $(, $args:expr )* $(,)?) => {{ 49 | eprintln!($fmt $( , $args )*); 50 | ::std::process::exit($code); 51 | }}; 52 | } 53 | 54 | pub(crate) use die; 55 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * Author: Dave Eddy 3 | * Date: January 25, 2022 4 | * License: MIT 5 | */ 6 | 7 | /*! 8 | * A rust port of `vsv` 9 | * 10 | * Original: 11 | */ 12 | 13 | #![allow(clippy::uninlined_format_args)] 14 | 15 | use anyhow::{Context, Result}; 16 | use yansi::{Color, Paint}; 17 | 18 | mod arguments; 19 | mod commands; 20 | mod config; 21 | mod die; 22 | mod runit; 23 | mod service; 24 | mod utils; 25 | 26 | use config::{Config, ProgramMode}; 27 | use die::die; 28 | use utils::verbose; 29 | 30 | fn do_main() -> Result<()> { 31 | // disable color until we absolutely know we want it 32 | Paint::disable(); 33 | 34 | // parse CLI options + env vars 35 | let args = arguments::parse(); 36 | let cfg = 37 | Config::from_args(&args).context("failed to parse args into config")?; 38 | 39 | // toggle color if the user wants it or the env dictates 40 | if cfg.colorize { 41 | Paint::enable(); 42 | } 43 | 44 | verbose!( 45 | cfg, 46 | "program_mode={} num_threads={} color_output={}", 47 | cfg.mode, 48 | rayon::current_num_threads(), 49 | cfg.colorize 50 | ); 51 | 52 | // figure out subcommand to run 53 | match cfg.mode { 54 | ProgramMode::Status => commands::status::do_status(&cfg), 55 | ProgramMode::Enable => commands::enable_disable::do_enable(&cfg), 56 | ProgramMode::Disable => commands::enable_disable::do_disable(&cfg), 57 | ProgramMode::External => commands::external::do_external(&cfg), 58 | } 59 | } 60 | 61 | fn main() { 62 | let ret = do_main(); 63 | 64 | if let Err(err) = ret { 65 | die!(1, "{}: {:?}", Color::Red.paint("error"), err); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/runit.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * Author: Dave Eddy 3 | * Date: January 26, 2022 4 | * License: MIT 5 | */ 6 | 7 | //! Runit service related structs and enums. 8 | 9 | use libc::pid_t; 10 | use path::{Path, PathBuf}; 11 | use std::fs; 12 | use std::io; 13 | use std::path; 14 | use std::time; 15 | 16 | use anyhow::{anyhow, Context, Result}; 17 | 18 | /// Possible states for a runit service. 19 | pub enum RunitServiceState { 20 | Run, 21 | Down, 22 | Finish, 23 | Unknown, 24 | } 25 | 26 | /** 27 | * A runit service. 28 | * 29 | * This struct defines an object that can represent an individual service for 30 | * Runit. 31 | */ 32 | #[derive(Debug, Eq, Ord, PartialEq, PartialOrd)] 33 | pub struct RunitService { 34 | pub path: PathBuf, 35 | pub name: String, 36 | } 37 | 38 | impl RunitService { 39 | /// Create a new runit service object from a given path and name. 40 | pub fn new(name: &str, path: &Path) -> Self { 41 | let name = name.to_string(); 42 | let path = path.to_path_buf(); 43 | Self { path, name } 44 | } 45 | 46 | /// Check if service is valid. 47 | pub fn valid(&self) -> bool { 48 | let p = self.path.join("supervise"); 49 | 50 | p.exists() 51 | } 52 | 53 | /// Check if a service is enabled. 54 | pub fn enabled(&self) -> bool { 55 | // "///down" 56 | let p = self.path.join("down"); 57 | 58 | !p.exists() 59 | } 60 | 61 | /// Enable the service. 62 | pub fn enable(&self) -> Result<()> { 63 | // "///down" 64 | let p = self.path.join("down"); 65 | 66 | if let Err(err) = fs::remove_file(p) { 67 | // allow ENOENT to be considered success as well 68 | match err.kind() { 69 | io::ErrorKind::NotFound => return Ok(()), 70 | _ => return Err(err.into()), 71 | }; 72 | }; 73 | 74 | Ok(()) 75 | } 76 | 77 | /// Disable the service. 78 | pub fn disable(&self) -> Result<()> { 79 | // "///down" 80 | let p = self.path.join("down"); 81 | 82 | fs::File::create(p)?; 83 | 84 | Ok(()) 85 | } 86 | 87 | /// Get the service PID if possible. 88 | pub fn get_pid(&self) -> Result { 89 | // "///supervise/pid" 90 | let p = self.path.join("supervise").join("pid"); 91 | 92 | let pid: pid_t = fs::read_to_string(p)?.trim().parse()?; 93 | 94 | Ok(pid) 95 | } 96 | 97 | /// Get the service state. 98 | pub fn get_state(&self) -> RunitServiceState { 99 | // "///supervise/stat" 100 | let p = self.path.join("supervise").join("stat"); 101 | 102 | let s = 103 | fs::read_to_string(p).unwrap_or_else(|_| String::from("unknown")); 104 | 105 | match s.trim() { 106 | "run" => RunitServiceState::Run, 107 | "down" => RunitServiceState::Down, 108 | "finish" => RunitServiceState::Finish, 109 | _ => RunitServiceState::Unknown, 110 | } 111 | } 112 | 113 | /// Get the service uptime. 114 | pub fn get_start_time(&self) -> Result { 115 | // "///supervise/stat" 116 | let p = self.path.join("supervise").join("stat"); 117 | 118 | Ok(fs::metadata(p)?.modified()?) 119 | } 120 | } 121 | 122 | /** 123 | * List the services in a given runit service directory. 124 | * 125 | * This function optionally allows you to specify the `log` boolean. If set, 126 | * this will return the correponding log service for each base-level service 127 | * found. 128 | * 129 | * You may also specify an optional filter to only allow services that contain a 130 | * given string. 131 | */ 132 | pub fn get_services( 133 | path: &Path, 134 | log: bool, 135 | filter: Option, 136 | ) -> Result> 137 | where 138 | T: AsRef, 139 | { 140 | // loop services directory and collect service names 141 | let mut dirs = Vec::new(); 142 | 143 | for entry in fs::read_dir(path) 144 | .with_context(|| format!("failed to read dir {:?}", path))? 145 | { 146 | let entry = entry?; 147 | let p = entry.path(); 148 | 149 | if !p.is_dir() { 150 | continue; 151 | } 152 | 153 | let name = p 154 | .file_name() 155 | .ok_or_else(|| anyhow!("{:?}: failed to get service name", p))? 156 | .to_str() 157 | .ok_or_else(|| anyhow!("{:?}: failed to parse service name", p))? 158 | .to_string(); 159 | 160 | if let Some(ref filter) = filter { 161 | if !name.contains(filter.as_ref()) { 162 | continue; 163 | } 164 | } 165 | 166 | let service = RunitService::new(&name, &p); 167 | dirs.push(service); 168 | 169 | if log { 170 | let p = entry.path().join("log"); 171 | let name = "- log"; 172 | let service = RunitService::new(name, &p); 173 | dirs.push(service); 174 | } 175 | } 176 | 177 | dirs.sort(); 178 | 179 | Ok(dirs) 180 | } 181 | -------------------------------------------------------------------------------- /src/service.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * Author: Dave Eddy 3 | * Date: January 26, 2022 4 | * License: MIT 5 | */ 6 | 7 | //! Generic service related structs and enums. 8 | 9 | use libc::pid_t; 10 | use std::fmt; 11 | use std::path::Path; 12 | use std::time; 13 | 14 | use anyhow::Result; 15 | use yansi::{Color, Style}; 16 | 17 | use crate::runit::{RunitService, RunitServiceState}; 18 | use crate::utils; 19 | 20 | /// Possible states for a service. 21 | pub enum ServiceState { 22 | Run, 23 | Down, 24 | Finish, 25 | Unknown, 26 | } 27 | 28 | impl ServiceState { 29 | /// Get a suitable `yansi::Style` for the state. 30 | pub fn get_style(&self) -> Style { 31 | let style = Style::default(); 32 | 33 | let color = match self { 34 | ServiceState::Run => Color::Green, 35 | ServiceState::Down => Color::Red, 36 | ServiceState::Finish => Color::Yellow, 37 | ServiceState::Unknown => Color::Yellow, 38 | }; 39 | 40 | style.fg(color) 41 | } 42 | 43 | /// Get a suitable char for the state (as a `String`). 44 | pub fn get_char(&self) -> String { 45 | let s = match self { 46 | ServiceState::Run => "✔", 47 | ServiceState::Down => "X", 48 | ServiceState::Finish => "X", 49 | ServiceState::Unknown => "?", 50 | }; 51 | 52 | s.to_string() 53 | } 54 | } 55 | 56 | impl fmt::Display for ServiceState { 57 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 58 | let s = match self { 59 | ServiceState::Run => "run", 60 | ServiceState::Down => "down", 61 | ServiceState::Finish => "finish", 62 | ServiceState::Unknown => "n/a", 63 | }; 64 | 65 | s.fmt(f) 66 | } 67 | } 68 | 69 | /** 70 | * A struct suitable for describing an abstract service. 71 | * 72 | * This struct itself doesn't do much - it just stores information about a 73 | * service and knows how to format it to look pretty. 74 | */ 75 | pub struct Service { 76 | name: String, 77 | state: ServiceState, 78 | enabled: bool, 79 | command: Option, 80 | pid: Option, 81 | start_time: Result, 82 | pstree: Option>, 83 | } 84 | 85 | impl Service { 86 | /// Create a new service from a `RunitService`. 87 | pub fn from_runit_service( 88 | service: &RunitService, 89 | want_pstree: bool, 90 | proc_path: &Path, 91 | pstree_prog: &str, 92 | ) -> (Self, Vec) { 93 | let mut messages: Vec = vec![]; 94 | let name = service.name.to_string(); 95 | let enabled = service.enabled(); 96 | let pid = service.get_pid(); 97 | let state = service.get_state(); 98 | let start_time = service.get_start_time(); 99 | 100 | let mut command = None; 101 | if let Ok(p) = pid { 102 | match utils::cmd_from_pid(p, proc_path) { 103 | Ok(cmd) => { 104 | command = Some(cmd); 105 | } 106 | Err(err) => { 107 | messages.push(format!( 108 | "{:?}: failed to get command for pid {}: {:?}", 109 | service.path, p, err 110 | )); 111 | } 112 | }; 113 | } 114 | 115 | let pid = match pid { 116 | Ok(pid) => Some(pid), 117 | Err(ref err) => { 118 | messages.push(format!( 119 | "{:?}: failed to get pid: {}", 120 | service.path, err 121 | )); 122 | None 123 | } 124 | }; 125 | 126 | // optionally get pstree. None if the user wants it, Some if the user 127 | // wants it regardless of execution success. 128 | let pstree = if want_pstree { 129 | pid.map(|pid| get_pstree(pid, pstree_prog)) 130 | } else { 131 | None 132 | }; 133 | 134 | let state = match state { 135 | RunitServiceState::Run => ServiceState::Run, 136 | RunitServiceState::Down => ServiceState::Down, 137 | RunitServiceState::Finish => ServiceState::Finish, 138 | RunitServiceState::Unknown => ServiceState::Unknown, 139 | }; 140 | 141 | let svc = 142 | Self { name, state, enabled, command, pid, start_time, pstree }; 143 | 144 | (svc, messages) 145 | } 146 | 147 | /// Format the service name as a string. 148 | fn format_name(&self) -> (String, Style) { 149 | (self.name.to_string(), Style::default()) 150 | } 151 | 152 | /// Format the service char as a string. 153 | fn format_status_char(&self) -> (String, Style) { 154 | (self.state.get_char(), self.state.get_style()) 155 | } 156 | 157 | /// Format the service state as a string. 158 | fn format_state(&self) -> (String, Style) { 159 | (self.state.to_string(), self.state.get_style()) 160 | } 161 | 162 | /// Format the service enabled status as a string. 163 | fn format_enabled(&self) -> (String, Style) { 164 | let style = match self.enabled { 165 | true => Style::default().fg(Color::Green), 166 | false => Style::default().fg(Color::Red), 167 | }; 168 | 169 | let s = self.enabled.to_string(); 170 | 171 | (s, style) 172 | } 173 | 174 | /// Format the service pid as a string. 175 | fn format_pid(&self) -> (String, Style) { 176 | let style = Style::default().fg(Color::Magenta); 177 | 178 | let s = match self.pid { 179 | Some(pid) => pid.to_string(), 180 | None => String::from("---"), 181 | }; 182 | 183 | (s, style) 184 | } 185 | 186 | /// Format the service command a string. 187 | fn format_command(&self) -> (String, Style) { 188 | let style = Style::default().fg(Color::Green); 189 | 190 | let s = match &self.command { 191 | Some(cmd) => cmd.clone(), 192 | None => String::from("---"), 193 | }; 194 | 195 | (s, style) 196 | } 197 | 198 | /// Format the service time as a string. 199 | fn format_time(&self) -> (String, Style) { 200 | let style = Style::default(); 201 | 202 | let time = match &self.start_time { 203 | Ok(time) => time, 204 | Err(err) => return (err.to_string(), style.fg(Color::Red)), 205 | }; 206 | 207 | let t = match time.elapsed() { 208 | Ok(t) => t, 209 | Err(err) => return (err.to_string(), style.fg(Color::Red)), 210 | }; 211 | 212 | let s = utils::relative_duration(&t); 213 | let style = match t.as_secs() { 214 | t if t < 5 => style.fg(Color::Red), 215 | t if t < 30 => style.fg(Color::Yellow), 216 | _ => style.dimmed(), 217 | }; 218 | 219 | (s, style) 220 | } 221 | 222 | /// Format the service `pstree` output as a string. 223 | pub fn format_pstree(&self) -> (String, Style) { 224 | let style = Style::default(); 225 | 226 | let tree = match &self.pstree { 227 | Some(tree) => tree, 228 | None => return ("".into(), style), 229 | }; 230 | 231 | let (tree_s, style) = match tree { 232 | Ok(stdout) => (stdout.trim().into(), style.dimmed()), 233 | Err(err) => { 234 | (format!("pstree call failed: {}", err), style.fg(Color::Red)) 235 | } 236 | }; 237 | 238 | (format!("\n{}\n", tree_s), style) 239 | } 240 | } 241 | 242 | impl fmt::Display for Service { 243 | /// Format the service as a string suitable for output by `vsv`. 244 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 245 | let base = utils::format_status_line( 246 | self.format_status_char(), 247 | self.format_name(), 248 | self.format_state(), 249 | self.format_enabled(), 250 | self.format_pid(), 251 | self.format_command(), 252 | self.format_time(), 253 | ); 254 | 255 | base.fmt(f) 256 | } 257 | } 258 | 259 | /// Get the `pstree` for a given pid. 260 | fn get_pstree(pid: pid_t, pstree_prog: &str) -> Result { 261 | let cmd = pstree_prog.to_string(); 262 | let args = ["-ac".to_string(), pid.to_string()]; 263 | utils::run_program_get_output(&cmd, &args) 264 | } 265 | -------------------------------------------------------------------------------- /src/utils.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * Author: Dave Eddy 3 | * Date: January 26, 2022 4 | * License: MIT 5 | */ 6 | 7 | //! Contains various util functions for vsv. 8 | 9 | use libc::{c_int, pid_t}; 10 | use std::fs; 11 | use std::path::Path; 12 | use std::process::{Command, ExitStatus}; 13 | use std::time::Duration; 14 | 15 | use anyhow::{anyhow, Context, Result}; 16 | use yansi::Style; 17 | 18 | /** 19 | * A `println!()`-like macro that will only print if `-v` is set. 20 | */ 21 | macro_rules! verbose { 22 | ($cfg:expr, $fmt:expr $(, $args:expr )* $(,)? ) => { 23 | if $cfg.verbose > 0 { 24 | let s = format!($fmt $(, $args)*); 25 | eprintln!("> {}", ::yansi::Style::default().dimmed().paint(s)); 26 | } 27 | }; 28 | } 29 | pub(crate) use verbose; 30 | 31 | /** 32 | * Format a status line - made specifically for vsv. 33 | * 34 | * # Example 35 | * ``` 36 | * use yansi::Style; 37 | * let style = Style::default(); 38 | * println!( 39 | * "{}", 40 | * format_status_line( 41 | * ("", style.bold()), 42 | * ("SERVICE", style.bold()), 43 | * ("STATE", style.bold()), 44 | * ("ENABLED", style.bold()), 45 | * ("PID", style.bold()), 46 | * ("COMMAND", style.bold()), 47 | * ("TIME", style.bold()), 48 | * ) 49 | * ); 50 | * ``` 51 | */ 52 | pub fn format_status_line>( 53 | status_char: (T, Style), 54 | name: (T, Style), 55 | state: (T, Style), 56 | enabled: (T, Style), 57 | pid: (T, Style), 58 | command: (T, Style), 59 | time: (T, Style), 60 | ) -> String { 61 | // ( data + style to print, max width, suffix ) 62 | let data = [ 63 | (status_char, 1, ""), 64 | (name, 20, "..."), 65 | (state, 7, "..."), 66 | (enabled, 9, "..."), 67 | (pid, 8, "..."), 68 | (command, 17, "..."), 69 | (time, 0, "..."), 70 | ]; 71 | 72 | let mut line = String::new(); 73 | 74 | for ((text, style), max, suffix) in data { 75 | let column = if max == 0 { 76 | format!(" {}", style.paint(text.as_ref())) 77 | } else { 78 | let text = trim_long_string(text.as_ref(), max, suffix); 79 | format!(" {0:1$}", style.paint(text), max) 80 | }; 81 | 82 | line.push_str(&column); 83 | } 84 | 85 | line 86 | } 87 | 88 | /** 89 | * Get the program name (arg0) for a PID. 90 | * 91 | * # Example 92 | * 93 | * ``` 94 | * use std::path::PathBuf; 95 | * 96 | * let pid = 1; 97 | * let proc_path = PathBuf::from("/proc"); 98 | * let cmd = cmd_from_pid(pid, &proc_path)?; 99 | * println!("pid {} program is {}", pid, cmd); 100 | * ``` 101 | */ 102 | pub fn cmd_from_pid(pid: pid_t, proc_path: &Path) -> Result { 103 | // ///cmdline 104 | let p = proc_path.join(pid.to_string()).join("cmdline"); 105 | 106 | let data = fs::read_to_string(&p) 107 | .with_context(|| format!("failed to read pid file: {:?}", p))?; 108 | 109 | let first = data.split('\0').next(); 110 | 111 | match first { 112 | Some(f) => Ok(f.to_string()), 113 | None => Err(anyhow!("failed to split cmdline data: {:?}", first)), 114 | } 115 | } 116 | 117 | /** 118 | * Run a program and get stdout. 119 | * 120 | * # Example 121 | * 122 | * ``` 123 | * let cmd = "echo"; 124 | * let args = ["hello", "world"]; 125 | * let out = run_program_get_output(&cmd, &args)?; 126 | * println!("stdout is '{}'", out); 127 | * ``` 128 | */ 129 | pub fn run_program_get_output(cmd: &T1, args: &[T2]) -> Result 130 | where 131 | T1: AsRef, 132 | T2: AsRef, 133 | { 134 | let output = make_command(cmd, args).output()?; 135 | 136 | if !output.status.success() { 137 | return Err(anyhow!("program '{}' returned non-zero", cmd.as_ref())); 138 | } 139 | 140 | let stdout = String::from_utf8(output.stdout)?; 141 | 142 | Ok(stdout) 143 | } 144 | 145 | /** 146 | * Run a program and get the exit status. 147 | * 148 | * # Example 149 | * 150 | * ``` 151 | * let cmd = "echo"; 152 | * let args = ["hello", "world"]; 153 | * let c = run_program_get_status(&cmd, &args); 154 | * match c { 155 | * Ok(status) => println!("exited with code: {}", 156 | * status.code().unwrap_or(-1)), 157 | * Err(err) => eprintln!("program failed: {}", err), 158 | * }; 159 | * ``` 160 | */ 161 | pub fn run_program_get_status( 162 | cmd: &T1, 163 | args: &[T2], 164 | ) -> Result 165 | where 166 | T1: AsRef, 167 | T2: AsRef, 168 | { 169 | let p = make_command(cmd, args).status()?; 170 | 171 | Ok(p) 172 | } 173 | 174 | /** 175 | * Create a `std::process::Command` from a given command name and argument 176 | * slice. 177 | * 178 | * # Example 179 | * 180 | * ``` 181 | * let cmd = "echo"; 182 | * let args = ["hello", "world"]; 183 | * let c = make_command(&cmd, &args); 184 | * ``` 185 | */ 186 | fn make_command(cmd: &T1, args: &[T2]) -> Command 187 | where 188 | T1: AsRef, 189 | T2: AsRef, 190 | { 191 | let mut c = Command::new(cmd.as_ref()); 192 | 193 | for arg in args { 194 | c.arg(arg.as_ref()); 195 | } 196 | 197 | c 198 | } 199 | 200 | /** 201 | * Convert a duration to a human-readable string like "5 minutes", "2 hours", 202 | * etc. 203 | * 204 | * # Example 205 | * 206 | * Duration for 5 seconds ago: 207 | * 208 | * ``` 209 | * use std::time::Duration; 210 | * let dur = Duration::new(5, 0); 211 | * assert_eq!(relative_duration(&dur), "5 seconds".to_string()); 212 | * ``` 213 | */ 214 | pub fn relative_duration(t: &Duration) -> String { 215 | let secs = t.as_secs(); 216 | 217 | let v = [ 218 | (secs / 60 / 60 / 24 / 365, "year"), 219 | (secs / 60 / 60 / 24 / 30, "month"), 220 | (secs / 60 / 60 / 24 / 7, "week"), 221 | (secs / 60 / 60 / 24, "day"), 222 | (secs / 60 / 60, "hour"), 223 | (secs / 60, "minute"), 224 | (secs, "second"), 225 | ]; 226 | 227 | let mut plural = ""; 228 | for (num, name) in v { 229 | if num > 1 { 230 | plural = "s" 231 | } 232 | 233 | if num > 0 { 234 | return format!("{} {}{}", num, name, plural); 235 | } 236 | } 237 | 238 | String::from("0 seconds") 239 | } 240 | 241 | /** 242 | * Trim a string to be (at most) a certain number of characters with an 243 | * optional suffix. 244 | * 245 | * # Examples 246 | * 247 | * Trim the string `"hello world"` to be (at most) 8 characters and add 248 | * `"..."`: 249 | * 250 | * ``` 251 | * let s = trim_long_string("hello world", 8, "..."); 252 | * assert_eq!(s, "hello..."); 253 | * ``` 254 | * 255 | * The suffix will only be added if the original string needed to be trimmed: 256 | * 257 | * ``` 258 | * let s = trim_long_string("hello world", 100, "..."); 259 | * assert_eq!(s, "hello world"); 260 | * ``` 261 | */ 262 | pub fn trim_long_string(s: &str, limit: usize, suffix: &str) -> String { 263 | let suffix_len = suffix.len(); 264 | 265 | assert!(limit > suffix_len, "number too small"); 266 | 267 | let len = s.len(); 268 | 269 | // don't do anything if string is smaller than limit 270 | if len < limit { 271 | return s.to_string(); 272 | } 273 | 274 | // make new string (without formatting) 275 | format!( 276 | "{}{}", 277 | s.chars().take(limit - suffix_len).collect::(), 278 | suffix 279 | ) 280 | } 281 | 282 | /** 283 | * Check if the given file descriptor (by number) is a tty. 284 | * 285 | * # Example 286 | * 287 | * Print "hello world" if stdout is a tty: 288 | * 289 | * ``` 290 | * if isatty(1) { 291 | * println!("hello world"); 292 | * } 293 | * ``` 294 | */ 295 | pub fn isatty(fd: c_int) -> bool { 296 | unsafe { libc::isatty(fd) != 0 } 297 | } 298 | 299 | #[cfg(test)] 300 | mod tests { 301 | use super::*; 302 | 303 | #[test] 304 | fn test_run_program_get_output_good_program_exit_success() -> Result<()> { 305 | let cmd = "echo"; 306 | let args = ["hello", "world"]; 307 | let out = run_program_get_output(&cmd, &args)?; 308 | 309 | assert_eq!(out, "hello world\n", "stdout is correct"); 310 | 311 | Ok(()) 312 | } 313 | 314 | #[test] 315 | fn test_run_program_get_output_good_program_exit_failure() -> Result<()> { 316 | let cmd = "false"; 317 | let args: [&str; 0] = []; 318 | let out = run_program_get_output(&cmd, &args); 319 | 320 | assert!(out.is_err(), "program generates an error"); 321 | 322 | Ok(()) 323 | } 324 | 325 | #[test] 326 | fn test_run_program_get_output_bad_program() -> Result<()> { 327 | let cmd = "this-command-should-never-exist---seriously"; 328 | let args: [&str; 0] = []; 329 | let out = run_program_get_output(&cmd, &args); 330 | 331 | assert!(out.is_err(), "program generates an error"); 332 | 333 | Ok(()) 334 | } 335 | 336 | #[test] 337 | fn test_run_program_get_status_good_program_exit_success() -> Result<()> { 338 | let cmd = "true"; 339 | let args: [&str; 0] = []; 340 | let c = run_program_get_status(&cmd, &args)?; 341 | 342 | assert_eq!(c.code().unwrap_or(-1), 0, "program exits successfully"); 343 | 344 | Ok(()) 345 | } 346 | 347 | #[test] 348 | fn test_run_program_get_status_good_program_exit_failure() -> Result<()> { 349 | let cmd = "false"; 350 | let args: [&str; 0] = []; 351 | let c = run_program_get_status(&cmd, &args)?; 352 | 353 | let code = 354 | c.code().ok_or_else(|| anyhow!("failed to get exit code"))?; 355 | 356 | assert_ne!(code, 0, "program exit code is not 0"); 357 | 358 | Ok(()) 359 | } 360 | 361 | #[test] 362 | fn test_run_program_get_status_bad_program() -> Result<()> { 363 | let cmd = "this-command-should-never-exist---seriously"; 364 | let args: [&str; 0] = []; 365 | let c = run_program_get_status(&cmd, &args); 366 | 367 | assert!(c.is_err(), "program generates an error"); 368 | 369 | Ok(()) 370 | } 371 | 372 | #[test] 373 | fn test_isatty_bad_fd() { 374 | let b = isatty(-1); 375 | 376 | assert!(!b, "fd -1 is not a tty"); 377 | } 378 | 379 | #[test] 380 | fn test_relative_durations() { 381 | use std::time::Duration; 382 | 383 | let arr = [ 384 | (0, "0 seconds"), 385 | (3, "3 seconds"), 386 | (3 * 60, "3 minutes"), 387 | (3 * 60 * 60, "3 hours"), 388 | (3 * 60 * 60 * 24, "3 days"), 389 | (3 * 60 * 60 * 24 * 7, "3 weeks"), 390 | (3 * 60 * 60 * 24 * 30, "3 months"), 391 | (3 * 60 * 60 * 24 * 365, "3 years"), 392 | ]; 393 | 394 | for (secs, s) in arr { 395 | let dur = Duration::new(secs, 0); 396 | assert_eq!(relative_duration(&dur), s, "duration mismatch"); 397 | } 398 | } 399 | } 400 | -------------------------------------------------------------------------------- /tests/basic.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * Integration tests for vsv. 3 | * 4 | * Author: Dave Eddy 5 | * Date: February 19, 2022 6 | * License: MIT 7 | */ 8 | 9 | use anyhow::Result; 10 | 11 | mod common; 12 | 13 | #[test] 14 | fn usage() -> Result<()> { 15 | let assert = common::vsv()?.arg("-h").assert(); 16 | 17 | assert.success().stderr(""); 18 | 19 | Ok(()) 20 | } 21 | 22 | #[test] 23 | fn external_success() -> Result<()> { 24 | let mut cmd = common::vsv()?; 25 | 26 | cmd.env("SV_PROG", "true"); 27 | 28 | let assert = cmd.args(&["external", "cmd"]).assert(); 29 | 30 | assert.success(); 31 | 32 | Ok(()) 33 | } 34 | 35 | #[test] 36 | fn external_failure() -> Result<()> { 37 | let mut cmd = common::vsv()?; 38 | 39 | cmd.env("SV_PROG", "false"); 40 | 41 | let assert = cmd.args(&["external", "cmd"]).assert(); 42 | 43 | assert.failure(); 44 | 45 | Ok(()) 46 | } 47 | -------------------------------------------------------------------------------- /tests/common/mod.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * Author: Dave Eddy 3 | * Date: February 20, 2022 4 | * License: MIT 5 | */ 6 | 7 | use anyhow::Result; 8 | use assert_cmd::Command; 9 | 10 | pub fn vsv() -> Result { 11 | let mut cmd = Command::cargo_bin("vsv")?; 12 | 13 | cmd.env_clear(); 14 | 15 | Ok(cmd) 16 | } 17 | -------------------------------------------------------------------------------- /tests/integration.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * Integration tests for vsv. 3 | * 4 | * Author: Dave Eddy 5 | * Date: February 19, 2022 6 | * License: MIT 7 | */ 8 | 9 | use std::fs; 10 | use std::fs::File; 11 | use std::io::Write; 12 | use std::path::{Path, PathBuf}; 13 | use std::str; 14 | 15 | use anyhow::{anyhow, Result}; 16 | use assert_cmd::Command; 17 | 18 | mod common; 19 | 20 | struct Config { 21 | proc_path: PathBuf, 22 | service_path: PathBuf, 23 | } 24 | 25 | fn vsv(cfg: &Config) -> Result { 26 | let mut cmd = common::vsv()?; 27 | 28 | cmd.env("SVDIR", &cfg.service_path); 29 | cmd.env("PROC_DIR", &cfg.proc_path); 30 | 31 | Ok(cmd) 32 | } 33 | 34 | fn write_file(fname: &Path, contents: &str) -> Result<()> { 35 | let mut f = File::create(fname)?; 36 | write!(f, "{}", contents)?; 37 | 38 | Ok(()) 39 | } 40 | 41 | fn get_tmp_path() -> PathBuf { 42 | PathBuf::from(env!("CARGO_TARGET_TMPDIR")).join("tests") 43 | } 44 | 45 | fn parse_status_line(line: &str) -> Result> { 46 | let mut vec: Vec<&str> = vec![]; 47 | let mut chars = line.chars().map(|c| c.len_utf8()); 48 | 49 | let lengths = [1, 20, 7, 9, 8, 17]; 50 | 51 | // skip the first space char 52 | let mut start = 0; 53 | start += chars.next().ok_or(anyhow!("first char must be a space"))?; 54 | assert_eq!(start, 1, "first char should always be 1 byte (space)"); 55 | 56 | for num in lengths { 57 | let mut end = start; 58 | 59 | for _ in 0..num { 60 | end += chars.next().ok_or(anyhow!("not enough chars in line"))?; 61 | } 62 | 63 | vec.push(&line[start..end]); 64 | 65 | let space = chars 66 | .next() 67 | .ok_or(anyhow!("next field should have a space char"))?; 68 | 69 | assert_eq!(space, 1, "should be space character"); 70 | 71 | start = end + space; 72 | } 73 | 74 | vec.push(&line[start..]); 75 | 76 | Ok(vec) 77 | } 78 | 79 | fn parse_status_output(s: &str) -> Result>> { 80 | let mut lines: Vec> = vec![]; 81 | 82 | let spl: Vec<&str> = s.lines().collect(); 83 | let len = spl.len(); 84 | 85 | for (i, line) in spl.iter().enumerate() { 86 | // remove first and last line of output (blank lines) 87 | if i == 0 || i == (len - 1) { 88 | assert!(line.is_empty(), "first and last line should be empty"); 89 | continue; 90 | } 91 | 92 | let items = parse_status_line(line)?; 93 | lines.push(items); 94 | } 95 | 96 | assert!( 97 | !lines.is_empty(), 98 | "status must have at least one line (the header)" 99 | ); 100 | 101 | // check header 102 | let header = lines.remove(0); 103 | let good_header = 104 | &["", "SERVICE", "STATE", "ENABLED", "PID", "COMMAND", "TIME"]; 105 | 106 | for (i, good_item) in good_header.iter().enumerate() { 107 | assert_eq!(&header[i].trim_end(), good_item, "check header field"); 108 | } 109 | 110 | Ok(lines) 111 | } 112 | 113 | fn create_service( 114 | cfg: &Config, 115 | name: &str, 116 | state: &str, 117 | pid: Option<&str>, 118 | log_pid: Option<&str>, 119 | ) -> Result<()> { 120 | let svc_dir = cfg.service_path.join(name); 121 | let dirs = [("cmd", &svc_dir, pid), ("log", &svc_dir.join("log"), log_pid)]; 122 | 123 | for (s, dir, pid) in dirs { 124 | let supervise_dir = dir.join("supervise"); 125 | let stat_file = supervise_dir.join("stat"); 126 | 127 | fs::create_dir(&dir)?; 128 | fs::create_dir(&supervise_dir)?; 129 | fs::write(&stat_file, format!("{}\n", state))?; 130 | 131 | // write pid and proc info if supplied 132 | if let Some(pid) = pid { 133 | let proc_pid_dir = cfg.proc_path.join(pid); 134 | let pid_file = supervise_dir.join("pid"); 135 | let cmd_file = proc_pid_dir.join("cmdline"); 136 | 137 | fs::create_dir(&proc_pid_dir)?; 138 | fs::write(&pid_file, format!("{}\n", pid))?; 139 | fs::write(&cmd_file, format!("{}-{}\0", name, s))?; 140 | } 141 | } 142 | 143 | Ok(()) 144 | } 145 | 146 | fn remove_service( 147 | cfg: &Config, 148 | name: &str, 149 | pid: Option<&str>, 150 | log_pid: Option<&str>, 151 | ) -> Result<()> { 152 | let svc_dir = cfg.service_path.join(name); 153 | fs::remove_dir_all(&svc_dir)?; 154 | 155 | for pid in [pid, log_pid].into_iter().flatten() { 156 | let proc_pid_dir = cfg.proc_path.join(pid); 157 | fs::remove_dir_all(&proc_pid_dir)?; 158 | } 159 | 160 | Ok(()) 161 | } 162 | 163 | //fn compare_output(have: &Vec>, want: &Vec>) { 164 | fn compare_output(have: &[Vec<&str>], want: &[&[&str; 6]]) { 165 | println!("compare_output\nhave = '{:?}'\nwant = '{:?}'", have, want); 166 | 167 | assert_eq!(have.len(), want.len(), "status lines not same length"); 168 | 169 | // loop each line of output 170 | for (i, have_items) in have.iter().enumerate() { 171 | let line_no = i + 1; 172 | let want_items = want[i]; 173 | 174 | // loop each field in the line 175 | for (j, want_item) in want_items.iter().enumerate() { 176 | let field_no = j + 1; 177 | let have_item = &have_items[j].trim_end(); 178 | 179 | println!( 180 | "line {} field {}: checking '{}' == '{}'", 181 | line_no, field_no, have_item, want_item 182 | ); 183 | 184 | // compare the fields to each other 185 | assert_eq!( 186 | have_item, want_item, 187 | "line {} field {} incorrect", 188 | line_no, field_no 189 | ); 190 | } 191 | } 192 | 193 | println!("output the same\n"); 194 | } 195 | 196 | fn run_command_compare_output( 197 | cmd: &mut Command, 198 | want: &[&[&str; 6]], 199 | ) -> Result<()> { 200 | let assert = cmd.assert().success(); 201 | let output = assert.get_output(); 202 | let stdout = str::from_utf8(&output.stdout)?; 203 | let status = parse_status_output(stdout)?; 204 | 205 | compare_output(&status, want); 206 | 207 | Ok(()) 208 | } 209 | 210 | #[test] 211 | fn full_synthetic_test() -> Result<()> { 212 | let tmp_path = get_tmp_path(); 213 | 214 | let cfg = Config { 215 | proc_path: tmp_path.join("proc"), 216 | service_path: tmp_path.join("service"), 217 | }; 218 | 219 | // create the vsv command to use for all tests 220 | let mut status_cmd = vsv(&cfg)?; 221 | let mut status_cmd_l = vsv(&cfg)?; 222 | status_cmd_l.arg("status").arg("-l"); 223 | 224 | // start fresh by removing the service and proc paths 225 | let _ = fs::remove_dir_all(&tmp_path); 226 | 227 | // vsv should fail when the service dir doesn't exist 228 | status_cmd.assert().failure(); 229 | 230 | // create test dirs 231 | for p in [&tmp_path, &cfg.proc_path, &cfg.service_path] { 232 | fs::create_dir(p)?; 233 | } 234 | 235 | // test no services 236 | let want: &[&[&str; 6]] = &[]; 237 | run_command_compare_output(&mut status_cmd, want)?; 238 | 239 | // test service 240 | create_service(&cfg, "foo", "run", Some("123"), None)?; 241 | let want = &[&["✔", "foo", "run", "true", "123", "foo-cmd"]]; 242 | run_command_compare_output(&mut status_cmd, want)?; 243 | 244 | // test another service 245 | create_service(&cfg, "bar", "run", Some("234"), None)?; 246 | let want = &[ 247 | &["✔", "bar", "run", "true", "234", "bar-cmd"], 248 | &["✔", "foo", "run", "true", "123", "foo-cmd"], 249 | ]; 250 | run_command_compare_output(&mut status_cmd, want)?; 251 | 252 | // test service no pid 253 | create_service(&cfg, "baz", "run", None, None)?; 254 | let want = &[ 255 | &["✔", "bar", "run", "true", "234", "bar-cmd"], 256 | &["✔", "baz", "run", "true", "---", "---"], 257 | &["✔", "foo", "run", "true", "123", "foo-cmd"], 258 | ]; 259 | run_command_compare_output(&mut status_cmd, want)?; 260 | 261 | // test service bad pid 262 | create_service( 263 | &cfg, 264 | "bat", 265 | "run", 266 | Some("uh oh this one won't parse"), 267 | None, 268 | )?; 269 | let want = &[ 270 | &["✔", "bar", "run", "true", "234", "bar-cmd"], 271 | &["✔", "bat", "run", "true", "---", "---"], 272 | &["✔", "baz", "run", "true", "---", "---"], 273 | &["✔", "foo", "run", "true", "123", "foo-cmd"], 274 | ]; 275 | run_command_compare_output(&mut status_cmd, want)?; 276 | 277 | // remove services 278 | remove_service(&cfg, "bar", Some("234"), None)?; 279 | remove_service(&cfg, "bat", None, None)?; 280 | remove_service(&cfg, "baz", None, None)?; 281 | let want = &[&["✔", "foo", "run", "true", "123", "foo-cmd"]]; 282 | run_command_compare_output(&mut status_cmd, want)?; 283 | 284 | // add down service 285 | create_service(&cfg, "bar", "down", None, None)?; 286 | let want = &[ 287 | &["X", "bar", "down", "true", "---", "---"], 288 | &["✔", "foo", "run", "true", "123", "foo-cmd"], 289 | ]; 290 | run_command_compare_output(&mut status_cmd, want)?; 291 | 292 | // add unknown state service 293 | create_service(&cfg, "bat", "something-bad", None, None)?; 294 | let want = &[ 295 | &["X", "bar", "down", "true", "---", "---"], 296 | &["?", "bat", "n/a", "true", "---", "---"], 297 | &["✔", "foo", "run", "true", "123", "foo-cmd"], 298 | ]; 299 | run_command_compare_output(&mut status_cmd, want)?; 300 | 301 | // add long service name 302 | create_service( 303 | &cfg, 304 | "some-really-long-service-name", 305 | "run", 306 | Some("1"), 307 | None, 308 | )?; 309 | let want = &[ 310 | &["X", "bar", "down", "true", "---", "---"], 311 | &["?", "bat", "n/a", "true", "---", "---"], 312 | &["✔", "foo", "run", "true", "123", "foo-cmd"], 313 | &["✔", "some-really-long-...", "run", "true", "1", "some-really-lo..."], 314 | ]; 315 | run_command_compare_output(&mut status_cmd, want)?; 316 | 317 | // remove services 318 | remove_service(&cfg, "some-really-long-service-name", Some("1"), None)?; 319 | remove_service(&cfg, "bar", None, None)?; 320 | remove_service(&cfg, "bat", None, None)?; 321 | let want = &[&["✔", "foo", "run", "true", "123", "foo-cmd"]]; 322 | run_command_compare_output(&mut status_cmd, want)?; 323 | 324 | // add some more services 325 | create_service(&cfg, "bar", "run", Some("234"), None)?; 326 | create_service(&cfg, "baz", "run", Some("345"), None)?; 327 | create_service(&cfg, "bat", "run", Some("456"), None)?; 328 | 329 | // test disable 330 | let mut cmd = vsv(&cfg)?; 331 | cmd.args(&["disable", "bar", "baz"]).assert().success(); 332 | 333 | let want = &[ 334 | &["✔", "bar", "run", "false", "234", "bar-cmd"], 335 | &["✔", "bat", "run", "true", "456", "bat-cmd"], 336 | &["✔", "baz", "run", "false", "345", "baz-cmd"], 337 | &["✔", "foo", "run", "true", "123", "foo-cmd"], 338 | ]; 339 | run_command_compare_output(&mut status_cmd, want)?; 340 | 341 | // test enable 342 | let mut cmd = vsv(&cfg)?; 343 | cmd.args(&["enable", "foo", "bar"]).assert().success(); 344 | let want = &[ 345 | &["✔", "bar", "run", "true", "234", "bar-cmd"], 346 | &["✔", "bat", "run", "true", "456", "bat-cmd"], 347 | &["✔", "baz", "run", "false", "345", "baz-cmd"], 348 | &["✔", "foo", "run", "true", "123", "foo-cmd"], 349 | ]; 350 | run_command_compare_output(&mut status_cmd, want)?; 351 | 352 | // test bad disable 353 | let mut cmd = vsv(&cfg)?; 354 | cmd.args(&["disable", "fake-service", "foo"]).assert().failure(); 355 | let want = &[ 356 | &["✔", "bar", "run", "true", "234", "bar-cmd"], 357 | &["✔", "bat", "run", "true", "456", "bat-cmd"], 358 | &["✔", "baz", "run", "false", "345", "baz-cmd"], 359 | &["✔", "foo", "run", "false", "123", "foo-cmd"], 360 | ]; 361 | run_command_compare_output(&mut status_cmd, want)?; 362 | 363 | // test bad enable 364 | let mut cmd = vsv(&cfg)?; 365 | cmd.args(&["enable", "fake-service", "foo"]).assert().failure(); 366 | let want = &[ 367 | &["✔", "bar", "run", "true", "234", "bar-cmd"], 368 | &["✔", "bat", "run", "true", "456", "bat-cmd"], 369 | &["✔", "baz", "run", "false", "345", "baz-cmd"], 370 | &["✔", "foo", "run", "true", "123", "foo-cmd"], 371 | ]; 372 | run_command_compare_output(&mut status_cmd, want)?; 373 | 374 | // remove all services 375 | remove_service(&cfg, "foo", Some("123"), None)?; 376 | remove_service(&cfg, "bar", Some("234"), None)?; 377 | remove_service(&cfg, "baz", Some("345"), None)?; 378 | remove_service(&cfg, "bat", Some("456"), None)?; 379 | let want: &[&[&str; 6]] = &[]; 380 | run_command_compare_output(&mut status_cmd, want)?; 381 | 382 | // create a service with a logger function 383 | create_service(&cfg, "foo", "run", Some("100"), Some("150"))?; 384 | let want = &[ 385 | &["✔", "foo", "run", "true", "100", "foo-cmd"], 386 | &["✔", "- log", "run", "true", "150", "foo-log"], 387 | ]; 388 | run_command_compare_output(&mut status_cmd_l, want)?; 389 | 390 | // disable logger only 391 | let mut cmd = vsv(&cfg)?; 392 | cmd.args(&["disable", "foo/log"]).assert().success(); 393 | let want = &[ 394 | &["✔", "foo", "run", "true", "100", "foo-cmd"], 395 | &["✔", "- log", "run", "false", "150", "foo-log"], 396 | ]; 397 | run_command_compare_output(&mut status_cmd_l, want)?; 398 | 399 | // manually create a bad service (just a file) 400 | let dir = cfg.service_path.join("not-a-dir"); 401 | write_file(&dir, "whatever")?; 402 | let want = &[&["✔", "foo", "run", "true", "100", "foo-cmd"]]; 403 | run_command_compare_output(&mut status_cmd, want)?; 404 | 405 | // manually create an unknown service (just a dir) 406 | let dir = cfg.service_path.join("just-a-dir"); 407 | fs::create_dir(dir)?; 408 | let want = &[ 409 | &["✔", "foo", "run", "true", "100", "foo-cmd"], 410 | &["?", "just-a-dir", "n/a", "true", "---", "---"], 411 | ]; 412 | run_command_compare_output(&mut status_cmd, want)?; 413 | 414 | // create services and use a filter 415 | create_service(&cfg, "test-1", "run", Some("1"), None)?; 416 | create_service(&cfg, "test-2", "run", Some("2"), None)?; 417 | create_service(&cfg, "test-3", "run", Some("3"), None)?; 418 | let mut cmd = vsv(&cfg)?; 419 | cmd.args(&["status", "test"]).assert().success(); 420 | let want = &[ 421 | &["✔", "test-1", "run", "true", "1", "test-1-cmd"], 422 | &["✔", "test-2", "run", "true", "2", "test-2-cmd"], 423 | &["✔", "test-3", "run", "true", "3", "test-3-cmd"], 424 | ]; 425 | run_command_compare_output(&mut cmd, want)?; 426 | 427 | // status mode should work without "status" when -t or -l is supplied 428 | let mut cmd = vsv(&cfg)?; 429 | cmd.args(&["-l", "test"]).assert().success(); 430 | let want = &[ 431 | &["✔", "test-1", "run", "true", "1", "test-1-cmd"], 432 | &["✔", "- log", "run", "true", "---", "---"], 433 | &["✔", "test-2", "run", "true", "2", "test-2-cmd"], 434 | &["✔", "- log", "run", "true", "---", "---"], 435 | &["✔", "test-3", "run", "true", "3", "test-3-cmd"], 436 | &["✔", "- log", "run", "true", "---", "---"], 437 | ]; 438 | run_command_compare_output(&mut cmd, want)?; 439 | 440 | Ok(()) 441 | } 442 | --------------------------------------------------------------------------------