From 0517d1894c2646d29fbbb25f71befe48815482ca Mon Sep 17 00:00:00 2001 From: Hayden Briese Date: Sat, 25 Jul 2020 19:32:41 +1000 Subject: [PATCH] - Added ability to enable/disable on a per-fan basis - Added CLI args - Fixed test all bug where only some devices were tested - Fixed issue where fan recoveries were potentially tried without waiting between attempts - Restore driver control of device once disabled - Updated README Signed-off-by: Hayden Briese --- CMakeLists.txt | 1 + README.md | 20 +-- debian/changelog | 2 +- proto/DevicesSpec.proto | 31 +++-- src/Args.hpp | 12 +- src/Client.cpp | 264 +++++++++++++++++++++++----------------- src/Client.hpp | 33 ++--- src/Controller.cpp | 219 ++++++++++++++++----------------- src/Controller.hpp | 62 ++++------ src/Devices.cpp | 6 +- src/Devices.hpp | 2 +- src/FanInterface.cpp | 2 +- src/FanThread.cpp | 16 +++ src/FanThread.hpp | 26 ++++ src/Service.cpp | 196 +++++++++++++++++------------ src/Service.hpp | 62 +++++----- src/Util.hpp | 1 + src/main.hpp | 17 --- 18 files changed, 536 insertions(+), 436 deletions(-) create mode 100644 src/FanThread.cpp create mode 100644 src/FanThread.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 8579dc4..6030f24 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -51,6 +51,7 @@ set(SOURCE_FILES ${SRC}/main.cpp ${SRC}/main.hpp ${SRC}/Args.hpp ${SRC}/Args.cpp ${SRC}/Client.cpp ${SRC}/Client.hpp ${SRC}/Controller.cpp ${SRC}/Controller.hpp + ${SRC}/FanThread.cpp ${SRC}/FanThread.hpp ${SRC}/Devices.cpp ${SRC}/Devices.hpp ${SRC}/FanInterface.cpp ${SRC}/FanInterface.hpp ${SRC}/SensorInterface.cpp ${SRC}/SensorInterface.hpp diff --git a/README.md b/README.md index 737a53c..77d1d85 100644 --- a/README.md +++ b/README.md @@ -99,18 +99,20 @@ devices { ```text fancon arg [value] ... h help Show this help -s status Status of the controller - enable Enable controller (default: true) - disable Disable controller - reload Reload config -t test Test ALL (untested) fans -t test [fan] Test the given fan +s status Status of all fans +e enable Enable control of all fans +e enable [fan] Enable control of the fan +d disable Disable control of all fans +d disable [fan] Disable control of the fans +t test Test all (untested) fans +t test [fan] Test the fan if untested f force Test even already tested fans (default: false) -c config [file] Config path (default: /etc/fancon.conf) +r reload Reload config +c config [file] Config path (default: /etc/fancon.conf) service Start as service -d daemon Daemonize the process (default: false) + daemon Daemonize the process (default: false) stop-service Stop the service -i sysinfo [file] Save system info to file (default: fancon_sysinfo.txt) +i sysinfo [file] Save system info to file (default: sysinfo.txt) nv-init Init nvidia devices v verbose Debug logging level a trace Trace logging level diff --git a/debian/changelog b/debian/changelog index 1d5f29c..37e74ba 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,4 +1,4 @@ -fancon (0.21.2) UNRELEASED; urgency=low +fancon (0.22.0) UNRELEASED; urgency=low * Initial release. Closes: #00000 diff --git a/proto/DevicesSpec.proto b/proto/DevicesSpec.proto index 12b2e29..10205dc 100644 --- a/proto/DevicesSpec.proto +++ b/proto/DevicesSpec.proto @@ -47,6 +47,10 @@ message Fan { uint32 id = 20; } +message FanLabel { + string label = 1; +} + message Sensor { DevType type = 1; string label = 2; @@ -72,31 +76,32 @@ message TestResponse { int32 status = 1; } -message ControllerState { - enum State { +message FanStatus { + enum Status { ENABLED = 0; DISABLED = 1; - RELOAD = 2; + TESTING = 2; } - State state = 1; + Status status = 1; } message Empty {} service DService { rpc StopService(Empty) returns (Empty) {} - rpc Enable(Empty) returns (Empty) {} - rpc Disable(Empty) returns (Empty) {} - rpc Reload(Empty) returns (Empty) {} - rpc NvInit(Empty) returns (Empty) {} - rpc ControllerStatus(Empty) returns (ControllerState) {} - rpc GetDevices(Empty) returns (Devices) {} rpc SetDevices(Devices) returns (Empty) {} + rpc SubscribeDevices(Empty) returns (stream Devices) {} rpc GetEnumeratedDevices(Empty) returns (Devices) {} - rpc Test(TestRequest) returns (stream TestResponse) {} - rpc GetControllerConfig(Empty) returns (ControllerConfig) {} rpc SetControllerConfig(ControllerConfig) returns (Empty) {} - + + rpc Status(FanLabel) returns (FanStatus) {} + rpc Enable(FanLabel) returns (Empty) {} + rpc EnableAll(Empty) returns (Empty) {} + rpc Disable(FanLabel) returns (Empty) {} + rpc DisableAll(Empty) returns (Empty) {} + rpc Test(TestRequest) returns (stream TestResponse) {} + rpc Reload(Empty) returns (Empty) {} + rpc NvInit(Empty) returns (Empty) {} } diff --git a/src/Args.hpp b/src/Args.hpp index 3c2f8a2..5a17f20 100644 --- a/src/Args.hpp +++ b/src/Args.hpp @@ -11,7 +11,7 @@ using fc::Util::is_root; namespace fc { static const char *DEFAULT_CONF_PATH(FANCON_SYSCONFDIR "/fancon.conf"), - *DEFAULT_SYSINFO_PATH = "fancon_sysinfo.txt"; + *DEFAULT_SYSINFO_PATH = "sysinfo.txt"; class Arg { public: @@ -27,11 +27,13 @@ class Arg { class Args { public: - Arg help = {"help", "h"}, status = {"status", "s"}, enable = {"enable"}, - disable = {"disable"}, reload = {"reload"}, + Arg help = {"help", "h"}, status = {"status", "s"}, + enable = {"enable", "e", true, false}, + disable = {"disable", "d", true, false}, test = {"test", "t", true, false}, force = {"force", "f"}, + reload = {"reload", "r"}, config = {"config", "c", true, true, DEFAULT_CONF_PATH, true}, - service = {"service"}, daemon = {"daemon", "d"}, + service = {"service"}, daemon = {"daemon"}, stop_service = {"stop-service"}, sysinfo = {"sysinfo", "i", true, true, DEFAULT_SYSINFO_PATH}, nv_init = {"nv-init"}, verbose = {"verbose", "v"}, trace = {"trace", "a"}; @@ -40,9 +42,9 @@ class Args { {status.key, status}, {enable.key, enable}, {disable.key, disable}, - {reload.key, reload}, {test.key, test}, {force.key, force}, + {reload.key, reload}, {config.key, config}, {service.key, service}, {daemon.key, daemon}, diff --git a/src/Client.cpp b/src/Client.cpp index 2adc143..a6b2805 100644 --- a/src/Client.cpp +++ b/src/Client.cpp @@ -23,169 +23,181 @@ void fc::Client::run(Args &args) { } if (args.status) { - const auto status = Status(); - if (status) { - string s; - if (*status == fc::ControllerState::ENABLED) - s = "enabled"; - else if (*status == fc::ControllerState::DISABLED) - s = "disabled"; - else if (*status == fc::ControllerState::RELOAD) - s = "reload"; - LOG(llvl::info) << "Status: " << s; - } else - LOG(llvl::info) << "Status: service offline"; + status(); } else if (args.enable) { - if (Enable()) - LOG(llvl::info) << "Status: enabled"; + if (args.enable.has_value()) + enable(args.enable.value); + else + enable(); } else if (args.disable) { - if (Disable()) - LOG(llvl::info) << "Status: disabled"; - } else if (args.reload) { - if (Reload()) - LOG(llvl::info) << "Status: reload"; - } else if (args.nv_init) { - NvInit(); + if (args.disable.has_value()) + disable(args.disable.value); + else + disable(); } else if (args.test) { - if (!args.test.value.empty()) - Test(args.test.value, args.force); + if (args.test.has_value()) + test(args.test.value, true); else - Test(args.force); + test(args.force); + } else if (args.reload) { + reload(); } else if (args.stop_service) { - if (!StopService()) - LOG(llvl::error) << "Failed to stop service"; + stop_service(); + } else if (args.nv_init) { + nv_init(); } else if (args.sysinfo) { - if (Sysinfo(args.sysinfo.value)) - LOG(llvl::info) << "Sysinfo written to: " << args.sysinfo.value; - else - LOG(llvl::error) << "Failed to write sysinfo"; + sysinfo(args.sysinfo.value); } else if (Util::is_atty() && !exists(args.config.value)) { // Offer test cout << log::fmt_green << "Test devices & generate a config? (y/n): "; char answer; std::cin >> answer; if (answer == 'y') { - Test(args.force); + test(args.force); } - } else if (!args.help) { // else, excluding help which has already run + } else { // else, excluding help which has already run print_help(args.config.value); } } -optional fc::Client::Status() { +void fc::Client::stop_service() { ClientContext context; - fc_pb::ControllerState resp; + if (check(client->StopService(&context, empty, &empty))) + LOG(llvl::info) << "Stopped service"; + else + LOG(llvl::error) << "Failed to stop service"; +} + +optional fc::Client::get_devices() { + ClientContext context; + fc_pb::Devices devices; - grpc::Status status = client->ControllerStatus(&context, empty, &resp); - if (!check(status)) + if (!check(client->GetDevices(&context, empty, &devices))) return nullopt; - return static_cast(resp.state()); + return devices; } -bool fc::Client::Enable() { +optional fc::Client::get_enumerated_devices() { ClientContext context; - return check(client->Enable(&context, empty, &empty)); + fc_pb::Devices devices; + + if (!check(client->GetEnumeratedDevices(&context, empty, &devices))) + return nullopt; + + return devices; } -bool fc::Client::Disable() { - ClientContext context; - return check(client->Disable(&context, empty, &empty)); +void fc::Client::status() { + const auto devices = get_devices(); + if (!devices || devices->fan_size() == 0) { + LOG(llvl::info) << "No devices found"; + return; + } + + for (const auto &f : devices->fan()) { + ClientContext context; + fc_pb::FanLabel req = from(f.label()); + + fc_pb::FanStatus status; + if (check(client->Status(&context, req, &status))) + LOG(llvl::info) << f.label() << ": " << status_text(status.status()); + } } -bool fc::Client::Reload() { +void fc::Client::enable(const string &flabel) { ClientContext context; - return check(client->Reload(&context, empty, &empty)); + fc_pb::FanLabel req = from(flabel); + + if (check(client->Enable(&context, req, &empty))) + LOG(llvl::info) << flabel << ": enabled"; } -bool fc::Client::NvInit() { +void fc::Client::enable() { ClientContext context; - return check(client->NvInit(&context, empty, &empty)); + if (check(client->EnableAll(&context, empty, &empty))) + status(); } -optional fc::Client::GetDevices() { +void fc::Client::disable(const string &flabel) { ClientContext context; - fc_pb::Devices resp; - - grpc::Status status = client->GetDevices(&context, empty, &resp); - if (!check(status)) - return nullopt; - - fc::Devices devices; - devices.from(resp); + fc_pb::FanLabel req = from(flabel); - return devices; + if (check(client->Disable(&context, req, &empty))) + LOG(llvl::info) << flabel << ": disabled"; } -optional fc::Client::GetEnumeratedDevices() { +void fc::Client::disable() { ClientContext context; - fc_pb::Devices resp; - - grpc::Status status = client->GetEnumeratedDevices(&context, empty, &resp); - if (!check(status)) - return nullopt; - - fc::Devices devices; - devices.from(resp); - - return devices; + if (check(client->DisableAll(&context, empty, &empty))) + status(); } -bool fc::Client::Test(bool forced) { - ClientContext context; +void fc::Client::test(bool forced) { + const auto devices = get_devices(); + if (!devices || devices->fan_size() == 0) { + LOG(llvl::info) << "No devices found"; + return; + } + mutex write_mutex; vector threads; - vector> results; - - for (const auto &fan : {"hwmon3/fan1", "hwmon3/fan2"}) { - results.emplace_back(std::async([&, fan] { + for (const auto &f : devices->fan()) { + const string &flabel = f.label(); + threads.emplace_back([&, flabel] { + ClientContext context; fc_pb::TestRequest req; - req.set_device_label(fan); + req.set_device_label(flabel); req.set_forced(forced); auto reader = client->Test(&context, req); fc_pb::TestResponse resp; while (reader->Read(&resp)) { const lock_guard lock(write_mutex); - LOG(llvl::info) << fan << ": " << resp.status() << "%"; + LOG(llvl::info) << flabel << ": " << resp.status() << "%"; } - return check(reader->Finish()); - })); + if (!check(reader->Finish())) + LOG(llvl::error) << flabel << ": test failed"; + }); } - for (auto &r : results) { - if (r.valid() && !r.get()) - return false; + for (auto &t : threads) { + if (t.joinable()) + t.join(); } - - return true; } -bool fc::Client::Test(const string &fan_label, bool forced) { +void fc::Client::test(const string &flabel, bool forced) { ClientContext context; fc_pb::TestRequest req; - req.set_device_label(fan_label); + req.set_device_label(flabel); req.set_forced(forced); auto reader = client->Test(&context, req); fc_pb::TestResponse resp; while (reader->Read(&resp)) { - LOG(llvl::info) << fan_label << ": " << resp.status() << "%"; + LOG(llvl::info) << flabel << ": " << resp.status() << "%"; } - const auto status = reader->Finish(); - const bool success = check(status); - if (!success) - LOG(llvl::error) << fan_label << ": failed to start test"; - return success; + if (!check(reader->Finish())) + LOG(llvl::error) << flabel << ": test failed"; } -bool fc::Client::StopService() { +void fc::Client::reload() { ClientContext context; - return check(client->StopService(&context, empty, &empty)); + if (check(client->Reload(&context, empty, &empty))) + LOG(llvl::info) << "Reloaded"; + else + LOG(llvl::error) << "Failed to reload"; } -bool fc::Client::Sysinfo(const string &p) { +void fc::Client::nv_init() { + ClientContext context; + if (!check(client->NvInit(&context, empty, &empty))) + LOG(llvl::error) << "Failed to init nvidia"; +} + +void fc::Client::sysinfo(const string &p) { string out; std::ofstream ofs(p); @@ -195,6 +207,8 @@ bool fc::Client::Sysinfo(const string &p) { if (check(client->GetEnumeratedDevices(&context, empty, &enumerated))) { google::protobuf::TextFormat::PrintToString(enumerated, &out); ofs << out; + } else { + ofs << "Failed"; } ClientContext context_user; @@ -203,6 +217,8 @@ bool fc::Client::Sysinfo(const string &p) { if (check(client->GetDevices(&context_user, empty, &user))) { google::protobuf::TextFormat::PrintToString(user, &out); ofs << out; + } else { + ofs << "Failed"; } const path hwmon_dir = "/sys/class/hwmon"; @@ -211,33 +227,35 @@ bool fc::Client::Sysinfo(const string &p) { // TODO: logs - if (!ofs) - return false; - - // Allow all users to read & write to the file - const auto perms = fs::perms::others_read | fs::perms::others_write; - fs::permissions(p, perms, fs::perm_options::add); + if (ofs) { + // Allow all users to read & write to the file + const auto perms = fs::perms::others_read | fs::perms::others_write; + fs::permissions(p, perms, fs::perm_options::add); - return true; + LOG(llvl::info) << "Sysinfo written to: " << p; + } else { + LOG(llvl::error) << "Failed to write sysinfo to: " << p; + } } void fc::Client::print_help(const string &conf) { LOG(llvl::info) << "fancon arg [value] ..." << endl << "h help Show this help" << endl - << "s status Status of the controller" << endl - << " enable Enable controller (default: true)" - << endl - << " disable Disable controller" << endl - << " reload Reload config" << endl - << "t test Test ALL (untested) fans" << endl - << "t test [fan] Test the given fan" << endl + << "s status Status of all fans" << endl + << "e enable Enable control of all fans" << endl + << "e enable [fan] Enable control of the fan" << endl + << "d disable Disable control of all fans" << endl + << "d disable [fan] Disable control of the fans" << endl + << "t test Test all (untested) fans" << endl + << "t test [fan] Test the fan (forced)" << endl << "f force Test even already tested fans " << "(default: false)" << endl - << "c config [file] Config path (default: " + << "r reload Reload config" << endl + << "c config [file] Config path (default: " << log::fmt_green_bold << conf << log::fmt_reset << ")" << endl << " service Start as service" << endl - << "d daemon Daemonize the process (default: false)" + << " daemon Daemonize the process (default: false)" << endl << " stop-service Stop the service" << endl << "i sysinfo [file] Save system info to file (default: " @@ -265,11 +283,13 @@ bool fc::Client::check(const grpc::Status &status) { if (status.ok()) return true; - using grpc::StatusCode; switch (status.error_code()) { case StatusCode::UNAVAILABLE: log_service_unavailable(); break; + case StatusCode::NOT_FOUND: + LOG(llvl::error) << status.error_message() << ": not found"; + break; default: LOG(llvl::error) << status.error_message() << ": " << status.error_details(); @@ -279,8 +299,9 @@ bool fc::Client::check(const grpc::Status &status) { } void fc::Client::log_service_unavailable() { - LOG(llvl::fatal) << "Unable to connect to service; " << log::fmt_bold - << "start with 'fancon service'" << log::fmt_reset; + LOG(llvl::fatal) << "Unable to connect to service; " << endl + << log::fmt_bold << "start with 'sudo fancon service'" + << log::fmt_reset; } void fc::Client::enumerate_directory(const path &dir, std::ostream &os, @@ -303,3 +324,22 @@ void fc::Client::enumerate_directory(const path &dir, std::ostream &os, } } } + +string fc::Client::status_text(fc_pb::FanStatus_Status status) { + switch (status) { + case fc_pb::FanStatus_Status_ENABLED: + return "enabled"; + case fc_pb::FanStatus_Status_DISABLED: + return "disabled"; + case fc_pb::FanStatus_Status_TESTING: + return "testing"; + default: + return "invalid"; + } +} + +fc_pb::FanLabel fc::Client::from(const string &flabel) { + fc_pb::FanLabel l; + l.set_label(flabel); + return l; +} diff --git a/src/Client.hpp b/src/Client.hpp index 4f11837..5306575 100644 --- a/src/Client.hpp +++ b/src/Client.hpp @@ -2,19 +2,16 @@ #define FANCON_SRC_CLIENT_HPP #include "Args.hpp" -#include "Devices.hpp" #include "Util.hpp" #include "proto/DevicesSpec.grpc.pb.h" #include "proto/DevicesSpec.pb.h" #include using fc::Args; -using fc_pb::ControllerState; using fc_pb::Empty; -using grpc::ClientAsyncResponseReader; using grpc::ClientContext; -using grpc::CompletionQueue; using grpc::Status; +using grpc::StatusCode; namespace fc { class Client { @@ -22,17 +19,21 @@ class Client { Client(); void run(Args &args); - optional Status(); - bool Enable(); - bool Disable(); - bool Reload(); - bool NvInit(); - optional GetDevices(); - optional GetEnumeratedDevices(); - bool Test(bool forced); - bool Test(const string &fan_label, bool forced); - bool StopService(); - bool Sysinfo(const string &p); + + void stop_service(); + optional get_devices(); + optional get_enumerated_devices(); + + void status(); + void enable(const string &flabel); + void enable(); + void disable(const string &flabel); + void disable(); + void test(bool forced); + void test(const string &flabel, bool forced); + void reload(); + void nv_init(); + void sysinfo(const string &p); static void print_help(const string &conf); static bool service_running(); @@ -49,6 +50,8 @@ class Client { static void log_service_unavailable(); static void enumerate_directory(const path &dir, std::ostream &os, uint depth = 0); + static string status_text(fc_pb::FanStatus_Status status); + static fc_pb::FanLabel from(const string &flabel); }; } // namespace fc diff --git a/src/Controller.cpp b/src/Controller.cpp index 1df6543..750e2ba 100644 --- a/src/Controller.cpp +++ b/src/Controller.cpp @@ -7,22 +7,26 @@ uint top_stickiness_intervals = 2; uint temp_averaging_intervals = 3; } // namespace fc -fc::FanThread &fc::FanThread::operator=(fc::FanThread &&other) noexcept { - t = move(other.t); - test_status = other.test_status; - return *this; -} - -fc::Controller::Controller(path conf_path_) - : state(ControllerState::DISABLED), config_path(move(conf_path_)) { +fc::Controller::Controller(path conf_path_) : config_path(move(conf_path_)) { load_conf_and_enumerated(); + + watcher = spawn_watcher(); } -fc::Controller::~Controller() { disable(); } +fc::Controller::~Controller() { disable_all(); } + +FanStatus fc::Controller::status(const string &flabel) const { + const auto it = fthreads.find(flabel); + if (it == fthreads.end()) + return FanStatus::FanStatus_Status_DISABLED; + + return (it->second.is_testing()) ? FanStatus::FanStatus_Status_TESTING + : FanStatus::FanStatus_Status_ENABLED; +} -void fc::Controller::control(fc::FanInterface &f) { - if (threads.count(f.label) > 0) { - LOG(llvl::debug) << f << ": duplicate thread"; +void fc::Controller::enable(fc::FanInterface &f) { + if (fthreads.count(f.label) > 0) { + LOG(llvl::trace) << f << ": already enabled"; return; } @@ -30,14 +34,77 @@ void fc::Controller::control(fc::FanInterface &f) { return; auto update_func = [this, &f]() { - while (enabled()) { + while (true) { f.update(); sleep_for(update_interval); } - threads.erase(f.label); }; - threads.try_emplace(f.label, thread(update_func), nullptr); + LOG(llvl::trace) << f.label << ": enabled"; + fthreads.try_emplace(f.label, thread(update_func), nullptr); +} + +void fc::Controller::enable_all() { + for (auto &[key, f] : devices.fans) { + if (!fthreads.contains(f->label)) + enable(*f); + } +} + +void fc::Controller::disable(const string &flabel) { + const auto it = fthreads.find(flabel); + if (it != fthreads.end()) { + fthreads.erase(it); + if (const auto fit = devices.fans.find(flabel); fit != devices.fans.end()) + fit->second->disable_control(); + LOG(llvl::trace) << flabel << ": disabled"; + } else { + LOG(llvl::error) << flabel << ": failed to find to disable"; + } +} + +void fc::Controller::disable_all() { + fthreads.clear(); + LOG(llvl::trace) << "Disabling all"; +} + +void fc::Controller::reload() { + // Remove all threads not testing + vector stopped; + for (auto &[key, fthread] : fthreads) { + if (!fthread.is_testing()) { + // fthread.running = false; + stopped.push_back(key); + } + } + + for (const string &key : stopped) + fthreads.erase(key); + + // Restart + LOG(llvl::info) << "Reloading changes"; + load_conf_and_enumerated(); + enable_all(); +} + +void fc::Controller::reload_added() { + // TODO: + // Load config changes + load_conf_and_enumerated(); + // Compare new devices to old + // Interrupt and enable ed + // Interrupt all threads not testing, and don't wait + for (auto &[key, fthread] : fthreads) { + if (!fthread.is_testing()) + fthread.t.interrupt(); + } +} + +void fc::Controller::nv_init() { +#ifdef FANCON_NVIDIA_SUPPORT + if (NV::init(true)) + reload(); // TODO: reload only added devices +#endif // FANCON_NVIDIA_SUPPORT } void fc::Controller::test(fc::FanInterface &fan, bool forced, @@ -46,7 +113,7 @@ void fc::Controller::test(fc::FanInterface &fan, bool forced, return; // If a test is already running for the device then just join onto it - if (auto it = threads.find(fan.label); it->second.is_testing()) { + if (auto it = fthreads.find(fan.label); it->second.is_testing()) { it->second.test_status->register_observer(cb); it->second.join(); return; @@ -55,8 +122,8 @@ void fc::Controller::test(fc::FanInterface &fan, bool forced, LOG(llvl::info) << fan << ": testing"; // Remove any running thread before testing - if (auto it = threads.find(fan.label); it != threads.end()) - threads.erase(it); + if (const auto it = fthreads.find(fan.label); it != fthreads.end()) + fthreads.erase(it); auto test_status = make_shared>(0); test_status->register_observer(cb); @@ -64,18 +131,18 @@ void fc::Controller::test(fc::FanInterface &fan, bool forced, auto test_func = [&] { // Test fan, then remove thread from map fan.test(*test_status); - threads.erase(fan.label); + fthreads.erase(fan.label); LOG(llvl::info) << fan << ": test complete"; - // TODO: use locks to avoid race condition + // Only write to file when no other fan are still testing if (tests_running() == 0) to_file(); - control(fan); + enable(fan); }; auto [it, success] = - threads.try_emplace(fan.label, thread(test_func), test_status); + fthreads.try_emplace(fan.label, thread(test_func), test_status); if (!success) test(fan, forced, cb); @@ -83,95 +150,11 @@ void fc::Controller::test(fc::FanInterface &fan, bool forced, it->second.join(); } -bool fc::Controller::testing(const string &label) const { - auto it = threads.find(label); - return (it != threads.end()) && it->second.is_testing(); -} - size_t fc::Controller::tests_running() const { auto f = [](const size_t sum, const auto &p) { return sum + int(p.second.is_testing()); }; - return std::accumulate(threads.begin(), threads.end(), 0, f); -} - -void fc::Controller::enable() { - state = ControllerState::ENABLED; - do { - for (auto &[key, f] : devices.fans) - control(*f); - - if (threads.empty()) - LOG(llvl::info) << "Awaiting configuration"; - - while (enabled()) { - if (config_file_modified()) { - reload(); - } else { - sleep_for(update_interval); - } - } - - if (reloading()) { - state = ControllerState::ENABLED; - LOG(llvl::info) << "Reloading changes"; - load_conf_and_enumerated(); - } - } while (enabled()); -} - -bool fc::Controller::enabled() const { - return state == ControllerState::ENABLED; -} - -void fc::Controller::disable() { - state = ControllerState::DISABLED; - - // Interrupt all threads, and wait for them to finish - threads.clear(); - join_threads(); -} - -bool fc::Controller::disabled() const { - return state == ControllerState::DISABLED; -} - -void fc::Controller::reload() { - state = ControllerState::RELOAD; - - // Interrupt all threads not testing, and don't wait - vector to_remove; - for (auto &[key, fthread] : threads) { - if (!fthread.is_testing()) - to_remove.push_back(key); - } - - for (const string &key : to_remove) - threads.erase(key); -} - -void fc::Controller::reload_added() { - // TODO: - // Load config changes - load_conf_and_enumerated(); - // Compare new devices to old - // Interrupt and enable ed - // Interrupt all threads not testing, and don't wait - for (auto &[key, fthread] : threads) { - if (!fthread.is_testing()) - fthread.t.interrupt(); - } -} - -bool fc::Controller::reloading() const { - return state == ControllerState::RELOAD; -} - -void fc::Controller::nv_init() { -#ifdef FANCON_NVIDIA_SUPPORT - if (NV::init(true)) - reload(); // TODO: just reload added devices -#endif // FANCON_NVIDIA_SUPPORT + return std::accumulate(fthreads.begin(), fthreads.end(), 0, f); } void fc::Controller::from(const fc_pb::Controller &c) { @@ -223,7 +206,7 @@ void fc::Controller::to(fc_pb::ControllerConfig &c) const { } void fc::Controller::load_conf_and_enumerated() { - devices = fc::Devices(false); + devices = fc::Devices(true); if (config_path.empty() || !exists(config_path)) { LOG(llvl::debug) << "No config found"; @@ -240,6 +223,7 @@ void fc::Controller::load_conf_and_enumerated() { if (ifs) { from(c); update_config_write_time(); + notify_devices_observers(); } else { LOG(llvl::error) << "Failed to read config from: " << config_path; } @@ -263,6 +247,7 @@ void fc::Controller::to_file(bool backup) { if (ofs) { update_config_write_time(); + notify_devices_observers(); } else { LOG(llvl::error) << "Failed to write config to: " << config_path; } @@ -280,11 +265,19 @@ bool fc::Controller::config_file_modified() const { return config_write_time != fs::last_write_time(config_path); } -void fc::Controller::join_threads() { - for (auto &[key, ft] : threads) { // Wait for all threads to shutdown - if (ft.t.joinable()) - ft.t.join(); - } +thread fc::Controller::spawn_watcher() { + return thread([this] { + while (true) { + if (config_file_modified()) + reload(); + sleep_for(update_interval); + } + }); +} + +void fc::Controller::notify_devices_observers() const { + for (const auto &f : devices_observers) + f(devices); } string fc::Controller::date_time_now() { diff --git a/src/Controller.hpp b/src/Controller.hpp index e12e7b1..27aed1c 100644 --- a/src/Controller.hpp +++ b/src/Controller.hpp @@ -1,25 +1,28 @@ #ifndef FANCON_CONTROLLER_HPP #define FANCON_CONTROLLER_HPP +#include "Devices.hpp" +#include "FanThread.hpp" +#include "proto/DevicesSpec.pb.h" #include #include #include #include #include +#include #include #include #include #include -#include "Devices.hpp" -#include "proto/DevicesSpec.pb.h" - -using boost::thread; using fc::FanInterface; -using fc::Util::Observable; +using fc::FanThread; using std::find_if; using std::future; using std::istringstream; +using FanStatus = fc_pb::FanStatus_Status; +using FThreads_map = std::map; +using FThreads_it = FThreads_map::iterator; namespace fc { extern bool dynamic; @@ -27,48 +30,25 @@ extern uint smoothing_intervals; extern uint top_stickiness_intervals; extern uint temp_averaging_intervals; -enum class ControllerState { - ENABLED = fc_pb::ControllerState_State_ENABLED, - DISABLED = fc_pb::ControllerState_State_DISABLED, - RELOAD = fc_pb::ControllerState_State_RELOAD -}; - -struct FanThread { - explicit FanThread(thread &&t, - shared_ptr> testing_status = nullptr) - : t(move(t)), test_status(move(testing_status)) {} - ~FanThread() { t.interrupt(); } // Interrupt thread when unwound - - thread t; - shared_ptr> test_status; - - bool is_testing() const { return bool(test_status); } - void join() { t.join(); } - - FanThread &operator=(FanThread &&other) noexcept; -}; - class Controller { public: explicit Controller(path conf_path_); ~Controller(); - ControllerState state; Devices devices; - - void control(fc::FanInterface &fan); - void test(fc::FanInterface &fan, bool forced, function cb); - bool testing(const string &label) const; - size_t tests_running() const; - - void enable(); - bool enabled() const; - void disable(); - bool disabled() const; + FThreads_map fthreads; + std::list> devices_observers; + + FanStatus status(const string &flabel) const; + void enable(fc::FanInterface &fan); + void enable_all(); + void disable(const string &flabel); + void disable_all(); void reload(); void reload_added(); - bool reloading() const; void nv_init(); + void test(fc::FanInterface &fan, bool forced, function cb); + size_t tests_running() const; void from(const fc_pb::Controller &c); void from(const fc_pb::ControllerConfig &c); @@ -77,16 +57,16 @@ class Controller { private: path config_path; + optional watcher; milliseconds update_interval{1000}; - std::map threads; fs::file_time_type config_write_time; void load_conf_and_enumerated(); void to_file(bool backup = true); - void update_config_write_time(); bool config_file_modified() const; - void join_threads(); + thread spawn_watcher(); + void notify_devices_observers() const; static string date_time_now(); }; } // namespace fc diff --git a/src/Devices.cpp b/src/Devices.cpp index e5e01de..89f875a 100644 --- a/src/Devices.cpp +++ b/src/Devices.cpp @@ -76,7 +76,10 @@ void fc::SensorChips::enumerate(FanMap &fans, SensorMap &sensors) { } } -fc::Devices::Devices(bool dry_run) { +fc::Devices::Devices(bool enumerate, bool dry_run) { + if (!enumerate) + return; + SensorChips().enumerate(fans, sensors); #ifdef FANCON_NVIDIA_SUPPORT @@ -172,7 +175,6 @@ void fc::Devices::from(const fc_pb::Devices &d) { if (f->valid()) { const string uid = f->hw_id(), label = f->label; if (uids.count(uid) == 0) { - LOG(llvl::debug) << *f << ": imported"; uids.emplace(uid); fans.insert_or_assign(label, move(f)); } else { diff --git a/src/Devices.hpp b/src/Devices.hpp index 3782fd0..8908c08 100644 --- a/src/Devices.hpp +++ b/src/Devices.hpp @@ -31,7 +31,7 @@ class SensorChips { class Devices { public: Devices() = default; - explicit Devices(bool dry_run); + explicit Devices(bool enumerate, bool dry_run = false); FanMap fans; SensorMap sensors; diff --git a/src/FanInterface.cpp b/src/FanInterface.cpp index 09d99e2..6570a0b 100644 --- a/src/FanInterface.cpp +++ b/src/FanInterface.cpp @@ -79,7 +79,7 @@ bool fc::FanInterface::set_rpm(Rpm rpm) { } bool fc::FanInterface::recover_control() const { - for (auto i = 1; i <= 3; ++i, sleep_for(interval)) + for (auto i = 1; i <= 3; ++i, sleep_for_interval()) if (enable_control()) return true; diff --git a/src/FanThread.cpp b/src/FanThread.cpp new file mode 100644 index 0000000..542cd0c --- /dev/null +++ b/src/FanThread.cpp @@ -0,0 +1,16 @@ +#include "FanThread.hpp" + +fc::FanThread::FanThread(thread &&t, shared_ptr> testing_status) + : t(move(t)), test_status(move(testing_status)) {} + +fc::FanThread::~FanThread() { t.interrupt(); } + +bool fc::FanThread::is_testing() const { return bool(test_status); } + +void fc::FanThread::join() { t.join(); } + +fc::FanThread &fc::FanThread::operator=(fc::FanThread &&other) noexcept { + t = move(other.t); + test_status = other.test_status; + return *this; +} diff --git a/src/FanThread.hpp b/src/FanThread.hpp new file mode 100644 index 0000000..b067615 --- /dev/null +++ b/src/FanThread.hpp @@ -0,0 +1,26 @@ +#ifndef FANCON_SRC_FANTHREAD_HPP +#define FANCON_SRC_FANTHREAD_HPP + +#include "Util.hpp" +#include "boost/thread.hpp" + +using boost::thread; +using fc::Util::Observable; + +namespace fc { +class FanThread { +public: + explicit FanThread(thread &&t, + shared_ptr> testing_status = nullptr); + ~FanThread(); + + thread t; + shared_ptr> test_status = nullptr; + + bool is_testing() const; + void join(); + FanThread &operator=(FanThread &&other) noexcept; +}; +} // namespace fc + +#endif // FANCON_SRC_FANTHREAD_HPP diff --git a/src/Service.cpp b/src/Service.cpp index 0183d78..f36d8d0 100644 --- a/src/Service.cpp +++ b/src/Service.cpp @@ -7,7 +7,6 @@ fc::Service::Service(const path &config_path, bool daemon) } fc::Service::~Service() { - disable_controller(); if (server) server->Shutdown(); } @@ -26,7 +25,7 @@ void fc::Service::run() { .BuildAndStart(); if (server) { - enable_controller(); + controller.enable_all(); server->Wait(); } } catch (std::exception &e) { @@ -35,90 +34,136 @@ void fc::Service::run() { } } -void fc::Service::enable_controller() { - if (controller.disabled()) - controller_thread = thread([this] { controller.enable(); }); +GStatus fc::Service::StopService([[maybe_unused]] ServerContext *context, + [[maybe_unused]] const fc_pb::Empty *e, + [[maybe_unused]] fc_pb::Empty *resp) { + thread([&] { server->Shutdown(); }).detach(); + return GStatus::OK; } -void fc::Service::disable_controller() { - controller.disable(); - controller_thread.interrupt(); +GStatus fc::Service::GetDevices([[maybe_unused]] ServerContext *context, + [[maybe_unused]] const fc_pb::Empty *e, + fc_pb::Devices *devices) { + controller.devices.to(*devices); + return GStatus::OK; } -Status fc::Service::StopService([[maybe_unused]] ServerContext *context, - [[maybe_unused]] const fc_pb::Empty *e, - [[maybe_unused]] fc_pb::Empty *resp) { - disable_controller(); - thread([&] { server->Shutdown(); }).detach(); - return Status::OK; +GStatus fc::Service::SetDevices([[maybe_unused]] ServerContext *context, + const fc_pb::Devices *devices, + [[maybe_unused]] fc_pb::Empty *e) { + controller.devices = fc::Devices(); + controller.devices.from(*devices); + return GStatus::OK; } -Status fc::Service::Enable([[maybe_unused]] ServerContext *context, - [[maybe_unused]] const fc_pb::Empty *e, - [[maybe_unused]] fc_pb::Empty *resp) { - enable_controller(); - return Status::OK; +GStatus fc::Service::SubscribeDevices([[maybe_unused]] ServerContext *context, + [[maybe_unused]] const fc_pb::Empty *e, + ServerWriter *writer) { + { // Send the initial state + fc_pb::Devices resp; + controller.devices.to(resp); + writer->Write(resp); + } + + bool still_receiving = true; + auto cb = [&](const fc::Devices &devices) { + if (still_receiving) { + fc_pb::Devices resp; + devices.to(resp); + if (!writer->Write(resp)) + still_receiving = false; + } + }; + + // Register the listener + const auto it = controller.devices_observers.insert( + controller.devices_observers.end(), move(cb)); + + while (still_receiving) + sleep_for(milliseconds(200)); + + // Remove the listener once the client is no longer receiving + controller.devices_observers.erase(it); + + return GStatus::OK; } -Status fc::Service::Disable([[maybe_unused]] ServerContext *context, - [[maybe_unused]] const fc_pb::Empty *e, - [[maybe_unused]] fc_pb::Empty *resp) { - disable_controller(); - return Status::OK; +GStatus +fc::Service::GetEnumeratedDevices([[maybe_unused]] ServerContext *context, + [[maybe_unused]] const fc_pb::Empty *req, + fc_pb::Devices *devices) { + Devices(true, true).to(*devices); + return GStatus::OK; +} + +GStatus +fc::Service::GetControllerConfig([[maybe_unused]] ServerContext *context, + [[maybe_unused]] const fc_pb::Empty *e, + fc_pb::ControllerConfig *config) { + controller.to(*config); + return GStatus::OK; } -Status fc::Service::Reload([[maybe_unused]] ServerContext *context, - [[maybe_unused]] const fc_pb::Empty *e, - [[maybe_unused]] fc_pb::Empty *resp) { +GStatus +fc::Service::SetControllerConfig([[maybe_unused]] ServerContext *context, + const fc_pb::ControllerConfig *config, + [[maybe_unused]] fc_pb::Empty *e) { + controller.from(*config); controller.reload(); - return Status::OK; + return GStatus::OK; } -Status fc::Service::NvInit([[maybe_unused]] ServerContext *context, - [[maybe_unused]] const fc_pb::Empty *e, - [[maybe_unused]] fc_pb::Empty *resp) { - controller.nv_init(); - return Status::OK; +GStatus fc::Service::Status([[maybe_unused]] ServerContext *context, + const fc_pb::FanLabel *l, + fc_pb::FanStatus *status) { + if (!controller.devices.fans.contains(l->label())) + return GStatus(GStatusCode::NOT_FOUND, l->label()); + + status->set_status(controller.status(l->label())); + return GStatus::OK; } -Status fc::Service::ControllerStatus([[maybe_unused]] ServerContext *context, - [[maybe_unused]] const fc_pb::Empty *e, - fc_pb::ControllerState *resp) { - auto s = static_cast(controller.state); - resp->set_state(s); - return Status::OK; +GStatus fc::Service::Enable([[maybe_unused]] ServerContext *context, + const fc_pb::FanLabel *l, + [[maybe_unused]] fc_pb::Empty *e) { + auto it = controller.devices.fans.find(l->label()); + if (it == controller.devices.fans.end()) + return GStatus(GStatusCode::NOT_FOUND, l->label()); + + controller.enable(*it->second); + return GStatus::OK; } -Status fc::Service::GetDevices([[maybe_unused]] ServerContext *context, +GStatus fc::Service::EnableAll([[maybe_unused]] ServerContext *context, [[maybe_unused]] const fc_pb::Empty *e, - fc_pb::Devices *devices) { - controller.devices.to(*devices); - return Status::OK; + [[maybe_unused]] fc_pb::Empty *resp) { + controller.enable_all(); + return GStatus::OK; } -Status fc::Service::SetDevices([[maybe_unused]] ServerContext *context, - const fc_pb::Devices *devices, - [[maybe_unused]] fc_pb::Empty *e) { - controller.devices = fc::Devices(); - controller.devices.from(*devices); - return Status::OK; +GStatus fc::Service::Disable([[maybe_unused]] ServerContext *context, + const fc_pb::FanLabel *l, + [[maybe_unused]] fc_pb::Empty *resp) { + if (!controller.devices.fans.contains(l->label())) + return GStatus(GStatusCode::NOT_FOUND, l->label()); + + controller.disable(l->label()); + return GStatus::OK; } -Status -fc::Service::GetEnumeratedDevices([[maybe_unused]] ServerContext *context, - [[maybe_unused]] const fc_pb::Empty *req, - fc_pb::Devices *devices) { - Devices(true).to(*devices); - return Status::OK; +GStatus fc::Service::DisableAll([[maybe_unused]] ServerContext *context, + [[maybe_unused]] const fc_pb::Empty *e, + [[maybe_unused]] fc_pb::Empty *resp) { + controller.disable_all(); + return GStatus::OK; } -Status fc::Service::Test([[maybe_unused]] ServerContext *context, - const fc_pb::TestRequest *e, - ServerWriter *writer) { - auto fit = controller.devices.fans.find(e->device_label()); - if (fit == controller.devices.fans.end()) - // TODO: error << device isn't found - return Status::OK; +GStatus fc::Service::Test([[maybe_unused]] ServerContext *context, + const fc_pb::TestRequest *e, + ServerWriter *writer) { + auto it = controller.devices.fans.find(e->device_label()); + if (it == controller.devices.fans.end()) + return GStatus(GStatusCode::NOT_FOUND, e->device_label()); auto cb = [&](int &status) { fc_pb::TestResponse resp; @@ -129,24 +174,23 @@ Status fc::Service::Test([[maybe_unused]] ServerContext *context, writer->Write(resp); }; - controller.test(*fit->second, e->forced(), cb); + controller.test(*it->second, e->forced(), cb); - return Status::OK; + return GStatus::OK; } -Status fc::Service::GetControllerConfig([[maybe_unused]] ServerContext *context, - [[maybe_unused]] const fc_pb::Empty *e, - fc_pb::ControllerConfig *config) { - controller.to(*config); - return Status::OK; +GStatus fc::Service::Reload([[maybe_unused]] ServerContext *context, + [[maybe_unused]] const fc_pb::Empty *e, + [[maybe_unused]] fc_pb::Empty *resp) { + controller.reload(); + return GStatus::OK; } -Status fc::Service::SetControllerConfig([[maybe_unused]] ServerContext *context, - const fc_pb::ControllerConfig *config, - [[maybe_unused]] fc_pb::Empty *e) { - controller.from(*config); - controller.reload(); - return Status::OK; +GStatus fc::Service::NvInit([[maybe_unused]] ServerContext *context, + [[maybe_unused]] const fc_pb::Empty *e, + [[maybe_unused]] fc_pb::Empty *resp) { + controller.nv_init(); + return GStatus::OK; } void fc::Service::daemonize() { @@ -192,7 +236,7 @@ void fc::Service::daemonize() { // case SIGINT: // case SIGQUIT: // case SIGTERM: -// fc::Controller::disable(); +// fc::Controller::disable_all(); // SERVER->Shutdown(); // break; // default: diff --git a/src/Service.hpp b/src/Service.hpp index 040adbe..578fd0c 100644 --- a/src/Service.hpp +++ b/src/Service.hpp @@ -22,7 +22,8 @@ using grpc::ServerContext; using grpc::ServerReader; using grpc::ServerReaderWriter; using grpc::ServerWriter; -using grpc::Status; +using GStatus = grpc::Status; +using GStatusCode = grpc::StatusCode; using std::lock_guard; using std::mutex; @@ -34,42 +35,43 @@ class Service : public fc_pb::DService::Service { void run(); - Status StopService(ServerContext *context, const fc_pb::Empty *e, + GStatus StopService(ServerContext *context, const fc_pb::Empty *e, + fc_pb::Empty *resp) override; + GStatus GetDevices(ServerContext *context, const fc_pb::Empty *e, + fc_pb::Devices *devices) override; + GStatus SetDevices(ServerContext *context, const fc_pb::Devices *devices, + fc_pb::Empty *e) override; + GStatus SubscribeDevices(ServerContext *context, const fc_pb::Empty *e, + ServerWriter *writer) override; + GStatus GetEnumeratedDevices(ServerContext *context, const fc_pb::Empty *req, + fc_pb::Devices *devices) override; + GStatus GetControllerConfig(ServerContext *context, const fc_pb::Empty *e, + fc_pb::ControllerConfig *config) override; + GStatus SetControllerConfig(ServerContext *context, + const fc_pb::ControllerConfig *config, + fc_pb::Empty *e) override; + + GStatus Status(ServerContext *context, const fc_pb::FanLabel *l, + fc_pb::FanStatus *status) override; + GStatus Enable(ServerContext *context, const fc_pb::FanLabel *l, + fc_pb::Empty *resp) override; + GStatus EnableAll(ServerContext *context, const fc_pb::Empty *e, + fc_pb::Empty *resp) override; + GStatus Disable(ServerContext *context, const fc_pb::FanLabel *l, + fc_pb::Empty *resp) override; + GStatus DisableAll(ServerContext *context, const fc_pb::Empty *e, fc_pb::Empty *resp) override; - Status Enable(ServerContext *context, const fc_pb::Empty *e, - fc_pb::Empty *resp) override; - Status Disable(ServerContext *context, const fc_pb::Empty *e, + GStatus Test(ServerContext *context, const fc_pb::TestRequest *e, + ServerWriter *writer) override; + GStatus Reload(ServerContext *context, const fc_pb::Empty *e, + fc_pb::Empty *resp) override; + GStatus NvInit(ServerContext *context, const fc_pb::Empty *e, fc_pb::Empty *resp) override; - Status Reload(ServerContext *context, const fc_pb::Empty *e, - fc_pb::Empty *resp) override; - Status NvInit(ServerContext *context, const fc_pb::Empty *e, - fc_pb::Empty *resp) override; - Status ControllerStatus(ServerContext *context, const fc_pb::Empty *e, - fc_pb::ControllerState *resp) override; - - Status GetDevices(ServerContext *context, const fc_pb::Empty *e, - fc_pb::Devices *devices) override; - Status SetDevices(ServerContext *context, const fc_pb::Devices *devices, - fc_pb::Empty *e) override; - Status GetEnumeratedDevices(ServerContext *context, const fc_pb::Empty *req, - fc_pb::Devices *devices) override; - Status Test(ServerContext *context, const fc_pb::TestRequest *e, - ServerWriter *writer) override; - - Status GetControllerConfig(ServerContext *context, const fc_pb::Empty *e, - fc_pb::ControllerConfig *config) override; - Status SetControllerConfig(ServerContext *context, - const fc_pb::ControllerConfig *config, - fc_pb::Empty *e) override; private: fc::Controller controller; unique_ptr server; - thread controller_thread; - mutex test_writer_mutex; - void enable_controller(); - void disable_controller(); static void daemonize(); // static void signal_handler(int signal); diff --git a/src/Util.hpp b/src/Util.hpp index 97453c0..451c3d0 100644 --- a/src/Util.hpp +++ b/src/Util.hpp @@ -183,6 +183,7 @@ string fc::Util::map_str(const std::map m) { template void fc::Util::Observable::register_observer( std::function callback) { + callback(value); observers.emplace_back(move(callback)); } diff --git a/src/main.hpp b/src/main.hpp index 1c17c43..429313b 100644 --- a/src/main.hpp +++ b/src/main.hpp @@ -6,23 +6,14 @@ #include #endif // FANCON_PROFILE -#include -#include -#include -#include -#include #include -#include #include -#include #include -#include #include "Args.hpp" #include "Client.hpp" #include "Service.hpp" -using boost::interprocess::file_lock; using fc::Args; using fc::Client; using fc::Util::is_root; @@ -33,14 +24,6 @@ int main(int argc, char *argv[]); namespace fc { Args &read_args(int argc, char **argv, Args &args); void print_args(Args &args); - -// tuple instance_check(); -// void stop_instances(); -// void reload_instances(); -// void nv_init(); -// void offer_trailing_journal(); -// void print_directory(const path &dir, std::ostream &os, uint depth = 0); -// bool save_system_info(const path &config_path); } // namespace fc #endif // FANCON_MAIN_HPP