diff --git a/Cargo.lock b/Cargo.lock index 934190d..023737e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -47,7 +47,7 @@ version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ - "windows-sys", + "windows-sys 0.61.2", ] [[package]] @@ -58,7 +58,7 @@ checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", "once_cell_polyfill", - "windows-sys", + "windows-sys 0.61.2", ] [[package]] @@ -67,6 +67,40 @@ version = "1.0.100" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" +[[package]] +name = "async-broadcast" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532" +dependencies = [ + "event-listener", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-recursion" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "async-trait" +version = "0.1.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "autocfg" version = "1.5.0" @@ -103,6 +137,18 @@ dependencies = [ "objc2", ] +[[package]] +name = "bumpalo" +version = "3.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" + +[[package]] +name = "bytes" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" + [[package]] name = "cfg-if" version = "1.0.4" @@ -117,9 +163,9 @@ checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" [[package]] name = "clap" -version = "4.5.55" +version = "4.5.57" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e34525d5bbbd55da2bb745d34b36121baac88d07619a9a09cfcf4a6c0832785" +checksum = "6899ea499e3fb9305a65d5ebf6e3d2248c5fab291f300ad0a704fbe142eae31a" dependencies = [ "clap_builder", "clap_derive", @@ -137,9 +183,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.55" +version = "4.5.57" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59a20016a20a3da95bef50ec7238dbd09baeef4311dcdd38ec15aba69812fb61" +checksum = "7b12c8b680195a62a8364d16b8447b01b6c2c8f9aaf68bee653be34d4245e238" dependencies = [ "anstream", "anstyle", @@ -190,6 +236,21 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + [[package]] name = "ctrlc" version = "3.5.1" @@ -198,7 +259,7 @@ checksum = "73736a89c4aff73035ba2ed2e565061954da00d4970fc9ac25dcc85a2a20d790" dependencies = [ "dispatch2", "nix 0.30.1", - "windows-sys", + "windows-sys 0.61.2", ] [[package]] @@ -213,6 +274,33 @@ dependencies = [ "objc2", ] +[[package]] +name = "endi" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66b7e2430c6dff6a955451e2cfc438f09cea1965a9d6f87f7e3b90decc014099" + +[[package]] +name = "enumflags2" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1027f7680c853e056ebcec683615fb6fbbc07dbaa13b4d5d9442b146ded4ecef" +dependencies = [ + "enumflags2_derive", + "serde", +] + +[[package]] +name = "enumflags2_derive" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "env_filter" version = "0.1.4" @@ -249,7 +337,28 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "event-listener" +version = "5.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "event-listener-strategy" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" +dependencies = [ + "event-listener", + "pin-project-lite", ] [[package]] @@ -264,6 +373,31 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-io" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" + +[[package]] +name = "futures-lite" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad" +dependencies = [ + "fastrand", + "futures-core", + "futures-io", + "parking", + "pin-project-lite", +] + [[package]] name = "getrandom" version = "0.3.4" @@ -294,6 +428,12 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + [[package]] name = "indexmap" version = "2.13.0" @@ -312,7 +452,7 @@ checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46" dependencies = [ "hermit-abi", "libc", - "windows-sys", + "windows-sys 0.61.2", ] [[package]] @@ -345,6 +485,16 @@ dependencies = [ "syn", ] +[[package]] +name = "js-sys" +version = "0.3.85" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c942ebf8e95485ca0d52d97da7c5a2c387d0e7f0ba4c35e93bfcaee045955b3" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + [[package]] name = "libc" version = "0.2.180" @@ -369,6 +519,26 @@ version = "2.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "mio" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + [[package]] name = "nix" version = "0.30.1" @@ -439,17 +609,39 @@ version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" +[[package]] +name = "ordered-stream" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50" +dependencies = [ + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + [[package]] name = "portable-atomic" -version = "1.13.0" +version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f89776e4d69bb58bc6993e99ffa1d11f228b839984854c7daeb5d37f87cbe950" +checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" [[package]] name = "portable-atomic-util" -version = "0.2.4" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507" +checksum = "7a9db96d7fa8782dd8c15ce32ffe8680bbd1e978a43bf51a34d39483540495f5" dependencies = [ "portable-atomic", ] @@ -463,6 +655,15 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "proc-macro-crate" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983" +dependencies = [ + "toml_edit", +] + [[package]] name = "proc-macro2" version = "1.0.106" @@ -474,9 +675,9 @@ dependencies = [ [[package]] name = "proptest" -version = "1.9.0" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bee689443a2bd0a16ab0348b52ee43e3b2d1b1f931c8aa5c9f8de4c86fbe8c40" +checksum = "37566cb3fdacef14c0737f9546df7cfeadbfbc9fef10991038bf5015d0c80532" dependencies = [ "bit-set", "bit-vec", @@ -552,9 +753,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.12.2" +version = "1.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" dependencies = [ "aho-corasick", "memchr", @@ -564,9 +765,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.13" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" dependencies = [ "aho-corasick", "memchr", @@ -575,9 +776,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.8" +version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" +checksum = "a96887878f22d7bad8a3b6dc5b7440e0ada9a245242924394987b21cf2210a4c" [[package]] name = "rustix" @@ -589,9 +790,15 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys", + "windows-sys 0.61.2", ] +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + [[package]] name = "rusty-fork" version = "0.3.1" @@ -634,6 +841,17 @@ dependencies = [ "syn", ] +[[package]] +name = "serde_repr" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "serde_spanned" version = "1.0.4" @@ -643,6 +861,26 @@ dependencies = [ "serde_core", ] +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "socket2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86f4aa3ad99f2088c990dfa82d367e19cb29268ed67c574d10d0a4bfe71f07e0" +dependencies = [ + "libc", + "windows-sys 0.60.2", +] + [[package]] name = "strsim" version = "0.11.1" @@ -670,7 +908,35 @@ dependencies = [ "getrandom", "once_cell", "rustix", - "windows-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio" +version = "1.49.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86" +dependencies = [ + "bytes", + "libc", + "mio", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "tracing", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" +dependencies = [ + "proc-macro2", + "quote", + "syn", ] [[package]] @@ -697,6 +963,18 @@ dependencies = [ "serde_core", ] +[[package]] +name = "toml_edit" +version = "0.23.10+spec-1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84c8b9f757e028cee9fa244aea147aab2a9ec09d5325a9b01e0a49730c2b5269" +dependencies = [ + "indexmap", + "toml_datetime", + "toml_parser", + "winnow", +] + [[package]] name = "toml_parser" version = "1.0.6+spec-1.1.0" @@ -712,6 +990,48 @@ version = "1.0.6+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ab16f14aed21ee8bfd8ec22513f7287cd4a91aa92e44edfe2c17ddd004e92607" +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "uds_windows" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89daebc3e6fd160ac4aa9fc8b3bf71e1f74fbf92367ae71fb83a037e8bf164b9" +dependencies = [ + "memoffset", + "tempfile", + "winapi", +] + [[package]] name = "unarray" version = "0.1.4" @@ -730,6 +1050,17 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" +[[package]] +name = "uuid" +version = "1.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee48d38b119b0cd71fe4141b30f5ba9c7c5d9f4e7a3a8b4a674e4b6ef789976f" +dependencies = [ + "js-sys", + "serde_core", + "wasm-bindgen", +] + [[package]] name = "wait-timeout" version = "0.2.1" @@ -739,6 +1070,12 @@ dependencies = [ "libc", ] +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + [[package]] name = "wasip2" version = "1.0.2+wasi-0.2.9" @@ -748,11 +1085,57 @@ dependencies = [ "wit-bindgen", ] +[[package]] +name = "wasm-bindgen" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64024a30ec1e37399cf85a7ffefebdb72205ca1c972291c51512360d90bd8566" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "008b239d9c740232e71bd39e8ef6429d27097518b6b30bdf9086833bd5b6d608" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5256bae2d58f54820e6490f9839c49780dff84c65aeab9e772f15d5f0e913a55" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f01b580c9ac74c8d8f0c0e4afb04eeef2acf145458e52c03845ee9cd23e3d12" +dependencies = [ + "unicode-ident", +] + [[package]] name = "watt" version = "1.0.0" dependencies = [ "anyhow", + "async-trait", "clap", "clap-verbosity-flag", "ctrlc", @@ -762,16 +1145,49 @@ dependencies = [ "num_cpus", "proptest", "serde", + "tokio", "toml", "yansi", + "zbus", ] +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + [[package]] name = "windows-link" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets", +] + [[package]] name = "windows-sys" version = "0.61.2" @@ -781,11 +1197,79 @@ dependencies = [ "windows-link", ] +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + [[package]] name = "winnow" version = "0.7.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" +dependencies = [ + "memchr", +] [[package]] name = "wit-bindgen" @@ -812,22 +1296,118 @@ dependencies = [ "is-terminal", ] +[[package]] +name = "zbus" +version = "5.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfeff997a0aaa3eb20c4652baf788d2dfa6d2839a0ead0b3ff69ce2f9c4bdd1" +dependencies = [ + "async-broadcast", + "async-recursion", + "async-trait", + "enumflags2", + "event-listener", + "futures-core", + "futures-lite", + "hex", + "libc", + "ordered-stream", + "rustix", + "serde", + "serde_repr", + "tokio", + "tracing", + "uds_windows", + "uuid", + "windows-sys 0.61.2", + "winnow", + "zbus_macros", + "zbus_names", + "zvariant", +] + +[[package]] +name = "zbus_macros" +version = "5.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bbd5a90dbe8feee5b13def448427ae314ccd26a49cac47905cafefb9ff846f1" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn", + "zbus_names", + "zvariant", + "zvariant_utils", +] + +[[package]] +name = "zbus_names" +version = "4.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffd8af6d5b78619bab301ff3c560a5bd22426150253db278f164d6cf3b72c50f" +dependencies = [ + "serde", + "winnow", + "zvariant", +] + [[package]] name = "zerocopy" -version = "0.8.35" +version = "0.8.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdea86ddd5568519879b8187e1cf04e24fce28f7fe046ceecbce472ff19a2572" +checksum = "57cf3aa6855b23711ee9852dfc97dfaa51c45feaba5b645d0c777414d494a961" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.35" +version = "0.8.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a616990af1a287837c4fe6596ad77ef57948f787e46ce28e166facc0cc1cb75" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zvariant" +version = "5.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68b64ef4f40c7951337ddc7023dd03528a57a3ce3408ee9da5e948bd29b232c4" +dependencies = [ + "endi", + "enumflags2", + "serde", + "winnow", + "zvariant_derive", + "zvariant_utils", +] + +[[package]] +name = "zvariant_derive" +version = "5.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c15e1b46eff7c6c91195752e0eeed8ef040e391cdece7c25376957d5f15df22" +checksum = "484d5d975eb7afb52cc6b929c13d3719a20ad650fea4120e6310de3fc55e415c" dependencies = [ + "proc-macro-crate", "proc-macro2", "quote", "syn", + "zvariant_utils", +] + +[[package]] +name = "zvariant_utils" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f75c23a64ef8f40f13a6989991e643554d9bef1d682a281160cf0c1bc389c5e9" +dependencies = [ + "proc-macro2", + "quote", + "serde", + "syn", + "winnow", ] diff --git a/Cargo.toml b/Cargo.toml index 857719f..b4807bc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,7 +9,7 @@ edition = "2024" # K license = "MPL-2.0" readme = true repository = "https://github.com/notashelf/watt" -rust-version = "1.88" +rust-version = "1.91.1" version = "1.0.0" [workspace.dependencies] @@ -26,3 +26,6 @@ num_cpus = "1.17.0" serde = { features = [ "derive" ], version = "1.0.228" } toml = "0.9.8" yansi = { features = [ "detect-env", "detect-tty" ], version = "1.0.1" } +zbus = { version = "5.13.2", default-features = false, features = [ "tokio" ] } +tokio = { version = "1.49.0", features = [ "rt-multi-thread", "macros", "signal", "time" ] } +async-trait = "0.1.89" diff --git a/dbus/net.hadess.PowerProfiles.conf b/dbus/net.hadess.PowerProfiles.conf new file mode 100644 index 0000000..e1395b8 --- /dev/null +++ b/dbus/net.hadess.PowerProfiles.conf @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + diff --git a/watt/Cargo.toml b/watt/Cargo.toml index 7371e78..2633d70 100644 --- a/watt/Cargo.toml +++ b/watt/Cargo.toml @@ -25,6 +25,9 @@ num_cpus.workspace = true serde.workspace = true toml.workspace = true yansi.workspace = true +zbus.workspace = true +tokio.workspace = true +async-trait.workspace = true [dev-dependencies] proptest = "1.9.0" diff --git a/watt/config.rs b/watt/config.rs index aa1148d..bc42cc6 100644 --- a/watt/config.rs +++ b/watt/config.rs @@ -395,6 +395,7 @@ mod expression { named!(power_supply_discharge_rate => "%power-supply-discharge-rate"); named!(discharging => "?discharging"); + named!(power_profile_preference => "$power-profile-preference"); } #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] @@ -458,6 +459,9 @@ pub enum Expression { #[serde(with = "expression::discharging")] Discharging, + #[serde(with = "expression::power_profile_preference")] + PowerProfilePreference, + Boolean(bool), Number(f64), @@ -621,6 +625,8 @@ pub struct EvalState<'peripherals, 'context> { pub discharging: bool, + pub power_profile_preference: crate::profile::PowerProfile, + pub context: EvalContext<'context>, pub cpus: &'peripherals HashSet>, @@ -758,6 +764,10 @@ impl Expression { Discharging => Boolean(state.discharging), + PowerProfilePreference => { + String(state.power_profile_preference.as_str().to_owned()) + }, + literal @ (Boolean(_) | Number(_) | String(_)) => literal.clone(), List(items) => { @@ -1050,6 +1060,7 @@ mod tests { power_supply_charge: Some(0.8), power_supply_discharge_rate: Some(10.0), discharging: false, + power_profile_preference: crate::profile::PowerProfile::Balanced, context: EvalContext::Cpu(&cpu), cpus: &cpus, power_supplies: &power_supplies, @@ -1134,6 +1145,7 @@ mod tests { power_supply_charge: Some(0.8), power_supply_discharge_rate: Some(10.0), discharging: false, + power_profile_preference: crate::profile::PowerProfile::Balanced, context: EvalContext::Cpu(&cpu), cpus: &cpus, power_supplies: &power_supplies, diff --git a/watt/cpu.rs b/watt/cpu.rs index facd28c..4655418 100644 --- a/watt/cpu.rs +++ b/watt/cpu.rs @@ -174,7 +174,11 @@ impl Cpu { self.has_cpufreq = fs::exists(format!("/sys/devices/system/cpu/cpu{number}/cpufreq")); - log::trace!("CPU {number} has cpufreq: {has_cpufreq}", number = self.number, has_cpufreq = self.has_cpufreq); + log::trace!( + "CPU {number} has cpufreq: {has_cpufreq}", + number = self.number, + has_cpufreq = self.has_cpufreq + ); if self.has_cpufreq { self.scan_governor()?; @@ -459,7 +463,10 @@ impl Cpu { self.governor = Some(governor.to_owned()); - log::info!("CPU {number} governor set to {governor}", number = self.number); + log::info!( + "CPU {number} governor set to {governor}", + number = self.number + ); Ok(()) } diff --git a/watt/dbus/mod.rs b/watt/dbus/mod.rs new file mode 100644 index 0000000..0f90bda --- /dev/null +++ b/watt/dbus/mod.rs @@ -0,0 +1,3 @@ +pub mod ppd; +pub mod server; +pub mod watt; diff --git a/watt/dbus/ppd.rs b/watt/dbus/ppd.rs new file mode 100644 index 0000000..b16dae1 --- /dev/null +++ b/watt/dbus/ppd.rs @@ -0,0 +1,178 @@ +use std::{ + collections::HashMap, + sync::Arc, +}; + +use tokio::sync::RwLock; +use zbus::{ + fdo, + interface, + object_server::SignalEmitter, + zvariant::Value, +}; + +use crate::{ + profile::{ + PowerProfile, + ProfileHold, + }, + system::DaemonState, +}; + +pub struct PowerProfilesInterface { + state: Arc>, +} + +impl PowerProfilesInterface { + pub fn new(state: Arc>) -> Self { + Self { state } + } +} + +#[interface(name = "net.hadess.PowerProfiles")] +impl PowerProfilesInterface { + // Properties + #[zbus(property)] + async fn active_profile(&self) -> String { + let state = self.state.read().await; + state.profile.get_effective_profile().as_str().to_owned() + } + + #[zbus(property)] + async fn set_active_profile(&self, profile: &str) -> zbus::Result<()> { + let profile = match PowerProfile::from_str(profile) { + Some(profile) => profile, + None => { + return Err(zbus::Error::from(fdo::Error::InvalidArgs(format!( + "invalid profile: {profile}, valid: performance, balanced, \ + power-saver" + )))); + }, + }; + + let mut state = self.state.write().await; + state.profile.set_preference(profile); + + log::info!( + "D-Bus: active profile set to {profile}", + profile = profile.as_str() + ); + + Ok(()) + } + + #[zbus(property)] + async fn profiles(&self) -> Vec>> { + PowerProfile::all() + .iter() + .map(|profile| { + let mut map = HashMap::new(); + map.insert("Profile".to_owned(), Value::from(profile.as_str())); + map.insert("Driver".to_owned(), Value::from("watt")); + map.insert("CpuDriver".to_owned(), Value::from("unknown")); + map + }) + .collect() + } + + #[zbus(property)] + async fn actions(&self) -> Vec { + Vec::new() + } + + #[zbus(property)] + async fn performance_degraded(&self) -> String { + let state = self.state.read().await; + state.performance_degraded.clone().unwrap_or_default() + } + + #[zbus(property)] + async fn performance_inhibited(&self) -> String { + let state = self.state.read().await; + match state.profile.get_holds().first() { + Some(hold) => hold.reason.clone(), + None => String::new(), + } + } + + #[zbus(property)] + async fn active_profile_holds(&self) -> Vec>> { + let state = self.state.read().await; + state + .profile + .get_holds() + .into_iter() + .map(|hold: ProfileHold| { + let mut map = HashMap::new(); + map.insert("Profile".to_owned(), Value::from(hold.profile.as_str())); + map.insert("Reason".to_owned(), Value::from(hold.reason)); + map + .insert("ApplicationId".to_owned(), Value::from(hold.application_id)); + map + }) + .collect() + } + + async fn hold_profile( + &self, + #[zbus(signal_emitter)] signal_emitter: SignalEmitter<'_>, + profile: String, + reason: String, + application_id: String, + ) -> fdo::Result { + let profile = match PowerProfile::from_str(&profile) { + Some(profile) => profile, + None => { + return Err(fdo::Error::InvalidArgs(format!( + "invalid profile: {profile}" + ))); + }, + }; + + let mut state = self.state.write().await; + let cookie = state.profile.add_hold(profile, reason, application_id); + + log::info!("D-Bus profile hold added, cookie={cookie}"); + + // Emit property change signals + drop(state); // release lock before emitting signals + + // Log signal failures but don't fail the operation. + // State was already mutated. + if let Err(e) = self.active_profile_holds_changed(&signal_emitter).await { + log::warn!("failed to emit ActiveProfileHolds change signal: {e}"); + } + + if let Err(e) = self.active_profile_changed(&signal_emitter).await { + log::warn!("failed to emit ActiveProfile change signal: {e}"); + } + + Ok(cookie) + } + + async fn release_profile( + &self, + #[zbus(signal_emitter)] signal_emitter: SignalEmitter<'_>, + cookie: u32, + ) -> fdo::Result<()> { + let mut state = self.state.write().await; + state + .profile + .release_hold(cookie) + .map_err(|error| fdo::Error::Failed(error.to_string()))?; + + log::info!("D-Bus profile hold released, cookie={cookie}"); + + drop(state); + + if let Err(e) = self.active_profile_holds_changed(&signal_emitter).await { + log::warn!("Failed to emit ActiveProfileHolds change signal: {e}"); + } + + if let Err(e) = self.active_profile_changed(&signal_emitter).await { + log::warn!("Failed to emit ActiveProfile change signal: {e}"); + } + + Ok(()) + } +} diff --git a/watt/dbus/server.rs b/watt/dbus/server.rs new file mode 100644 index 0000000..0fbee9b --- /dev/null +++ b/watt/dbus/server.rs @@ -0,0 +1,59 @@ +use std::{ + future, + sync::Arc, + time::Duration, +}; + +use tokio::sync::RwLock; +use zbus::connection; + +use crate::system::DaemonState; + +pub async fn start_dbus_server( + state: Arc>, +) -> zbus::Result<()> { + log::info!("starting D-Bus server..."); + + let mut attempt: u32 = 0; + loop { + match try_start(state.clone()).await { + Ok(()) => return Ok(()), + Err(e) => { + attempt += 1; + log::error!("D-Bus server error on attempt {attempt}: {e}"); + + if attempt >= 5 { + log::error!("D-Bus server failed after {attempt} attempts, bailing"); + return Err(e); + } + + let delay = Duration::from_secs(2 * attempt as u64); + log::info!( + "retrying D-Bus in {delay_secs}s", + delay_secs = delay.as_secs() + ); + tokio::time::sleep(delay).await; + }, + } + } +} + +async fn try_start(state: Arc>) -> zbus::Result<()> { + let ppd = crate::dbus::ppd::PowerProfilesInterface::new(state.clone()); + let watt = crate::dbus::watt::WattInterface::new(state); + + let _connection = connection::Builder::system()? + .name("net.hadess.PowerProfiles")? + .name("dev.notashelf.Watt")? + .serve_at("/net/hadess/PowerProfiles", ppd)? + .serve_at("/dev/notashelf/Watt", watt)? + .build() + .await?; + + log::info!("D-Bus server started"); + + // Block forever to keep the D-Bus server alive + loop { + future::pending::<()>().await; + } +} diff --git a/watt/dbus/watt.rs b/watt/dbus/watt.rs new file mode 100644 index 0000000..3584231 --- /dev/null +++ b/watt/dbus/watt.rs @@ -0,0 +1,69 @@ +use std::{ + collections::HashMap, + sync::Arc, +}; + +use tokio::sync::RwLock; +use zbus::{ + interface, + zvariant::Value, +}; + +use crate::system::DaemonState; + +pub struct WattInterface { + state: Arc>, +} + +impl WattInterface { + pub fn new(state: Arc>) -> Self { + Self { state } + } +} + +#[interface(name = "dev.notashelf.Watt")] +impl WattInterface { + #[zbus(property)] + async fn version(&self) -> String { + env!("CARGO_PKG_VERSION").to_owned() + } + + #[zbus(property)] + async fn rule_count(&self) -> u32 { + let state = self.state.read().await; + state.config.rules.len() as u32 + } + + #[zbus(property)] + async fn cpu_count(&self) -> u32 { + let state = self.state.read().await; + state.system.cpus.len() as u32 + } + + async fn get_status(&self) -> HashMap> { + let state = self.state.read().await; + let mut status = HashMap::new(); + + if let Some(log) = state.system.cpu_log.back() { + status.insert("cpu-usage".to_owned(), Value::from(log.usage * 100.0)); + status.insert("cpu-temperature".to_owned(), Value::from(log.temperature)); + } + + status.insert( + "profile".to_owned(), + Value::from(String::from(state.profile.get_effective_profile().as_str())), + ); + + status.insert( + "is-discharging".to_owned(), + Value::from(state.system.is_discharging()), + ); + + status + } + + async fn get_applied_rules(&self) -> Vec { + let state = self.state.read().await; + state.last_applied_rules.clone() + } +} diff --git a/watt/lib.rs b/watt/lib.rs index 0feb389..bcbdeed 100644 --- a/watt/lib.rs +++ b/watt/lib.rs @@ -2,6 +2,7 @@ use std::path::PathBuf; use anyhow::Context as _; use clap::Parser as _; +use tokio::runtime::Builder as RuntimeBuilder; pub mod cpu; pub mod power_supply; @@ -13,6 +14,9 @@ pub mod config; pub mod lock; +pub mod dbus; +pub mod profile; + #[derive(clap::Parser, Debug)] #[command(version, about)] pub struct Cli { @@ -43,5 +47,10 @@ pub fn main() -> anyhow::Result<()> { let lock_path = PathBuf::from("/run/watt/lock"); let _lock = lock::LockFile::acquire(&lock_path)?; - system::run_daemon(config) + let runtime = RuntimeBuilder::new_multi_thread() + .enable_all() + .build() + .context("failed to build tokio runtime")?; + + runtime.block_on(system::run_daemon(config)) } diff --git a/watt/power_supply.rs b/watt/power_supply.rs index 3af8c3d..bb9bf13 100644 --- a/watt/power_supply.rs +++ b/watt/power_supply.rs @@ -197,7 +197,11 @@ impl PowerSupply { self.is_from_peripheral = 'is_from_peripheral: { let name_lower = self.name.to_lowercase(); - log::trace!("power supply '{name}' type: {type_}", name = self.name, type_ = self.type_); + log::trace!( + "power supply '{name}' type: {type_}", + name = self.name, + type_ = self.type_ + ); // Common peripheral battery names. if name_lower.contains("mouse") diff --git a/watt/profile.rs b/watt/profile.rs new file mode 100644 index 0000000..fbb08f2 --- /dev/null +++ b/watt/profile.rs @@ -0,0 +1,146 @@ +use std::{ + collections::HashMap, + time::Instant, +}; + +use serde::{ + Deserialize, + Serialize, +}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[serde(rename_all = "kebab-case")] +pub enum PowerProfile { + Performance, + Balanced, + PowerSaver, +} + +impl PowerProfile { + pub fn as_str(&self) -> &'static str { + match self { + Self::Performance => "performance", + Self::Balanced => "balanced", + Self::PowerSaver => "power-saver", + } + } + + // FIXME: change this to a less ambigious name + pub fn from_str(value: &str) -> Option { + match value { + "performance" => Some(Self::Performance), + "balanced" => Some(Self::Balanced), + "power-saver" => Some(Self::PowerSaver), + _ => None, + } + } + + pub fn all() -> [Self; 3] { + [Self::Performance, Self::Balanced, Self::PowerSaver] + } +} + +#[derive(Debug, Clone)] +pub struct ProfileHold { + pub cookie: u32, + pub profile: PowerProfile, + pub reason: String, + pub application_id: String, + pub timestamp: Instant, +} + +#[derive(Debug)] +pub struct ProfileState { + preferred_profile: PowerProfile, + holds: HashMap, + next_cookie: u32, +} + +impl ProfileState { + pub fn new() -> Self { + Self { + preferred_profile: PowerProfile::Balanced, + holds: HashMap::new(), + next_cookie: 1, + } + } + + pub fn add_hold( + &mut self, + profile: PowerProfile, + reason: String, + application_id: String, + ) -> u32 { + // Find an unused cookie, we'll want to handle wrap-around collision + let mut cookie = self.next_cookie; + while self.holds.contains_key(&cookie) { + cookie = cookie.wrapping_add(1); + } + self.next_cookie = cookie.wrapping_add(1); + + let hold = ProfileHold { + cookie, + profile, + reason, + application_id, + timestamp: Instant::now(), + }; + + log::info!( + "profile hold added: cookie={cookie}, profile={profile:?}, app={app}", + app = hold.application_id, + ); + + self.holds.insert(cookie, hold); + cookie + } + + pub fn release_hold(&mut self, cookie: u32) -> anyhow::Result<()> { + self + .holds + .remove(&cookie) + .ok_or_else(|| anyhow::anyhow!("hold with cookie {cookie} not found"))?; + + log::info!("profile hold released: cookie={cookie}"); + Ok(()) + } + + pub fn set_preference(&mut self, profile: PowerProfile) { + if self.preferred_profile != profile { + log::info!( + "profile preference changed: {old:?} -> {new:?}", + old = self.preferred_profile, + new = profile, + ); + self.preferred_profile = profile; + } + } + + pub fn get_preference(&self) -> PowerProfile { + self.preferred_profile + } + + pub fn get_effective_profile(&self) -> PowerProfile { + for profile in [ + PowerProfile::Performance, + PowerProfile::Balanced, + PowerProfile::PowerSaver, + ] { + if self.holds.values().any(|hold| hold.profile == profile) { + return profile; + } + } + + self.preferred_profile + } + + pub fn get_holds(&self) -> Vec { + self.holds.values().cloned().collect() + } +} + +impl Default for ProfileState { + fn default() -> Self { + Self::new() + } +} diff --git a/watt/system.rs b/watt/system.rs index a57f8bb..602cb8d 100644 --- a/watt/system.rs +++ b/watt/system.rs @@ -6,14 +6,7 @@ use std::{ }, mem, path::Path, - sync::{ - Arc, - atomic::{ - AtomicBool, - Ordering, - }, - }, - thread, + sync::Arc, time::{ Duration, Instant, @@ -24,23 +17,28 @@ use anyhow::{ Context, bail, }; +use tokio::{ + signal, + sync::RwLock, +}; use crate::{ config, cpu, fs, power_supply, + profile, }; #[derive(Debug)] -struct CpuLog { - at: Instant, +pub struct CpuLog { + pub at: Instant, /// CPU usage between 0-1, a percentage. - usage: f64, + pub usage: f64, /// CPU temperature in celsius. - temperature: f64, + pub temperature: f64, } #[derive(Debug)] @@ -51,7 +49,7 @@ struct CpuVolatility { } #[derive(Debug)] -struct PowerSupplyLog { +pub struct PowerSupplyLog { at: Instant, /// Charge 0-1, as a percentage. @@ -59,23 +57,25 @@ struct PowerSupplyLog { } #[derive(Default, Debug)] -struct System { - is_ac: bool, +pub struct System { + pub is_ac: bool, - load_average_1min: f64, - load_average_5min: f64, - load_average_15min: f64, + pub load_average_1min: f64, + pub load_average_5min: f64, + pub load_average_15min: f64, /// All CPUs. - cpus: HashSet>, + pub cpus: HashSet>, + /// CPU usage and temperature log. - cpu_log: VecDeque, - cpu_temperatures: HashMap, + pub cpu_log: VecDeque, + pub cpu_temperatures: HashMap, /// All power supplies. - power_supplies: HashSet>, + pub power_supplies: HashSet>, + /// Power supply status log. - power_supply_log: VecDeque, + pub power_supply_log: VecDeque, } impl System { @@ -574,7 +574,7 @@ impl System { .is_none_or(|volatility| volatility.usage < 0.05) } - fn is_discharging(&self) -> bool { + pub fn is_discharging(&self) -> bool { self.power_supplies.iter().any(|power_supply| { power_supply.charge_state.as_deref() == Some("Discharging") }) @@ -624,6 +624,15 @@ impl System { } } +#[derive(Debug)] +pub struct DaemonState { + pub system: System, + pub config: config::DaemonConfig, + pub profile: profile::ProfileState, + pub last_applied_rules: Vec, + pub performance_degraded: Option, +} + /// Calculate the idle time multiplier based on system idle time. /// /// Returns a multiplier between 1.0 and 5.0: @@ -647,281 +656,331 @@ fn idle_multiplier(idle_for: Duration) -> f64 { (1.0 + factor).clamp(1.0, 5.0) } -pub fn run_daemon(config: config::DaemonConfig) -> anyhow::Result<()> { - assert!(config.rules.is_sorted_by_key(|rule| rule.priority)); +fn compute_poll_delay( + system: &System, + last_polling_delay: Option, + last_user_activity: Instant, +) -> Duration { + let mut delay = Duration::from_secs(5); + + if system.is_discharging() { + match system.power_supply_discharge_rate() { + Some(discharge_rate) => { + if discharge_rate > 0.2 { + delay *= 3; + } else if discharge_rate > 0.1 { + delay *= 2; + } else { + delay /= 2; + delay *= 3; + } + }, - log::info!("starting daemon..."); + None => delay *= 2, + } + } - let cancelled = Arc::new(AtomicBool::new(false)); + if system.is_cpu_idle() { + let idle_for = last_user_activity.elapsed(); - { - log::debug!("setting ctrl-c handler..."); - ctrlc::set_handler({ - let cancelled = Arc::clone(&cancelled); + if idle_for > Duration::from_secs(30) { + let factor = idle_multiplier(idle_for); - move || { - log::info!("received shutdown signal"); - cancelled.store(true, Ordering::SeqCst); - } - }) - .context("failed to set ctrl-c handler")?; + log::debug!( + "system has been idle for {seconds} seconds (approx {minutes} \ + minutes), applying idle factor: {factor:.2}x", + seconds = idle_for.as_secs(), + minutes = idle_for.as_secs() / 60, + ); + + delay = Duration::from_secs_f64(delay.as_secs_f64() * factor); + } } - let mut system = System::default(); - let mut last_polling_delay = None::; - // TODO: Set this somewhere. - let last_user_activity = Instant::now(); + if let Some(volatility) = system.cpu_volatility() + && (volatility.usage > 0.1 || volatility.temperature > 0.02) + { + delay = (delay / 2).max(Duration::from_secs(1)); + } - while !cancelled.load(Ordering::SeqCst) { - log::debug!("starting main polling loop iteration"); + let delay = match last_polling_delay { + Some(last_delay) => { + Duration::from_secs_f64( + delay.as_secs_f64() * 0.3 + last_delay.as_secs_f64() * 0.7, + ) + }, - system.scan()?; - - let delay = { - let mut delay = Duration::from_secs(5); - - // We are on battery, so we must be more conservative with our polling. - if system.is_discharging() { - match system.power_supply_discharge_rate() { - Some(discharge_rate) => { - if discharge_rate > 0.2 { - delay *= 3; - } else if discharge_rate > 0.1 { - delay *= 2; - } else { - // *= 1.5; - delay /= 2; - delay *= 3; - } - }, + None => delay, + }; - // If we can't determine the discharge rate, that means that - // we were very recently started. Which is user activity. - None => { - delay *= 2; - }, - } - } + Duration::from_secs_f64(delay.as_secs_f64().clamp(1.0, 30.0)) +} - if system.is_cpu_idle() { - let idle_for = last_user_activity.elapsed(); +fn detect_performance_degradation(_system: &System) -> Option { + // TODO: implement temperature or throttling based detection. + None +} - if idle_for > Duration::from_secs(30) { - let factor = idle_multiplier(idle_for); +pub async fn run_daemon(config: config::DaemonConfig) -> anyhow::Result<()> { + if !config.rules.is_sorted_by_key(|rule| rule.priority) { + bail!("daemon config rules must be sorted by priority"); + } - log::debug!( - "system has been idle for {seconds} seconds (approx {minutes} \ - minutes), applying idle factor: {factor:.2}x", - seconds = idle_for.as_secs(), - minutes = idle_for.as_secs() / 60, - ); + log::info!("starting daemon..."); - delay = Duration::from_secs_f64(delay.as_secs_f64() * factor); - } - } + let state = Arc::new(RwLock::new(DaemonState { + system: System::default(), + config, + profile: profile::ProfileState::new(), + last_applied_rules: Vec::new(), + performance_degraded: None, + })); - if let Some(volatility) = system.cpu_volatility() - && (volatility.usage > 0.1 || volatility.temperature > 0.02) + { + let dbus_state = Arc::clone(&state); + tokio::spawn(async move { + if let Err(error) = + crate::dbus::server::start_dbus_server(dbus_state).await { - delay = (delay / 2).max(Duration::from_secs(1)); + log::error!("D-Bus server exited with error: {error}"); } + }); + } - let delay = match last_polling_delay { - Some(last_delay) => { - Duration::from_secs_f64( - // 30% of current computed delay, 70% of last delay. - delay.as_secs_f64() * 0.3 + last_delay.as_secs_f64() * 0.7, - ) - }, - - None => delay, - }; + let mut last_polling_delay = None::; + // TODO: Track user activity events instead of initializing at start. + let last_user_activity = Instant::now(); + let shutdown_signal = signal::ctrl_c(); + tokio::pin!(shutdown_signal); - let delay = Duration::from_secs_f64(delay.as_secs_f64().clamp(1.0, 30.0)); + // Initial delay before first poll + let mut sleep_for = Duration::from_secs(0); - last_polling_delay = Some(delay); + loop { + // Wait for either shutdown signal or the next poll time + tokio::select! { + _ = &mut shutdown_signal => { + log::info!("received shutdown signal"); + break; + } + _ = tokio::time::sleep(sleep_for) => { + // Continue with polling iteration + } + } - delay - }; - log::info!( - "next poll will be in {seconds} seconds or {minutes} minutes, possibly \ - delayed if application of rules takes more than the polling delay", - seconds = delay.as_secs_f64(), - minutes = delay.as_secs_f64() / 60.0, - ); + log::debug!("starting main polling loop iteration"); + let start = Instant::now(); - log::info!("filtering rules and applying them..."); + let delay; - let start = Instant::now(); + { + let mut guard = state.write().await; + + guard.system.scan()?; + + guard.performance_degraded = + detect_performance_degradation(&guard.system); + + let eval_state = config::EvalState { + frequency_available: guard + .system + .cpus + .iter() + .any(|cpu| cpu.frequency_mhz.is_some()), + turbo_available: cpu::Cpu::turbo() + .context( + "failed to read CPU turbo boost status for `is-turbo-available`", + )? + .is_some(), + + cpu_usage: guard + .system + .cpu_log + .back() + .context("CPU log is empty")? + .usage, + cpu_usage_volatility: guard + .system + .cpu_volatility() + .map(|vol| vol.usage), + cpu_temperature: guard + .system + .cpu_log + .back() + .map(|log| log.temperature), + cpu_temperature_volatility: guard + .system + .cpu_volatility() + .map(|vol| vol.temperature), + cpu_idle_seconds: last_user_activity.elapsed().as_secs_f64(), + cpu_frequency_maximum: cpu::Cpu::hardware_frequency_mhz_maximum() + .context("failed to read CPU hardware maximum frequency")? + .map(|u64| u64 as f64), + cpu_frequency_minimum: cpu::Cpu::hardware_frequency_mhz_minimum() + .context("failed to read CPU hardware minimum frequency")? + .map(|u64| u64 as f64), + + power_supply_charge: guard + .system + .power_supply_log + .back() + .map(|log| log.charge), + power_supply_discharge_rate: guard.system.power_supply_discharge_rate(), + + discharging: guard.system.is_discharging(), + + power_profile_preference: guard.profile.get_effective_profile(), + + context: config::EvalContext::WidestPossible, + + cpus: &guard.system.cpus, + power_supplies: &guard.system.power_supplies, + }; - let state = config::EvalState { - frequency_available: system + let mut cpu_deltas: HashMap, cpu::Delta> = guard + .system .cpus .iter() - .any(|cpu| cpu.frequency_mhz.is_some()), - turbo_available: cpu::Cpu::turbo() - .context( - "failed to read CPU turbo boost status for `is-turbo-available`", - )? - .is_some(), - - cpu_usage: system - .cpu_log - .back() - .context("CPU log is empty")? - .usage, - cpu_usage_volatility: system.cpu_volatility().map(|vol| vol.usage), - cpu_temperature: system - .cpu_log - .back() - .map(|log| log.temperature), - cpu_temperature_volatility: system - .cpu_volatility() - .map(|vol| vol.temperature), - cpu_idle_seconds: last_user_activity.elapsed().as_secs_f64(), - cpu_frequency_maximum: cpu::Cpu::hardware_frequency_mhz_maximum() - .context("failed to read CPU hardware maximum frequency")? - .map(|u64| u64 as f64), - cpu_frequency_minimum: cpu::Cpu::hardware_frequency_mhz_minimum() - .context("failed to read CPU hardware minimum frequency")? - .map(|u64| u64 as f64), - - power_supply_charge: system - .power_supply_log - .back() - .map(|log| log.charge), - power_supply_discharge_rate: system.power_supply_discharge_rate(), - - discharging: system.is_discharging(), - - context: config::EvalContext::WidestPossible, - - cpus: &system.cpus, - power_supplies: &system.power_supplies, - }; + .map(|cpu| (Arc::clone(cpu), cpu::Delta::default())) + .collect(); + let mut cpu_turbo: Option = None; + + let mut power_deltas: HashMap< + Arc, + power_supply::Delta, + > = guard + .system + .power_supplies + .iter() + .map(|power_supply| { + (Arc::clone(power_supply), power_supply::Delta::default()) + }) + .collect(); + let mut power_platform_profile: Option = None; - let mut cpu_deltas: HashMap, cpu::Delta> = system - .cpus - .iter() - .map(|cpu| (Arc::clone(cpu), cpu::Delta::default())) - .collect(); - let mut cpu_turbo: Option = None; + let mut last_applied_rules = Vec::new(); - let mut power_deltas: HashMap< - Arc, - power_supply::Delta, - > = system - .power_supplies - .iter() - .map(|power_supply| { - (Arc::clone(power_supply), power_supply::Delta::default()) - }) - .collect(); - let mut power_platform_profile: Option = None; + for rule in guard.config.rules.iter().rev() { + let Some(condition) = rule.condition.eval(&eval_state)? else { + continue; + }; - // Higher priority rule first, so we can short-circuit. - for rule in config.rules.iter().rev() { - let Some(condition) = rule.condition.eval(&state)? else { - continue; - }; + let condition = condition + .try_into_boolean() + .context("`if` was not a boolean")?; - let condition = condition - .try_into_boolean() - .context("`if` was not a boolean")?; + if condition { + log::info!( + "rule '{name}' condition evaluated to true! evaluating members...", + name = rule.name, + ); - if condition { - log::info!( - "rule '{name}' condition evaluated to true! evaluating members...", - name = rule.name, - ); + last_applied_rules.push(rule.name.clone()); - let cpu_some = { - let (cpu_deltas_lo, cpu_turbo_lo) = rule.cpu.eval(&state)?; + let cpu_some = { + let (cpu_deltas_lo, cpu_turbo_lo) = rule.cpu.eval(&eval_state)?; - let deltas_some = cpu_deltas.iter_mut().all(|(cpu, delta)| { - let delta_lo = cpu_deltas_lo - .get(cpu) - .expect("cpu deltas and cpus should match"); + let deltas_some = cpu_deltas.iter_mut().all(|(cpu, delta)| { + let delta_lo = cpu_deltas_lo + .get(cpu) + .expect("cpu deltas and cpus should match"); - *delta = mem::take(delta).or(delta_lo); + *delta = mem::take(delta).or(delta_lo); - delta.is_some() - }); + delta.is_some() + }); - cpu_turbo = cpu_turbo.or(cpu_turbo_lo); + cpu_turbo = cpu_turbo.or(cpu_turbo_lo); - deltas_some && cpu_turbo.is_some() - }; + deltas_some && cpu_turbo.is_some() + }; - let power_some = { - let (power_deltas_lo, power_platform_profile_lo) = - rule.power.eval(&state)?; + let power_some = { + let (power_deltas_lo, power_platform_profile_lo) = + rule.power.eval(&eval_state)?; - let deltas_some = power_deltas.iter_mut().all(|(power, delta)| { - let delta_lo = power_deltas_lo - .get(power) - .expect("power deltas and power supplies should match"); + let deltas_some = power_deltas.iter_mut().all(|(power, delta)| { + let delta_lo = power_deltas_lo + .get(power) + .expect("power deltas and power supplies should match"); - *delta = mem::take(delta).or(delta_lo); + *delta = mem::take(delta).or(delta_lo); - delta.is_some() - }); + delta.is_some() + }); - power_platform_profile = - power_platform_profile.or(power_platform_profile_lo); + power_platform_profile = + power_platform_profile.or(power_platform_profile_lo); - deltas_some && power_platform_profile.is_some() - }; + deltas_some && power_platform_profile.is_some() + }; - if cpu_some && power_some { - log::debug!( - "got a full delta from rules, short circuting evaluation" - ); - break; + if cpu_some && power_some { + log::debug!( + "got a full delta from rules, short circuting evaluation" + ); + break; + } } } - } - for (cpu, delta) in &cpu_deltas { - delta - .apply(&mut (**cpu).clone()) - .with_context(|| format!("failed to apply delta to {cpu}"))?; - } + for (cpu, delta) in &cpu_deltas { + delta + .apply(&mut (**cpu).clone()) + .with_context(|| format!("failed to apply delta to {cpu}"))?; + } - log::info!("applying CPU deltas to {len} CPUs", len = cpu_deltas.len()); + log::info!("applying CPU deltas to {len} CPUs", len = cpu_deltas.len()); - if let Some(turbo) = cpu_turbo { - cpu::Cpu::set_turbo(turbo, cpu_deltas.keys().map(|arc| &**arc)) - .context("failed to set CPU turbo")?; - } + if let Some(turbo) = cpu_turbo { + cpu::Cpu::set_turbo(turbo, cpu_deltas.keys().map(|arc| &**arc)) + .context("failed to set CPU turbo")?; + } - log::info!( - "applying power supply deltas to {len} devices", - len = power_deltas.len(), - ); + log::info!( + "applying power supply deltas to {len} devices", + len = power_deltas.len(), + ); - for (power, delta) in power_deltas { - delta - .apply(&mut (*power).clone()) - .with_context(|| format!("failed to apply delta to {power}"))?; - } + for (power, delta) in power_deltas { + delta + .apply(&mut (*power).clone()) + .with_context(|| format!("failed to apply delta to {power}"))?; + } - if let Some(platform_profile) = power_platform_profile { - power_supply::PowerSupply::set_platform_profile(&platform_profile) - .context("failed to set power supply platform profile")?; + if let Some(platform_profile) = power_platform_profile { + power_supply::PowerSupply::set_platform_profile(&platform_profile) + .context("failed to set power supply platform profile")?; + } + + delay = compute_poll_delay( + &guard.system, + last_polling_delay, + last_user_activity, + ); + guard.last_applied_rules = last_applied_rules; + last_polling_delay = Some(delay); + + let elapsed = start.elapsed(); + log::info!( + "filtered and applied rules in {seconds} seconds or {minutes} minutes", + seconds = elapsed.as_secs_f64(), + minutes = elapsed.as_secs_f64() / 60.0, + ); } - let elapsed = start.elapsed(); log::info!( - "filtered and applied rules in {seconds} seconds or {minutes} minutes", - seconds = elapsed.as_secs_f64(), - minutes = elapsed.as_secs_f64() / 60.0, + "next poll will be in {seconds} seconds or {minutes} minutes, possibly \ + delayed if application of rules takes more than the polling delay", + seconds = delay.as_secs_f64(), + minutes = delay.as_secs_f64() / 60.0, ); - thread::sleep(delay.saturating_sub(elapsed)); + let elapsed = start.elapsed(); + sleep_for = delay.saturating_sub(elapsed); } - log::info!("stopping polling loop and thus .."); + log::info!("stopping polling loop and shutting down"); Ok(()) }