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(())
}