From 83e9de9bcbb63f3be4da959282446bfc2011fdef Mon Sep 17 00:00:00 2001 From: Vicki Pfau Date: Fri, 22 Mar 2024 19:21:03 -0700 Subject: [PATCH] Merge steamos-workerd in --- .gitignore | 1 + .gitlab-ci.yml | 13 + Cargo.lock | 244 +++++++-------- Cargo.toml | 11 +- data/steamosmanager.service | 8 + src/ds_inhibit.rs | 601 ++++++++++++++++++++++++++++++++++++ src/main.rs | 236 +++++++++++++- src/sls/ftrace.rs | 286 +++++++++++++++++ src/sls/mod.rs | 137 ++++++++ src/testing.rs | 47 +++ 10 files changed, 1445 insertions(+), 139 deletions(-) create mode 100644 .gitlab-ci.yml create mode 100644 src/ds_inhibit.rs create mode 100644 src/sls/ftrace.rs create mode 100644 src/sls/mod.rs create mode 100644 src/testing.rs diff --git a/.gitignore b/.gitignore index ea8c4bf..4e8c2af 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ /target +/tarpaulin-report.html diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 0000000..cf26d42 --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,13 @@ +stages: + - test + +image: ${CI_DEPENDENCY_PROXY_GROUP_IMAGE_PREFIX}/archlinux:base-devel + +test: + stage: test + tags: + - docker + - linux + script: + - pacman -Sy --noconfirm --needed dbus rust + - dbus-run-session cargo test diff --git a/Cargo.lock b/Cargo.lock index e8716d1..6e57bfb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -19,9 +19,9 @@ checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" [[package]] name = "aho-corasick" -version = "1.0.5" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c378d78423fdad8089616f827526ee33c19f2fddbd5de1629152c9593ba4783" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" dependencies = [ "memchr", ] @@ -57,31 +57,6 @@ dependencies = [ "pin-project-lite", ] -[[package]] -name = "async-executor" -version = "1.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17ae5ebefcc48e7452b4987947920dac9450be1110cadf34d1b8c116bdbaf97c" -dependencies = [ - "async-lock 3.3.0", - "async-task", - "concurrent-queue", - "fastrand", - "futures-lite", - "slab", -] - -[[package]] -name = "async-fs" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc19683171f287921f2405677dd2ed2549c3b3bda697a563ebc3a121ace2aba1" -dependencies = [ - "async-lock 3.3.0", - "blocking", - "futures-lite", -] - [[package]] name = "async-io" version = "2.3.2" @@ -141,9 +116,9 @@ dependencies = [ [[package]] name = "async-recursion" -version = "1.0.5" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fd55a5ba1179988837d24ab4c7cc8ed6efdeff578ede0416b4225a5fca35bd0" +checksum = "30c5ef0ede93efbf733c1a727f3b6b5a1060bbedd5600183e66f6e4be4af0ec5" dependencies = [ "proc-macro2", "quote", @@ -187,9 +162,9 @@ dependencies = [ [[package]] name = "atomic-waker" -version = "1.1.1" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1181e1e0d1fce796a03db1ae795d67167da795f9cf4a39c37589e85ef57f26d3" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" [[package]] name = "autocfg" @@ -199,9 +174,9 @@ checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" [[package]] name = "backtrace" -version = "0.3.69" +version = "0.3.71" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837" +checksum = "26b05800d2e817c8b3b4b54abd461726265fa9789ae34330622f2db9ee696f9d" dependencies = [ "addr2line", "cc", @@ -220,9 +195,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.4.0" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4682ae6287fcf752ecaabbfcc7b6f9b72aa33933dc23a554d853aea8eea8635" +checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" [[package]] name = "block-buffer" @@ -251,18 +226,15 @@ dependencies = [ [[package]] name = "bytes" -version = "1.5.0" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223" +checksum = "514de17de45fdb8dc022b1a7975556c53c86f9f0aa5f534b98977b171857c2c9" [[package]] name = "cc" -version = "1.0.83" +version = "1.0.90" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" -dependencies = [ - "libc", -] +checksum = "8cd6604a82acf3039f1144f54b8eb34e91ffba622051189e71b781822d5ee1f5" [[package]] name = "cfg-if" @@ -278,30 +250,27 @@ checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e" [[package]] name = "concurrent-queue" -version = "2.2.0" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62ec6771ecfa0762d24683ee5a32ad78487a3d3afdc0fb8cae19d2c5deb50b7c" +checksum = "d16048cd947b08fa32c24458a22f5dc5e835264f689f4f5653210c69fd107363" dependencies = [ "crossbeam-utils", ] [[package]] name = "cpufeatures" -version = "0.2.9" +version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a17b76ff3a4162b0b27f354a0c87015ddad39d35f9c0c36607a3bdd175dde1f1" +checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504" dependencies = [ "libc", ] [[package]] name = "crossbeam-utils" -version = "0.8.16" +version = "0.8.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a22b2d63d4d1dc0b7f1b6b2747dd0088008a9be28b6ddf0b1e7d335e3037294" -dependencies = [ - "cfg-if", -] +checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345" [[package]] name = "crypto-common" @@ -427,9 +396,9 @@ dependencies = [ [[package]] name = "fastrand" -version = "2.0.0" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6999dc1837253364c2ebb0704ba97994bd874e8f195d665c50b7548f6ea92764" +checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5" [[package]] name = "futures-core" @@ -445,17 +414,15 @@ checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" [[package]] name = "futures-lite" -version = "2.0.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c1155db57329dca6d018b61e76b1488ce9a2e5e44028cac420a5898f4fcef63" +checksum = "52527eb5074e35e9339c6b4e8d12600c7128b68fb25dcb9fa9dec18f7c25f3a5" dependencies = [ "fastrand", "futures-core", "futures-io", - "memchr", "parking", "pin-project-lite", - "waker-fn", ] [[package]] @@ -477,10 +444,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" dependencies = [ "futures-core", - "futures-io", "futures-sink", "futures-task", - "memchr", "pin-project-lite", "pin-utils", "slab", @@ -498,9 +463,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.10" +version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427" +checksum = "190092ea657667030ac6a35e305e62fc4dd69fd98ac98631e5d3a2b1575a12b5" dependencies = [ "cfg-if", "libc", @@ -509,21 +474,21 @@ dependencies = [ [[package]] name = "gimli" -version = "0.28.0" +version = "0.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6fb8d784f27acf97159b40fc4db5ecd8aa23b9ad5ef69cdd136d3bc80665f0c0" +checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" [[package]] name = "hashbrown" -version = "0.14.0" +version = "0.14.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c6201b9ff9fd90a5a3bac2e56a830d0caa509576f0e503818ee82c181b3437a" +checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" [[package]] name = "hermit-abi" -version = "0.3.2" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "443144c8cdadd93ebf52ddb4056d257f5b52c04d3c804e657d19eb73fc33668b" +checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" [[package]] name = "hex" @@ -533,14 +498,36 @@ checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" [[package]] name = "indexmap" -version = "2.0.0" +version = "2.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5477fe2230a79769d8dc68e0eabf5437907c0457a5614a9e8dddb67f65eb65d" +checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26" dependencies = [ "equivalent", "hashbrown", ] +[[package]] +name = "inotify" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdd168d97690d0b8c412d6b6c10360277f4d7ee495c5d0d5d5fe0854923255cc" +dependencies = [ + "bitflags 1.3.2", + "futures-core", + "inotify-sys", + "libc", + "tokio", +] + +[[package]] +name = "inotify-sys" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e05c02b5e89bff3b946cedeca278abc628fe811e604f027c45a8aa3cf793d0eb" +dependencies = [ + "libc", +] + [[package]] name = "lazy_static" version = "1.4.0" @@ -561,9 +548,9 @@ checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c" [[package]] name = "memchr" -version = "2.6.3" +version = "2.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f232d6ef707e1956a43342693d2a31e72989554d58299d7a88738cc95b0d35c" +checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149" [[package]] name = "memoffset" @@ -576,18 +563,18 @@ dependencies = [ [[package]] name = "miniz_oxide" -version = "0.7.1" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" +checksum = "9d811f3e15f28568be3407c8e7fdb6514c1cda3cb30683f15b6a1a1dc4ea14a7" dependencies = [ "adler", ] [[package]] name = "mio" -version = "0.8.8" +version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "927a765cd3fc26206e66b296465fa9d3e5ab003e651c1b3c060e7956d96b19d2" +checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" dependencies = [ "libc", "wasi", @@ -600,7 +587,7 @@ version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ab2156c4fce2f8df6c499cc1c763e4394b7482525bf2a9701c9d79d215f519e4" dependencies = [ - "bitflags 2.4.0", + "bitflags 2.5.0", "cfg-if", "cfg_aliases", "libc", @@ -619,18 +606,18 @@ dependencies = [ [[package]] name = "object" -version = "0.32.1" +version = "0.32.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9cf5f9dd3933bd50a9e1f149ec995f39ae2c496d31fd772c1fd45ebc27e902b0" +checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441" dependencies = [ "memchr", ] [[package]] name = "once_cell" -version = "1.18.0" +version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" [[package]] name = "ordered-stream" @@ -644,9 +631,9 @@ dependencies = [ [[package]] name = "parking" -version = "2.1.0" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14f2252c834a40ed9bb5422029649578e63aa341ac401f74e719dd1afda8394e" +checksum = "bb813b8af86854136c6922af0598d719255ecb2179515e6e7730d468f05c9cae" [[package]] name = "pin-project-lite" @@ -748,20 +735,11 @@ dependencies = [ "getrandom", ] -[[package]] -name = "redox_syscall" -version = "0.3.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29" -dependencies = [ - "bitflags 1.3.2", -] - [[package]] name = "regex" -version = "1.10.3" +version = "1.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b62dbe01f0b06f9d8dc7d49e05a0785f153b00b2c227856282f671e0318c9b15" +checksum = "c117dbdfde9c8308975b6a18d71f3f385c89461f7b3fb054288ecf2a2058ba4c" dependencies = [ "aho-corasick", "memchr", @@ -798,7 +776,7 @@ version = "0.38.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "65e04861e65f21776e67888bfbea442b3642beaa0138fdb1dd7a84a52dffdb89" dependencies = [ - "bitflags 2.4.0", + "bitflags 2.5.0", "errno", "libc", "linux-raw-sys", @@ -807,18 +785,18 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.192" +version = "1.0.197" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bca2a08484b285dcb282d0f67b26cadc0df8b19f8c12502c13d966bf9482f001" +checksum = "3fb1c873e1b9b056a4dc4c0c198b24c3ffa059243875552b2bd0933b1aee4ce2" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.192" +version = "1.0.197" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6c7207fbec9faa48073f3e3074cbe553af6ea512d7c21ba46e434e70ea9fbc1" +checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b" dependencies = [ "proc-macro2", "quote", @@ -876,12 +854,12 @@ dependencies = [ [[package]] name = "socket2" -version = "0.5.4" +version = "0.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4031e820eb552adee9295814c0ced9e5cf38ddf1e8b7d566d6de8e2538ea989e" +checksum = "05ffd9c0a93b7543e062e759284fcf5f5e3b098501104bfbdde4d404db792871" dependencies = [ "libc", - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] @@ -895,7 +873,12 @@ name = "steamos-manager" version = "24.3.0" dependencies = [ "anyhow", + "inotify", + "nix", + "tempfile", "tokio", + "tokio-stream", + "tokio-util", "tracing", "tracing-subscriber", "zbus", @@ -925,15 +908,14 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.8.0" +version = "3.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb94d2f3cc536af71caac6b6fcebf65860b347e7ce0cc9ebe8f70d3e521054ef" +checksum = "85b77fafb263dd9d05cbeac119526425676db3784113aa9295c88498cbf8bff1" dependencies = [ "cfg-if", "fastrand", - "redox_syscall", "rustix", - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] @@ -948,9 +930,9 @@ dependencies = [ [[package]] name = "tokio" -version = "1.33.0" +version = "1.36.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f38200e3ef7995e5ef13baec2f432a6da0aa9ac495b2c0e8f3b7eec2c92d653" +checksum = "61285f6515fa018fb2d1e46eb21223fff441ee8db5d0f1435e8ab4f5cdb80931" dependencies = [ "backtrace", "bytes", @@ -967,15 +949,39 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "2.1.0" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e" +checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" dependencies = [ "proc-macro2", "quote", "syn 2.0.53", ] +[[package]] +name = "tokio-stream" +version = "0.1.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "267ac89e0bec6e691e5813911606935d77c476ff49024f98abcea3e7b15e37af" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5419f34732d9eb6ee4c3578b7989078579b7f039cbbb9ca2c4da015749371e15" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + [[package]] name = "toml_datetime" version = "0.6.5" @@ -1037,9 +1043,9 @@ dependencies = [ [[package]] name = "typenum" -version = "1.16.0" +version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba" +checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" [[package]] name = "uds_windows" @@ -1064,12 +1070,6 @@ version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" -[[package]] -name = "waker-fn" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d5b2c62b4012a3e1eca5a7e077d13b3bf498c4073e33ccd58626607748ceeca" - [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" @@ -1232,9 +1232,9 @@ checksum = "32b752e52a2da0ddfbdbcc6fceadfeede4c939ed16d13e648833a61dfb611ed8" [[package]] name = "winnow" -version = "0.5.15" +version = "0.5.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c2e3184b9c4e92ad5167ca73039d0c42476302ab603e2fec4487511f38ccefc" +checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" dependencies = [ "memchr", ] @@ -1256,15 +1256,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c9ff46f2a25abd690ed072054733e0bc3157e3d4c45f41bd183dce09c2ff8ab9" dependencies = [ "async-broadcast", - "async-executor", - "async-fs", - "async-io", - "async-lock 3.3.0", "async-process", "async-recursion", - "async-task", "async-trait", - "blocking", "derivative", "enumflags2", "event-listener 5.2.0", diff --git a/Cargo.toml b/Cargo.toml index ccb7b20..3272aef 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,12 +3,19 @@ name = "steamos-manager" version = "24.3.0" edition = "2021" +[dev-dependencies] +tempfile = "3" + [profile.release] strip="symbols" [dependencies] anyhow = "1" -tokio = { version = "1", features = ["fs", "io-std", "macros", "process", "rt-multi-thread", "signal"] } +inotify = { version = "0.10", default-features = false, features = ["stream"] } +nix = { version = "0.28", default-features = false, features = ["fs"] } +tokio = { version = "1", default-features = false, features = ["fs", "io-util", "macros", "rt-multi-thread", "signal", "sync"] } +tokio-stream = { version = "0.1", default-features = false } +tokio-util = { version = "0.7", default-features = false } tracing = { version = "0.1", default-features = false } tracing-subscriber = { version = "0.3", default-features = false, features = ["fmt"] } -zbus = { version = "4", features = ["tokio"] } +zbus = { version = "4", default-features = false, features = ["tokio"] } diff --git a/data/steamosmanager.service b/data/steamosmanager.service index b10f3b3..1675fc8 100644 --- a/data/steamosmanager.service +++ b/data/steamosmanager.service @@ -1,7 +1,15 @@ [Unit] Description=SteamOS Manager Daemon +Wants=steamos-log-submitter.service +After=steamos-log-submitter.service [Service] Type=dbus BusName=com.steampowered.SteamOSManager1 +Environment=RUST_LOG='INFO' ExecStart=/usr/lib/steamos-manager +Restart=on-failure +RestartSec=1 + +[Install] +WantedBy=multi-user.target diff --git a/src/ds_inhibit.rs b/src/ds_inhibit.rs new file mode 100644 index 0000000..618c133 --- /dev/null +++ b/src/ds_inhibit.rs @@ -0,0 +1,601 @@ +/* SPDX-License-Identifier: BSD-2-Clause */ +use anyhow::{Error, Result}; +use inotify::{Event, EventMask, EventStream, Inotify, WatchDescriptor, WatchMask}; +use std::collections::HashMap; +use std::ffi::OsString; +use std::path::{Path, PathBuf}; +use std::thread::sleep; +use std::time::Duration; +use tokio::fs; +use tokio_stream::StreamExt; +use tracing::{debug, error, info, warn}; + +use crate::{sysbase, Service}; + +struct HidNode { + id: u32, +} + +pub struct Inhibitor { + inotify: EventStream<[u8; 512]>, + dev_watch: WatchDescriptor, + watches: HashMap, +} + +impl HidNode { + fn new(id: u32) -> HidNode { + HidNode { id } + } + + fn sys_base(&self) -> PathBuf { + PathBuf::from(format!("{}/sys/class/hidraw/hidraw{}/device", sysbase(), self.id).as_str()) + } + + fn hidraw(&self) -> PathBuf { + PathBuf::from(format!("{}/dev/hidraw{}", sysbase(), self.id).as_str()) + } + + async fn get_nodes(&self) -> Result> { + let mut entries = Vec::new(); + let mut dir = fs::read_dir(self.sys_base().join("input")).await?; + while let Some(entry) = dir.next_entry().await? { + let path = entry.path(); + let mut dir = fs::read_dir(&path).await?; + while let Some(entry) = dir.next_entry().await? { + if entry + .path() + .file_name() + .map(|e| e.to_string_lossy()) + .is_some_and(|e| e.starts_with("mouse")) + { + debug!("Found {}", path.display()); + entries.push(path.join("inhibited")); + } + } + } + Ok(entries) + } + + async fn can_inhibit(&self) -> bool { + debug!("Checking if hidraw{} can be inhibited", self.id); + let driver = match fs::read_link(self.sys_base().join("driver")).await { + Ok(driver) => driver, + Err(e) => { + warn!( + "Failed to find associated driver for hidraw{}: {}", + self.id, e + ); + return false; + } + }; + + if !matches!( + driver.file_name().and_then(|d| d.to_str()), + Some("sony") | Some("playstation") + ) { + debug!("Not a PlayStation controller"); + return false; + } + let nodes = match self.get_nodes().await { + Ok(nodes) => nodes, + Err(e) => { + warn!("Failed to list inputs for hidraw{}: {e}", self.id); + return false; + } + }; + if nodes.is_empty() { + debug!("No nodes to inhibit"); + return false; + } + true + } + + async fn check(&self) -> Result<()> { + let hidraw = self.hidraw(); + let mut dir = fs::read_dir(sysbase() + "/proc").await?; + while let Some(entry) = dir.next_entry().await? { + let path = entry.path(); + let proc = match path.file_name().map(|p| p.to_str()) { + Some(Some(p)) => p, + _ => continue, + }; + let _: u32 = match proc.parse() { + Ok(i) => i, + _ => continue, + }; + let mut fds = match fs::read_dir(path.join("fd")).await { + Ok(fds) => fds, + Err(e) => { + debug!("Process {proc} disappeared while scanning: {e}"); + continue; + } + }; + while let Ok(Some(f)) = fds.next_entry().await { + let path = match fs::read_link(f.path()).await { + Ok(p) => p, + Err(e) => { + debug!("Process {proc} disappeared while scanning: {e}"); + continue; + } + }; + if path == hidraw { + let comm = match fs::read(format!("{}/proc/{proc}/comm", sysbase())).await { + Ok(c) => c, + Err(e) => { + debug!("Process {proc} disappeared while scanning: {e}"); + continue; + } + }; + if String::from_utf8_lossy(comm.as_ref()) == "steam\n" { + info!("Inhibiting hidraw{}", self.id); + self.inhibit().await?; + return Ok(()); + } + } + } + } + info!("Uninhibiting hidraw{}", self.id); + self.uninhibit().await?; + Ok(()) + } + + async fn inhibit(&self) -> Result<()> { + let mut res = Ok(()); + for node in self.get_nodes().await?.into_iter() { + if let Err(err) = fs::write(node, "1\n").await { + error!("Encountered error inhibiting: {err}"); + res = Err(err.into()); + } + } + res + } + + async fn uninhibit(&self) -> Result<()> { + let mut res = Ok(()); + for node in self.get_nodes().await?.into_iter() { + if let Err(err) = fs::write(node, "0\n").await { + error!("Encountered error inhibiting: {err}"); + res = Err(err.into()); + } + } + res + } +} + +impl Inhibitor { + pub fn new() -> Result { + let inotify = Inotify::init()?.into_event_stream([0; 512])?; + let dev_watch = inotify + .watches() + .add(sysbase() + "/dev", WatchMask::CREATE)?; + + Ok(Inhibitor { + inotify, + dev_watch, + watches: HashMap::new(), + }) + } + + pub async fn init() -> Result { + let mut inhibitor = match Inhibitor::new() { + Ok(i) => i, + Err(e) => { + error!("Could not create inotify watches: {e}"); + return Err(e); + } + }; + + let mut dir = fs::read_dir(sysbase() + "/dev").await?; + while let Some(entry) = dir.next_entry().await? { + if let Err(e) = inhibitor.watch(entry.path().as_path()).await { + error!("Encountered error attempting to watch: {e}"); + } + } + Ok(inhibitor) + } + + async fn watch(&mut self, path: &Path) -> Result { + let metadata = path.metadata()?; + if metadata.is_dir() { + return Ok(false); + } + + let id = match path + .file_name() + .and_then(|f| f.to_str()) + .and_then(|s| s.strip_prefix("hidraw")) + .and_then(|s| s.parse().ok()) + { + Some(id) => id, + None => return Ok(false), + }; + + let node = HidNode::new(id); + if !node.can_inhibit().await { + return Ok(false); + } + info!("Adding {} to watchlist", path.display()); + let watch = self.inotify.watches().add( + &node.hidraw(), + WatchMask::DELETE_SELF + | WatchMask::OPEN + | WatchMask::CLOSE_NOWRITE + | WatchMask::CLOSE_WRITE, + )?; + if let Err(e) = node.check().await { + error!( + "Encountered error attempting to check if hidraw{} can be inhibited: {e}", + node.id + ); + } + self.watches.insert(watch, node); + Ok(true) + } + + async fn process_event(&mut self, event: Event) -> Result<()> { + const QSEC: Duration = Duration::from_millis(250); + debug!("Got event: {:08x}", event.mask); + if event.wd == self.dev_watch { + let path = match event.name { + Some(fname) => PathBuf::from(fname), + None => { + error!("Got an event without an associated filename!"); + return Err(Error::msg("Got an event without an associated filename")); + } + }; + debug!("New device {} found", path.display()); + let path = PathBuf::from(sysbase() + "/dev").join(path); + sleep(QSEC); // Wait a quarter second for nodes to enumerate + if let Err(e) = self.watch(path.as_path()).await { + error!("Encountered error attempting to watch: {e}"); + return Err(e); + } + } else if event.mask == EventMask::DELETE_SELF { + debug!("Device removed"); + self.watches.remove(&event.wd); + let _ = self.inotify.watches().remove(event.wd); + } else if let Some(node) = self.watches.get(&event.wd) { + node.check().await?; + } else if event.mask != EventMask::IGNORED { + error!("Unhandled event: {:08x}", event.mask); + } + Ok(()) + } +} + +impl Service for Inhibitor { + const NAME: &'static str = "ds-inhibitor"; + + async fn run(&mut self) -> Result<()> { + loop { + let res = match self.inotify.next().await { + Some(Ok(event)) => self.process_event(event).await, + Some(Err(e)) => return Err(e.into()), + None => return Ok(()), + }; + if let Err(e) = res { + warn!("Got error processing event: {e}"); + } + } + } + + async fn shutdown(&mut self) -> Result<()> { + let mut res = Ok(()); + for (wd, node) in self.watches.drain() { + if let Err(e) = self.inotify.watches().remove(wd) { + warn!("Error removing watch while shutting down: {e}"); + res = Err(e.into()); + } + if let Err(e) = node.uninhibit().await { + warn!("Error uninhibiting {} while shutting down: {e}", node.id); + res = Err(e); + } + } + res + } +} + +#[cfg(test)] +mod test { + use super::*; + use crate::testing; + use std::fs::{create_dir_all, read_to_string, remove_file, write, File}; + use std::os::unix::fs::symlink; + use tokio::time::sleep; + + async fn nyield(times: u32) { + for i in 0..times { + sleep(Duration::from_millis(1)).await; + } + } + + #[tokio::test] + async fn hid_nodes() { + let h = testing::start(); + let path = h.test.path(); + + let hid = HidNode::new(0); + let sys_base = hid.sys_base(); + + create_dir_all(sys_base.join("input/input0/foo0")).expect("foo0"); + create_dir_all(sys_base.join("input/input1/bar0")).expect("bar0"); + create_dir_all(sys_base.join("input/input2/mouse0")).expect("mouse0"); + + assert_eq!( + hid.get_nodes().await.expect("get_nodes"), + &[sys_base.join("input/input2/inhibited")] + ); + } + + #[tokio::test] + async fn hid_can_inhibit() { + let h = testing::start(); + let path = h.test.path(); + + let hids = [ + HidNode::new(0), + HidNode::new(1), + HidNode::new(2), + HidNode::new(3), + HidNode::new(4), + HidNode::new(5), + HidNode::new(6), + ]; + + create_dir_all(hids[0].sys_base().join("input/input0/foo0")).expect("foo0"); + symlink("foo", hids[0].sys_base().join("driver")).expect("hidraw0"); + create_dir_all(hids[1].sys_base().join("input/input1/mouse0")).expect("mouse0"); + symlink("foo", hids[1].sys_base().join("driver")).expect("hidraw1"); + create_dir_all(hids[2].sys_base().join("input/input2/foo1")).expect("foo1"); + symlink("sony", hids[2].sys_base().join("driver")).expect("hidraw2"); + create_dir_all(hids[3].sys_base().join("input/input3/mouse1")).expect("mouse1"); + symlink("sony", hids[3].sys_base().join("driver")).expect("hidraw3"); + create_dir_all(hids[4].sys_base().join("input/input4/foo2")).expect("foo2"); + symlink("playstation", hids[4].sys_base().join("driver")).expect("hidraw4"); + create_dir_all(hids[5].sys_base().join("input/input5/mouse2")).expect("mouse2"); + symlink("playstation", hids[5].sys_base().join("driver")).expect("hidraw5"); + create_dir_all(hids[6].sys_base().join("input/input6/mouse3")).expect("mouse3"); + + assert!(!hids[0].can_inhibit().await); + assert!(!hids[1].can_inhibit().await); + assert!(!hids[2].can_inhibit().await); + assert!(hids[3].can_inhibit().await); + assert!(!hids[4].can_inhibit().await); + assert!(hids[5].can_inhibit().await); + assert!(!hids[6].can_inhibit().await); + } + + #[tokio::test] + async fn hid_inhibit() { + let h = testing::start(); + let path = h.test.path(); + + let hid = HidNode::new(0); + let sys_base = hid.sys_base(); + + create_dir_all(sys_base.join("input/input0/mouse0")).expect("mouse0"); + symlink("sony", sys_base.join("driver")).expect("hidraw0"); + + assert!(hid.can_inhibit().await); + + hid.inhibit().await; + assert_eq!( + read_to_string(sys_base.join("input/input0/inhibited")).expect("inhibited"), + "1\n" + ); + hid.uninhibit().await; + assert_eq!( + read_to_string(sys_base.join("input/input0/inhibited")).expect("inhibited"), + "0\n" + ); + } + + #[tokio::test] + async fn hid_inhibit_error_continue() { + let h = testing::start(); + let path = h.test.path(); + + let hid = HidNode::new(0); + let sys_base = hid.sys_base(); + + create_dir_all(sys_base.join("input/input0/mouse0")).expect("mouse0"); + create_dir_all(sys_base.join("input/input0/inhibited")).expect("inhibited"); + create_dir_all(sys_base.join("input/input1/mouse1")).expect("mouse0"); + symlink("sony", sys_base.join("driver")).expect("hidraw0"); + + assert!(hid.can_inhibit().await); + + hid.inhibit().await; + assert_eq!( + read_to_string(sys_base.join("input/input1/inhibited")).expect("inhibited"), + "1\n" + ); + hid.uninhibit().await; + assert_eq!( + read_to_string(sys_base.join("input/input1/inhibited")).expect("inhibited"), + "0\n" + ); + } + + #[tokio::test] + async fn hid_check() { + let h = testing::start(); + let path = h.test.path(); + + let hid = HidNode::new(0); + let sys_base = hid.sys_base(); + + create_dir_all(sys_base.join("input/input0/mouse0")).expect("mouse0"); + symlink("sony", sys_base.join("driver")).expect("hidraw0"); + create_dir_all(path.join("proc/1/fd")).expect("fd"); + + symlink(hid.hidraw(), path.join("proc/1/fd/3")).expect("symlink"); + write(path.join("proc/1/comm"), "steam\n").expect("comm"); + + hid.check().await.expect("check"); + assert_eq!( + read_to_string(sys_base.join("input/input0/inhibited")).expect("inhibited"), + "1\n" + ); + + write(path.join("proc/1/comm"), "epic\n").expect("comm"); + hid.check().await.expect("check"); + assert_eq!( + read_to_string(sys_base.join("input/input0/inhibited")).expect("inhibited"), + "0\n" + ); + + remove_file(path.join("proc/1/fd/3")).expect("rm"); + write(path.join("proc/1/comm"), "steam\n").expect("comm"); + hid.check().await.expect("check"); + assert_eq!( + read_to_string(sys_base.join("input/input0/inhibited")).expect("inhibited"), + "0\n" + ); + } + + #[tokio::test] + async fn inhibitor_start() { + let h = testing::start(); + let path = h.test.path(); + + let hid = HidNode::new(0); + let sys_base = hid.sys_base(); + + create_dir_all(path.join("dev")).expect("dev"); + create_dir_all(sys_base.join("input/input0/mouse0")).expect("mouse0"); + write(hid.hidraw(), "").expect("hidraw"); + symlink("sony", sys_base.join("driver")).expect("driver"); + create_dir_all(path.join("proc/1/fd")).expect("fd"); + symlink(hid.hidraw(), path.join("proc/1/fd/3")).expect("symlink"); + write(path.join("proc/1/comm"), "steam\n").expect("comm"); + + let mut inhibitor = Inhibitor::init().await.expect("init"); + + assert_eq!( + read_to_string(sys_base.join("input/input0/inhibited")).expect("inhibited"), + "1\n" + ); + + inhibitor.shutdown().await.expect("stop"); + + assert_eq!( + read_to_string(sys_base.join("input/input0/inhibited")).expect("inhibited"), + "0\n" + ); + } + + #[tokio::test] + async fn inhibitor_open_close() { + let h = testing::start(); + let path = h.test.path(); + + let hid = HidNode::new(0); + let sys_base = hid.sys_base(); + + create_dir_all(path.join("dev")).expect("dev"); + create_dir_all(sys_base.join("input/input0/mouse0")).expect("mouse0"); + File::create(hid.hidraw()).expect("hidraw"); + symlink("sony", sys_base.join("driver")).expect("driver"); + create_dir_all(path.join("proc/1/fd")).expect("fd"); + write(path.join("proc/1/comm"), "steam\n").expect("comm"); + + let mut inhibitor = Inhibitor::init().await.expect("init"); + let task = tokio::spawn(async move { + inhibitor.run().await; + }); + + nyield(1).await; + assert_eq!( + read_to_string(sys_base.join("input/input0/inhibited")).expect("inhibited"), + "0\n" + ); + + symlink(hid.hidraw(), path.join("proc/1/fd/3")).expect("symlink"); + let f = File::open(hid.hidraw()).expect("hidraw"); + nyield(2).await; + assert_eq!( + read_to_string(sys_base.join("input/input0/inhibited")).expect("inhibited"), + "1\n" + ); + + drop(f); + remove_file(path.join("proc/1/fd/3")).expect("rm"); + nyield(1).await; + assert_eq!( + read_to_string(sys_base.join("input/input0/inhibited")).expect("inhibited"), + "0\n" + ); + + task.abort(); + } + + #[tokio::test] + async fn inhibitor_fast_create() { + let h = testing::start(); + let path = h.test.path(); + + let hid = HidNode::new(0); + let sys_base = hid.sys_base(); + + create_dir_all(path.join("dev")).expect("dev"); + create_dir_all(sys_base.join("input/input0/mouse0")).expect("mouse0"); + symlink("sony", sys_base.join("driver")).expect("driver"); + create_dir_all(path.join("proc/1/fd")).expect("fd"); + write(path.join("proc/1/comm"), "steam\n").expect("comm"); + + let mut inhibitor = Inhibitor::init().await.expect("init"); + let task = tokio::spawn(async move { + inhibitor.run().await; + }); + + nyield(1).await; + assert!(read_to_string(sys_base.join("input/input0/inhibited")).is_err()); + + File::create(hid.hidraw()).expect("hidraw"); + symlink(hid.hidraw(), path.join("proc/1/fd/3")).expect("symlink"); + let f = File::open(hid.hidraw()).expect("hidraw"); + nyield(4).await; + assert_eq!( + read_to_string(sys_base.join("input/input0/inhibited")).expect("inhibited"), + "1\n" + ); + + task.abort(); + } + + #[tokio::test] + async fn inhibitor_create() { + let h = testing::start(); + let path = h.test.path(); + + let hid = HidNode::new(0); + let sys_base = hid.sys_base(); + + create_dir_all(path.join("dev")).expect("dev"); + create_dir_all(sys_base.join("input/input0/mouse0")).expect("mouse0"); + symlink("sony", sys_base.join("driver")).expect("driver"); + create_dir_all(path.join("proc/1/fd")).expect("fd"); + write(path.join("proc/1/comm"), "steam\n").expect("comm"); + + let mut inhibitor = Inhibitor::init().await.expect("init"); + let task = tokio::spawn(async move { + inhibitor.run().await; + }); + + nyield(3).await; + assert!(read_to_string(sys_base.join("input/input0/inhibited")).is_err()); + + File::create(hid.hidraw()).expect("hidraw"); + nyield(3).await; + symlink(hid.hidraw(), path.join("proc/1/fd/3")).expect("symlink"); + let f = File::open(hid.hidraw()).expect("hidraw"); + nyield(3).await; + assert_eq!( + read_to_string(sys_base.join("input/input0/inhibited")).expect("inhibited"), + "1\n" + ); + + task.abort(); + } +} diff --git a/src/main.rs b/src/main.rs index a9045c2..b475ec9 100644 --- a/src/main.rs +++ b/src/main.rs @@ -24,32 +24,244 @@ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -use anyhow::{Error, Result}; +use anyhow::{bail, Error, Result}; use tokio::signal::unix::{signal, SignalKind}; -use tracing_subscriber; +use tokio::task::JoinSet; +use tokio_util::sync::CancellationToken; +use tracing::{error, info, warn}; +use tracing_subscriber::prelude::*; +use tracing_subscriber::{fmt, Registry}; +use zbus::connection::Connection; use zbus::ConnectionBuilder; +use crate::ds_inhibit::Inhibitor; +use crate::sls::ftrace::Ftrace; +use crate::sls::{LogLayer, LogReceiver}; + +mod ds_inhibit; mod manager; +mod sls; + +#[cfg(test)] +mod testing; + +trait Service +where + Self: Sized, +{ + const NAME: &'static str; + + async fn run(&mut self) -> Result<()>; + + async fn shutdown(&mut self) -> Result<()> { + Ok(()) + } + + async fn start(mut self, token: CancellationToken) -> Result<()> { + info!("Starting {}", Self::NAME); + let res = tokio::select! { + r = self.run() => r, + _ = token.cancelled() => Ok(()), + }; + if res.is_err() { + warn!( + "{} encountered an error: {}", + Self::NAME, + res.as_ref().unwrap_err() + ); + token.cancel(); + } + info!("Shutting down {}", Self::NAME); + self.shutdown().await.and(res) + } +} + +#[cfg(not(test))] +pub fn sysbase() -> String { + String::new() +} + +#[cfg(test)] +pub fn sysbase() -> String { + let current_test = crate::testing::current(); + let path = current_test.path(); + String::from(path.as_os_str().to_str().unwrap()) +} + +pub fn read_comm(pid: u32) -> Result { + let comm = std::fs::read_to_string(format!("{}/proc/{}/comm", sysbase(), pid))?; + Ok(comm.trim_end().to_string()) +} + +pub fn get_appid(pid: u32) -> Result> { + let environ = std::fs::read_to_string(format!("{}/proc/{}/environ", sysbase(), pid))?; + for env_var in environ.split('\0') { + let (key, value) = match env_var.split_once('=') { + Some((k, v)) => (k, v), + None => continue, + }; + if key != "SteamGameId" { + continue; + } + match value.parse() { + Ok(appid) => return Ok(Some(appid)), + Err(_) => break, + }; + } + + let stat = std::fs::read_to_string(format!("{}/proc/{}/stat", sysbase(), pid))?; + let stat = match stat.rsplit_once(") ") { + Some((_, v)) => v, + None => return Ok(None), + }; + let ppid = match stat.split(' ').nth(1) { + Some(ppid) => ppid, + None => return Err(anyhow::Error::msg("stat data invalid")), + }; + let ppid: u32 = ppid.parse()?; + if ppid > 1 { + get_appid(ppid) + } else { + Ok(None) + } +} + +async fn reload() -> Result<()> { + loop { + let mut sighup = signal(SignalKind::hangup())?; + sighup.recv().await.ok_or(Error::msg(""))?; + } +} + +async fn create_connection() -> Result { + let manager = manager::SMManager::new()?; + + ConnectionBuilder::system()? + .name("com.steampowered.SteamOSManager1")? + .serve_at("/com/steampowered/SteamOSManager1", manager)? + .build() + .await + .map_err(|e| e.into()) +} #[tokio::main] async fn main() -> Result<()> { // This daemon is responsible for creating a dbus api that steam client can use to do various OS // level things. It implements com.steampowered.SteamOSManager1 interface - tracing_subscriber::fmt::init(); + let stdout_log = fmt::layer(); + let subscriber = Registry::default().with(stdout_log); + + let connection = match create_connection().await { + Ok(c) => c, + Err(e) => { + let _guard = tracing::subscriber::set_default(subscriber); + error!("Error connecting to DBus: {}", e); + bail!(e); + } + }; + + let mut services = JoinSet::new(); + let token = CancellationToken::new(); + + let mut log_receiver = LogReceiver::new(connection.clone()).await?; + let remote_logger = LogLayer::new(&log_receiver).await; + let subscriber = subscriber.with(remote_logger); + let _guard = tracing::subscriber::set_global_default(subscriber)?; let mut sigterm = signal(SignalKind::terminate())?; + let mut sigquit = signal(SignalKind::quit())?; - let manager = manager::SMManager::new()?; + let ftrace = Ftrace::init(connection.clone()).await?; + services.spawn(ftrace.start(token.clone())); - let _system_connection = ConnectionBuilder::system()? - .name("com.steampowered.SteamOSManager1")? - .serve_at("/com/steampowered/SteamOSManager1", manager)? - .build() - .await?; + let inhibitor = Inhibitor::init().await?; + services.spawn(inhibitor.start(token.clone())); - tokio::select! { - e = sigterm.recv() => e.ok_or(Error::msg("SIGTERM pipe broke")), - e = tokio::signal::ctrl_c() => Ok(e?), + let mut res = tokio::select! { + e = log_receiver.run() => e, + e = services.join_next() => match e.unwrap() { + Ok(Ok(())) => Ok(()), + Ok(Err(e)) => Err(e), + Err(e) => Err(e.into()) + }, + _ = tokio::signal::ctrl_c() => Ok(()), + e = sigterm.recv() => e.ok_or(Error::msg("SIGTERM machine broke")), + _ = sigquit.recv() => Err(Error::msg("Got SIGQUIT")), + e = reload() => e, + } + .inspect_err(|e| error!("Encountered error running: {e}")); + token.cancel(); + + info!("Shutting down"); + + while let Some(service_res) = services.join_next().await { + res = match service_res { + Ok(Err(e)) => Err(e), + Err(e) => Err(e.into()), + _ => continue, + }; + } + + res.inspect_err(|e| error!("Encountered error: {e}")) +} + +#[cfg(test)] +mod test { + use crate::testing; + use std::fs; + + #[test] + fn read_comm() { + let h = testing::start(); + let path = h.test.path(); + fs::create_dir_all(path.join("proc/123456")).expect("create_dir_all"); + fs::write(path.join("proc/123456/comm"), "test\n").expect("write comm"); + + assert_eq!(crate::read_comm(123456).expect("read_comm"), "test"); + assert!(crate::read_comm(123457).is_err()); + } + + #[test] + fn appid_environ() { + let h = testing::start(); + let path = h.test.path(); + fs::create_dir_all(path.join("proc/123456")).expect("create_dir_all"); + fs::write( + path.join("proc/123456/environ"), + "A=B\0SteamGameId=98765\0C=D", + ) + .expect("write environ"); + + assert_eq!(crate::get_appid(123456).expect("get_appid"), Some(98765)); + assert!(crate::get_appid(123457).is_err()); + } + + #[test] + fn appid_parent_environ() { + let h = testing::start(); + let path = h.test.path(); + fs::create_dir_all(path.join("proc/123456")).expect("create_dir_all"); + fs::write( + path.join("proc/123456/environ"), + "A=B\0SteamGameId=98765\0C=D", + ) + .expect("write environ"); + fs::create_dir_all(path.join("proc/123457")).expect("create_dir_all"); + fs::write(path.join("proc/123457/environ"), "A=B\0C=D").expect("write environ"); + fs::write(path.join("proc/123457/stat"), "0 (comm) S 123456 ...").expect("write stat"); + + assert_eq!(crate::get_appid(123457).expect("get_appid"), Some(98765)); + } + + #[test] + fn appid_missing() { + let h = testing::start(); + let path = h.test.path(); + fs::create_dir_all(path.join("proc/123457")).expect("create_dir_all"); + fs::write(path.join("proc/123457/environ"), "A=B\0C=D").expect("write environ"); + fs::write(path.join("proc/123457/stat"), "0 (comm) S 1 ...").expect("write stat"); + + assert_eq!(crate::get_appid(123457).expect("get_appid"), None); } } diff --git a/src/sls/ftrace.rs b/src/sls/ftrace.rs new file mode 100644 index 0000000..367fffe --- /dev/null +++ b/src/sls/ftrace.rs @@ -0,0 +1,286 @@ +/* SPDX-License-Identifier: BSD-2-Clause */ +use anyhow::{Error, Result}; +use std::collections::HashMap; +use std::fmt::Debug; +use std::path::Path; +use tokio::fs; +use tokio::io::{AsyncBufReadExt, BufReader}; +use tokio::net::unix::pipe; +use tracing::{error, info}; +use zbus::connection::Connection; +use zbus::zvariant; + +use crate::{get_appid, read_comm, sysbase, Service}; + +#[zbus::proxy( + interface = "com.steampowered.SteamOSLogSubmitter.Trace", + default_service = "com.steampowered.SteamOSLogSubmitter", + default_path = "/com/steampowered/SteamOSLogSubmitter/helpers/Trace" +)] +trait TraceHelper { + async fn log_event( + &self, + trace: &str, + data: HashMap<&str, zvariant::Value<'_>>, + ) -> zbus::Result<()>; +} + +pub struct Ftrace +where + Self: 'static, +{ + pipe: Option>, + proxy: TraceHelperProxy<'static>, +} + +async fn setup_traces(path: &Path) -> Result<()> { + fs::write(path.join("events/oom/mark_victim/enable"), "1").await?; + fs::write(path.join("set_ftrace_filter"), "split_lock_warn").await?; + fs::write(path.join("current_tracer"), "function").await?; + Ok(()) +} + +impl Ftrace { + pub async fn init(connection: Connection) -> Result { + let base = Self::base(); + let path = Path::new(base.as_str()); + fs::create_dir_all(path).await?; + setup_traces(path).await?; + let file = pipe::OpenOptions::new() + .unchecked(true) // Thanks tracefs for making trace_pipe a "regular" file + .open_receiver(path.join("trace_pipe"))?; + Ok(Ftrace { + pipe: Some(BufReader::new(file)), + proxy: TraceHelperProxy::new(&connection).await?, + }) + } + + fn base() -> String { + sysbase() + "/sys/kernel/tracing/instances/steamos-log-submitter" + } + + async fn handle_pid(data: &mut HashMap<&str, zvariant::Value<'_>>, pid: u32) -> Result<()> { + if let Ok(comm) = read_comm(pid) { + info!("├─ comm: {}", comm); + data.insert("comm", zvariant::Value::new(comm)); + } else { + info!("├─ comm not found"); + } + if let Ok(Some(appid)) = get_appid(pid) { + info!("└─ appid: {}", appid); + data.insert("appid", zvariant::Value::new(appid)); + } else { + info!("└─ appid not found"); + } + Ok(()) + } + + async fn handle_event(&mut self, line: &str) -> Result<()> { + info!("Forwarding line {}", line); + let mut data = HashMap::new(); + let mut split = line.rsplit(' '); + if let Some(("pid", pid)) = split.next().and_then(|arg| arg.split_once('=')) { + let pid = pid.parse()?; + Ftrace::handle_pid(&mut data, pid).await?; + } + self.proxy.log_event(line, data).await?; + Ok(()) + } +} + +impl Service for Ftrace { + const NAME: &'static str = "ftrace"; + + async fn run(&mut self) -> Result<()> { + loop { + let mut string = String::new(); + self.pipe + .as_mut() + .ok_or(Error::msg("BUG: trace_pipe missing"))? + .read_line(&mut string) + .await?; + if let Err(e) = self.handle_event(string.trim_end()).await { + error!("Encountered an error handling event: {}", e); + } + } + } + + async fn shutdown(&mut self) -> Result<()> { + self.pipe.take(); + fs::remove_dir(Self::base()).await?; + Ok(()) + } +} + +#[cfg(test)] +mod test { + use super::*; + use crate::testing; + use nix::sys::stat::Mode; + use nix::unistd; + use std::cell::Cell; + use std::fs; + use std::path::PathBuf; + use tokio::sync::mpsc::{unbounded_channel, UnboundedReceiver, UnboundedSender}; + + struct MockTrace { + traces: UnboundedSender<(String, HashMap)>, + } + + #[zbus::interface(name = "com.steampowered.SteamOSLogSubmitter.Trace")] + impl MockTrace { + fn log_event( + &mut self, + trace: &str, + data: HashMap<&str, zvariant::Value<'_>>, + ) -> zbus::fdo::Result<()> { + self.traces.send(( + String::from(trace), + HashMap::from_iter( + data.iter() + .map(|(k, v)| (String::from(*k), v.try_to_owned().unwrap())), + ), + )); + Ok(()) + } + } + + #[tokio::test] + async fn handle_pid() { + let h = testing::start(); + let path = h.test.path(); + + fs::create_dir_all(path.join("proc/1234")).expect("create_dir_all"); + fs::write(path.join("proc/1234/comm"), "ftrace\n").expect("write comm"); + fs::write(path.join("proc/1234/environ"), "SteamGameId=5678").expect("write environ"); + + fs::create_dir_all(path.join("proc/1235")).expect("create_dir_all"); + fs::write(path.join("proc/1235/comm"), "ftrace\n").expect("write comm"); + + fs::create_dir_all(path.join("proc/1236")).expect("create_dir_all"); + fs::write(path.join("proc/1236/environ"), "SteamGameId=5678").expect("write environ"); + + let mut map = HashMap::new(); + assert!(Ftrace::handle_pid(&mut map, 1234).await.is_ok()); + assert_eq!( + *map.get("comm").expect("comm"), + zvariant::Value::new("ftrace") + ); + assert_eq!( + *map.get("appid").expect("appid"), + zvariant::Value::new(5678 as u64) + ); + + let mut map = HashMap::new(); + assert!(Ftrace::handle_pid(&mut map, 1235).await.is_ok()); + assert_eq!( + *map.get("comm").expect("comm"), + zvariant::Value::new("ftrace") + ); + assert!(map.get("appid").is_none()); + + let mut map = HashMap::new(); + assert!(Ftrace::handle_pid(&mut map, 1236).await.is_ok()); + assert!(map.get("comm").is_none()); + assert_eq!( + *map.get("appid").expect("appid"), + zvariant::Value::new(5678 as u64) + ); + } + + #[tokio::test] + async fn ftrace_init() { + let h = testing::start(); + let path = h.test.path(); + + let tracefs = PathBuf::from(Ftrace::base()); + + fs::create_dir_all(tracefs.join("events/oom/mark_victim")).expect("create_dir_all"); + unistd::mkfifo( + tracefs.join("trace_pipe").as_path(), + Mode::S_IRUSR | Mode::S_IWUSR, + ) + .expect("trace_pipe"); + let dbus = Connection::session().await.expect("dbus"); + let ftrace = Ftrace::init(dbus).await.expect("ftrace"); + + assert_eq!( + fs::read_to_string(tracefs.join("events/oom/mark_victim/enable")).unwrap(), + "1" + ); + } + + #[tokio::test] + async fn ftrace_relay() { + let h = testing::start(); + let path = h.test.path(); + + let tracefs = PathBuf::from(Ftrace::base()); + + fs::create_dir_all(tracefs.join("events/oom/mark_victim")).expect("create_dir_all"); + unistd::mkfifo( + tracefs.join("trace_pipe").as_path(), + Mode::S_IRUSR | Mode::S_IWUSR, + ) + .expect("trace_pipe"); + + fs::create_dir_all(path.join("proc/14351")).expect("create_dir_all"); + fs::write(path.join("proc/14351/comm"), "ftrace\n").expect("write comm"); + fs::write(path.join("proc/14351/environ"), "SteamGameId=5678").expect("write environ"); + + let (sender, mut receiver) = unbounded_channel(); + let trace = MockTrace { traces: sender }; + let dbus = zbus::connection::Builder::session() + .unwrap() + .name("com.steampowered.SteamOSLogSubmitter") + .unwrap() + .serve_at("/com/steampowered/SteamOSLogSubmitter/helpers/Trace", trace) + .unwrap() + .build() + .await + .expect("dbus"); + let mut ftrace = Ftrace::init(dbus).await.expect("ftrace"); + + assert!(match receiver.try_recv() { + Empty => true, + _ => false, + }); + ftrace + .handle_event( + " GamepadUI Input-4886 [003] .N.1. 23828.572941: mark_victim: pid=14351", + ) + .await + .expect("event"); + let (line, data) = match receiver.try_recv() { + Ok((line, data)) => (line, data), + _ => panic!("Test failed"), + }; + assert_eq!( + line, + " GamepadUI Input-4886 [003] .N.1. 23828.572941: mark_victim: pid=14351" + ); + assert_eq!(data.len(), 2); + assert_eq!( + data.get("appid").map(|v| v.downcast_ref()), + Some(Ok(5678 as u64)) + ); + assert_eq!( + data.get("comm").map(|v| v.downcast_ref()), + Some(Ok("ftrace")) + ); + + ftrace + .handle_event(" GamepadUI Input-4886 [003] .N.1. 23828.572941: split_lock_warn <-") + .await + .expect("event"); + let (line, data) = match receiver.try_recv() { + Ok((line, data)) => (line, data), + _ => panic!("Test failed"), + }; + assert_eq!( + line, + " GamepadUI Input-4886 [003] .N.1. 23828.572941: split_lock_warn <-" + ); + assert_eq!(data.len(), 0); + } +} diff --git a/src/sls/mod.rs b/src/sls/mod.rs new file mode 100644 index 0000000..be314d8 --- /dev/null +++ b/src/sls/mod.rs @@ -0,0 +1,137 @@ +/* SPDX-License-Identifier: BSD-2-Clause */ +pub mod ftrace; + +use anyhow::Result; +use std::fmt::Debug; +use std::time::SystemTime; +use tokio::sync::mpsc::{unbounded_channel, UnboundedReceiver, UnboundedSender}; +use tracing::field::{Field, Visit}; +use tracing::{Event, Level, Subscriber}; +use tracing_subscriber::layer::Context; +use tracing_subscriber::Layer; +use zbus::connection::Connection; + +use crate::Service; + +#[zbus::proxy( + interface = "com.steampowered.SteamOSLogSubmitter.Manager", + default_service = "com.steampowered.SteamOSLogSubmitter", + default_path = "/com/steampowered/SteamOSLogSubmitter/Manager" +)] +trait Daemon { + async fn log( + &self, + timestamp: f64, + module: &str, + level: u32, + message: &str, + ) -> zbus::Result<()>; +} + +struct StringVisitor { + string: String, +} + +struct LogLine { + timestamp: f64, + module: String, + level: u32, + message: String, +} + +pub struct LogReceiver +where + Self: 'static, +{ + receiver: UnboundedReceiver, + sender: UnboundedSender, + proxy: DaemonProxy<'static>, +} + +pub struct LogLayer { + queue: UnboundedSender, +} + +impl Visit for StringVisitor { + fn record_debug(&mut self, _: &Field, value: &dyn Debug) { + self.string.push_str(format!("{value:?}").as_str()); + } +} + +impl LogReceiver { + pub async fn new(connection: Connection) -> Result { + let proxy = DaemonProxy::new(&connection).await?; + let (sender, receiver) = unbounded_channel(); + Ok(LogReceiver { + receiver, + sender, + proxy, + }) + } +} + +impl Service for LogReceiver { + const NAME: &'static str = "SLS log receiver"; + + async fn run(&mut self) -> Result<()> { + while let Some(message) = self.receiver.recv().await { + let _ = self + .proxy + .log( + message.timestamp, + message.module.as_ref(), + message.level, + message.message.as_ref(), + ) + .await; + } + Ok(()) + } +} + +impl LogLayer { + pub async fn new(receiver: &LogReceiver) -> LogLayer { + LogLayer { + queue: receiver.sender.clone(), + } + } +} + +impl tracing_subscriber::registry::LookupSpan<'a>> Layer for LogLayer { + fn on_event(&self, event: &Event<'_>, _ctx: Context<'_, S>) { + let target = event.metadata().target(); + if !target.starts_with("steamos_workerd::sls") { + // Don't forward non-SLS-related logs to SLS + return; + } + let target = target + .split("::") + .skip(2) + .fold(String::from("steamos_workerd"), |prefix, suffix| { + prefix + "." + suffix + }); + let level = match *event.metadata().level() { + Level::TRACE => 10, + Level::DEBUG => 10, + Level::INFO => 20, + Level::WARN => 30, + Level::ERROR => 40, + }; + let mut builder = StringVisitor { + string: String::new(), + }; + event.record(&mut builder); + let text = builder.string; + let now = SystemTime::now(); + let time = match now.duration_since(SystemTime::UNIX_EPOCH) { + Ok(duration) => duration.as_secs_f64(), + Err(_) => 0.0, + }; + let _ = self.queue.send(LogLine { + timestamp: time, + module: target, + level, + message: text, + }); + } +} diff --git a/src/testing.rs b/src/testing.rs new file mode 100644 index 0000000..0276d09 --- /dev/null +++ b/src/testing.rs @@ -0,0 +1,47 @@ +use std::cell::RefCell; +use std::path::Path; +use std::rc::Rc; +use tempfile::{tempdir, TempDir}; + +thread_local! { + static TEST: RefCell>> = RefCell::new(None); +} + +pub fn start() -> TestHandle { + TEST.with(|lock| { + assert!(lock.borrow().as_ref().is_none()); + let test: Rc = Rc::new(Test { + base: tempdir().expect("Couldn't create test directory"), + }); + *lock.borrow_mut() = Some(test.clone()); + TestHandle { test } + }) +} + +pub fn stop() { + TEST.with(|lock| *lock.borrow_mut() = None); +} + +pub fn current() -> Rc { + TEST.with(|lock| lock.borrow().as_ref().unwrap().clone()) +} + +pub struct Test { + base: TempDir, +} + +pub struct TestHandle { + pub test: Rc, +} + +impl Test { + pub fn path(&self) -> &Path { + self.base.path() + } +} + +impl Drop for TestHandle { + fn drop(&mut self) { + stop(); + } +}