├── rcscripts ├── systemd │ ├── override.conf │ ├── thinkfan-sleep.service │ ├── thinkfan-wakeup.service │ └── thinkfan.service.cmake ├── sysvinit │ ├── thinkfan.default │ └── thinkfan.init └── openrc │ └── thinkfan.cmake ├── .github └── workflows │ ├── ccpp.yml │ └── codeql.yml ├── src ├── yamlconfig.h ├── wtf_ptr.h ├── temperature_state.h ├── hwmon.h ├── libsensors.h ├── driver.cpp ├── fans.h ├── driver.h ├── temperature_state.cpp ├── error.h ├── thinkfan.h ├── message.cpp ├── config.h ├── parser.h ├── sensors.h ├── error.cpp ├── libsensors.cpp ├── hwmon.cpp ├── thinkfan.1.cmake ├── fans.cpp ├── thinkfan.conf.legacy.5.cmake ├── message.h ├── parser.cpp ├── thinkfan.cpp ├── config.cpp ├── sensors.cpp └── thinkfan.conf.5.cmake ├── README.md ├── CMakeLists.txt └── examples └── thinkfan.yaml /rcscripts/systemd/override.conf: -------------------------------------------------------------------------------- 1 | [Service] 2 | # Decrease biasing (up to -b-10) if your fan speed changes too quickly, 3 | # Increase biasing (up to -b20) if your fan speed changes too slowly. 4 | Environment='THINKFAN_ARGS=-b0' 5 | -------------------------------------------------------------------------------- /rcscripts/sysvinit/thinkfan.default: -------------------------------------------------------------------------------- 1 | # Should thinkfan be started automatically on boot? 2 | # Only say "yes" when you know what you are doing, have configured 3 | # thinkfan correctly for *YOUR* machine and loaded thinkpad_acpi 4 | # with fan_control=1 (if you have a ThinkPad). 5 | START=no 6 | 7 | # Additional startup parameters 8 | DAEMON_ARGS="-q" 9 | -------------------------------------------------------------------------------- /rcscripts/systemd/thinkfan-sleep.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Notify thinkfan of imminent sleep 3 | Before=sleep.target 4 | 5 | [Service] 6 | Type=oneshot 7 | ExecStart=/usr/bin/pkill -x -pwr thinkfan 8 | # Hack: Since the signal handler races with the sleep, we need to delay a bit 9 | ExecStart=sleep 1 10 | 11 | [Install] 12 | WantedBy=sleep.target 13 | -------------------------------------------------------------------------------- /rcscripts/systemd/thinkfan-wakeup.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Reload thinkfan after waking up from suspend 3 | After=sysinit.target 4 | After=suspend.target 5 | After=suspend-then-hibernate.target 6 | After=hybrid-sleep.target 7 | After=hibernate.target 8 | 9 | [Service] 10 | Type=oneshot 11 | ExecStart=/usr/bin/pkill -x -usr2 thinkfan 12 | 13 | [Install] 14 | WantedBy=sleep.target 15 | -------------------------------------------------------------------------------- /rcscripts/openrc/thinkfan.cmake: -------------------------------------------------------------------------------- 1 | #!/sbin/openrc-run 2 | 3 | command="@CMAKE_INSTALL_PREFIX@/sbin/thinkfan" 4 | command_args="-q -s5 -c /etc/thinkfan.conf" 5 | pidfile="@PID_FILE@" 6 | 7 | extra_started_commands="reload" 8 | 9 | required_files="/etc/thinkfan.conf" 10 | 11 | depend() { 12 | after modules 13 | } 14 | 15 | reload() { 16 | ebegin "Reloading ${SVCNAME}" 17 | start-stop-daemon --signal HUP --pidfile "${pidfile}" 18 | eend $? 19 | } 20 | -------------------------------------------------------------------------------- /rcscripts/systemd/thinkfan.service.cmake: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=thinkfan @THINKFAN_VERSION@ 3 | Documentation=man:thinkfan 4 | After=sysinit.target 5 | After=systemd-modules-load.service 6 | 7 | [Service] 8 | Type=forking 9 | ExecStart=@CMAKE_INSTALL_PREFIX@/sbin/thinkfan $THINKFAN_ARGS 10 | PIDFile=/run/thinkfan.pid 11 | ExecReload=/bin/kill -HUP $MAINPID 12 | 13 | [Install] 14 | WantedBy=multi-user.target 15 | Also=thinkfan-sleep.service 16 | Also=thinkfan-wakeup.service 17 | -------------------------------------------------------------------------------- /.github/workflows/ccpp.yml: -------------------------------------------------------------------------------- 1 | name: C/C++ CI 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | build-ubuntu: 11 | runs-on: ${{ matrix.os }} 12 | strategy: 13 | matrix: 14 | os: [ubuntu-24.04, ubuntu-22.04] 15 | steps: 16 | - uses: actions/checkout@v2 17 | - name: install-deps 18 | run: sudo apt install libyaml-cpp-dev libatasmart-dev cmake libsensors-dev 19 | - name: build 20 | run: | 21 | mkdir build 22 | cmake -B build 23 | cmake --build build 24 | - name: install 25 | run: sudo cmake --install ${{ github.workspace }}/build 26 | -------------------------------------------------------------------------------- /src/yamlconfig.h: -------------------------------------------------------------------------------- 1 | #ifndef THINKFAN_YAMLCONFIG_H_ 2 | #define THINKFAN_YAMLCONFIG_H_ 3 | 4 | #include "thinkfan.h" 5 | #include "wtf_ptr.h" 6 | 7 | #include 8 | 9 | 10 | namespace YAML { 11 | 12 | using namespace thinkfan; 13 | 14 | 15 | const string kw_sensors("sensors"); 16 | const string kw_fans("fans"); 17 | const string kw_levels("levels"); 18 | const string kw_tpacpi("tpacpi"); 19 | const string kw_hwmon("hwmon"); 20 | #ifdef USE_NVML 21 | const string kw_nvidia("nvml"); 22 | #endif 23 | #ifdef USE_ATASMART 24 | const string kw_atasmart("atasmart"); 25 | #endif 26 | #ifdef USE_LM_SENSORS 27 | const string kw_chip("chip"); 28 | const string kw_ids("ids"); 29 | #endif 30 | const string kw_speed("speed"); 31 | const string kw_upper("upper_limit"); 32 | const string kw_lower("lower_limit"); 33 | const string kw_name("name"); 34 | const string kw_model("model"); 35 | const string kw_indices("indices"); 36 | const string kw_correction("correction"); 37 | const string kw_optional("optional"); 38 | const string kw_max_errors("max_errors"); 39 | 40 | 41 | template<> 42 | struct convert> { 43 | static bool decode(const Node &node, wtf_ptr &config); 44 | }; 45 | 46 | 47 | struct LevelEntry { 48 | vector> fan_levels; 49 | vector lower_limit; 50 | vector upper_limit; 51 | }; 52 | 53 | 54 | } 55 | 56 | 57 | #endif // THINKFAN_YAMLCONFIG_H_ 58 | -------------------------------------------------------------------------------- /src/wtf_ptr.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "thinkfan.h" 4 | 5 | 6 | namespace thinkfan { 7 | 8 | /** 9 | * This is the glorious wtf_ptr which unifies shared_ptr semantics with 10 | * the unique_ptr's ability to release(), i.e. relinquish ownership. 11 | * This weird hack is required since yaml-cpp cannot deal with non-copyable types like 12 | * unique_ptr. I'm sorry. 13 | */ 14 | template 15 | class wtf_ptr : public shared_ptr>, public std::enable_shared_from_this> { 16 | public: 17 | using shared_ptr>::shared_ptr; 18 | 19 | template 20 | wtf_ptr(wtf_ptr &&ptr) 21 | : shared_ptr>(new unique_ptr(ptr.release())) 22 | {} 23 | 24 | wtf_ptr() 25 | : shared_ptr>() 26 | {} 27 | 28 | wtf_ptr(T *ptr) 29 | : shared_ptr>(new unique_ptr(ptr)) 30 | {} 31 | 32 | auto release() 33 | { return *this ? this->get()->release() : nullptr; } 34 | 35 | auto operator -> () const 36 | { return this->get()->operator -> (); } 37 | 38 | auto operator * () const 39 | { return this->get()->operator * (); } 40 | 41 | template 42 | wtf_ptr &operator = (const wtf_ptr& ptr) noexcept 43 | { return this->shared_ptr>::operator = (ptr); } 44 | }; 45 | 46 | 47 | template 48 | wtf_ptr make_wtf(TArgs... args) 49 | { return wtf_ptr(new unique_ptr(new T(args...))); } 50 | 51 | 52 | } // namespace thinkfan 53 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | name: "CodeQL" 2 | 3 | on: 4 | push: 5 | branches: [ 'master' ] 6 | pull_request: 7 | # The branches below must be a subset of the branches above 8 | branches: [ 'master' ] 9 | schedule: 10 | - cron: '32 3 * * 4' 11 | 12 | jobs: 13 | analyze: 14 | name: Analyze 15 | runs-on: ubuntu-latest 16 | permissions: 17 | actions: read 18 | contents: read 19 | security-events: write 20 | 21 | strategy: 22 | fail-fast: false 23 | matrix: 24 | language: [ 'cpp' ] 25 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] 26 | # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support 27 | 28 | steps: 29 | - name: Checkout repository 30 | uses: actions/checkout@v3 31 | 32 | - name: install-deps 33 | run: sudo apt install libyaml-cpp-dev libatasmart-dev cmake libsensors-dev 34 | 35 | # Initializes the CodeQL tools for scanning. 36 | - name: Initialize CodeQL 37 | uses: github/codeql-action/init@v2 38 | with: 39 | languages: ${{ matrix.language }} 40 | # If you wish to specify custom queries, you can do so here or in a config file. 41 | # By default, queries listed here will override any specified in a config file. 42 | # Prefix the list here with "+" to use these queries and those in the config file. 43 | 44 | # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs 45 | queries: +security-and-quality 46 | 47 | 48 | - name: build 49 | run: | 50 | mkdir build 51 | cmake -B build 52 | cmake --build build 53 | 54 | - name: Perform CodeQL Analysis 55 | uses: github/codeql-action/analyze@v2 56 | with: 57 | category: "/language:${{matrix.language}}" 58 | -------------------------------------------------------------------------------- /src/temperature_state.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | /******************************************************************** 4 | * temperature_state.cpp: Storage & math for current temperatures 5 | * (C) 2022, Victor Mataré 6 | * 7 | * this file is part of thinkfan. 8 | * 9 | * thinkfan is free software: you can redistribute it and/or modify 10 | * it under the terms of the GNU General Public License as published by 11 | * the Free Software Foundation, either version 3 of the License, or 12 | * (at your option) any later version. 13 | * 14 | * thinkfan is distributed in the hope that it will be useful, 15 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 16 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 17 | * GNU General Public License for more details. 18 | * 19 | * You should have received a copy of the GNU General Public License 20 | * along with thinkfan. If not, see . 21 | * 22 | * ******************************************************************/ 23 | 24 | #include "thinkfan.h" 25 | 26 | namespace thinkfan { 27 | 28 | 29 | class TemperatureState { 30 | public: 31 | template 32 | using Iter = typename vector::iterator; 33 | 34 | class Ref { 35 | public: 36 | Ref(); 37 | void add_temp(int t); 38 | void skip_temp(); 39 | void restart(); 40 | 41 | private: 42 | friend TemperatureState; 43 | Ref(TemperatureState &ts, unsigned int offset); 44 | 45 | Iter temp0_; 46 | Iter bias0_; 47 | Iter biased_temp0_; 48 | 49 | Iter temp_; 50 | Iter bias_; 51 | Iter biased_temp_; 52 | 53 | TemperatureState *tstate_; 54 | }; 55 | 56 | TemperatureState(unsigned int num_temps); 57 | 58 | const vector &biased_temps() const; 59 | const vector &temps() const; 60 | const vector &biases() const; 61 | 62 | Ref ref(unsigned int num_temps); 63 | 64 | void reset_refd_count(); 65 | 66 | private: 67 | vector temps_; 68 | vector biases_; 69 | vector biased_temps_; 70 | unsigned int refd_temps_; 71 | 72 | public: 73 | vector::const_iterator tmax; 74 | }; 75 | 76 | 77 | } // namespace thinkfan 78 | -------------------------------------------------------------------------------- /src/hwmon.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | /******************************************************************** 4 | * hwmon.h: Helper functionality for sysfs hwmon interface 5 | * (C) 2022, Victor Mataré 6 | * 7 | * this file is part of thinkfan. See thinkfan.c for further information. 8 | * 9 | * thinkfan is free software: you can redistribute it and/or modify 10 | * it under the terms of the GNU General Public License as published by 11 | * the Free Software Foundation, either version 3 of the License, or 12 | * (at your option) any later version. 13 | * 14 | * thinkfan is distributed in the hope that it will be useful, 15 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 16 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 17 | * GNU General Public License for more details. 18 | * 19 | * You should have received a copy of the GNU General Public License 20 | * along with thinkfan. If not, see . 21 | * 22 | * ******************************************************************/ 23 | 24 | #include "thinkfan.h" 25 | 26 | #include 27 | 28 | namespace thinkfan { 29 | 30 | 31 | class HwmonSensorDriver; 32 | class HwmonFanDriver; 33 | 34 | 35 | template 36 | class HwmonInterface { 37 | public: 38 | HwmonInterface(); 39 | HwmonInterface(const string &base_path, opt name, opt model, opt> indices); 40 | 41 | string lookup(); 42 | 43 | private: 44 | static vector find_files(const string &path, const vector &indices); 45 | static string filename(unsigned int index); 46 | 47 | static vector find_hwmons_by_model(const string &path, const string &model, unsigned char depth); 48 | static vector find_hwmons_by_name(const string &path, const string &name, unsigned char depth); 49 | static vector find_hwmons_by_indices(const string &path, const vector &indices, unsigned char depth); 50 | 51 | protected: 52 | opt base_path_; 53 | opt name_; 54 | opt model_; 55 | opt> indices_; 56 | vector found_paths_; 57 | opt::const_iterator> paths_it_; 58 | }; 59 | 60 | 61 | } // namespace thinkfan 62 | -------------------------------------------------------------------------------- /src/libsensors.h: -------------------------------------------------------------------------------- 1 | /******************************************************************** 2 | * libsensors.h: State management for libsensors 3 | * (C) 2022, Victor Mataré 4 | * 5 | * this file is part of thinkfan. See thinkfan.c for further information. 6 | * 7 | * thinkfan is free software: you can redistribute it and/or modify 8 | * it under the terms of the GNU General Public License as published by 9 | * the Free Software Foundation, either version 3 of the License, or 10 | * (at your option) any later version. 11 | * 12 | * thinkfan is distributed in the hope that it will be useful, 13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | * GNU General Public License for more details. 16 | * 17 | * You should have received a copy of the GNU General Public License 18 | * along with thinkfan. If not, see . 19 | * 20 | * ******************************************************************/ 21 | 22 | #pragma once 23 | 24 | #include "thinkfan.h" 25 | 26 | #ifdef USE_LM_SENSORS 27 | #include 28 | #include 29 | #include 30 | 31 | namespace thinkfan { 32 | 33 | static const int MIN_CELSIUS_TEMP = -273; 34 | 35 | class LMSensorsDriver; 36 | 37 | class LibsensorsInterface 38 | { 39 | public: 40 | ~LibsensorsInterface(); 41 | LibsensorsInterface(const LibsensorsInterface &) = delete; 42 | LibsensorsInterface(LibsensorsInterface &&) = delete; 43 | 44 | static shared_ptr instance(); 45 | 46 | string lookup_client_features(LMSensorsDriver *client); 47 | vector get_temps(LMSensorsDriver *client); 48 | 49 | private: 50 | struct chip_features { 51 | const ::sensors_chip_name *chip = nullptr; 52 | vector> features; 53 | }; 54 | 55 | /** @brief A scope guard to un-initialize libsensors when a requested feature/subfeature 56 | * isn't found. This is necessary because libsensors doesn't pick up kernel drivers that 57 | * are loaded after initialization. */ 58 | class InitGuard { 59 | public: 60 | InitGuard(LMSensorsDriver *client); 61 | ~InitGuard(); 62 | private: 63 | LMSensorsDriver *client_; 64 | shared_ptr iface_; 65 | }; 66 | 67 | LibsensorsInterface(); 68 | friend class InitGuard; 69 | 70 | 71 | static void initialize_lm_sensors(); 72 | 73 | // LM sensors call backs. 74 | static void parse_error_callback(const char *err, int line_no); 75 | static void parse_error_wfn_callback(const char *err, const char *file_name, int line_no); 76 | static void fatal_error_callback(const char *proc, const char *err); 77 | 78 | string get_chip_name(const ::sensors_chip_name &chip); 79 | 80 | const ::sensors_chip_name *find_chip_by_name(const string& chip_name); 81 | 82 | const ::sensors_feature *find_feature_by_name( 83 | const ::sensors_chip_name &chip, 84 | const string &feature_name 85 | ); 86 | 87 | static std::weak_ptr instance_; 88 | 89 | std::map clients_; 90 | bool libsensors_initialized_; 91 | }; 92 | 93 | 94 | } 95 | 96 | #endif /* USE_LM_SENSORS */ 97 | -------------------------------------------------------------------------------- /src/driver.cpp: -------------------------------------------------------------------------------- 1 | /******************************************************************** 2 | * driver.h: Common functionality for all drivers 3 | * (C) 2022, Victor Mataré 4 | * 5 | * this file is part of thinkfan. See thinkfan.c for further information. 6 | * 7 | * thinkfan is free software: you can redistribute it and/or modify 8 | * it under the terms of the GNU General Public License as published by 9 | * the Free Software Foundation, either version 3 of the License, or 10 | * (at your option) any later version. 11 | * 12 | * thinkfan is distributed in the hope that it will be useful, 13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | * GNU General Public License for more details. 16 | * 17 | * You should have received a copy of the GNU General Public License 18 | * along with thinkfan. If not, see . 19 | * 20 | * ******************************************************************/ 21 | 22 | #include "driver.h" 23 | #include "message.h" 24 | 25 | namespace thinkfan { 26 | 27 | Driver::Driver(bool optional, unsigned int max_errors) 28 | : max_errors_(max_errors) 29 | , errors_(0) 30 | , optional_(optional) 31 | , initialized_(false) 32 | {} 33 | 34 | 35 | void Driver::try_init() 36 | { 37 | robust_op( 38 | [&]/* op_fn */() { 39 | if (!available()) 40 | path_.emplace(lookup()); 41 | init(); 42 | initialized_ = true; 43 | }, 44 | [&]/* skip_fn */(const ExpectedError &e) { 45 | log(optional() ? TF_DBG : TF_INF) << "Ignoring error "; 46 | if (max_errors() && !optional()) 47 | log() << errors() << "/" << max_errors() << " "; 48 | log() << "while initializing " << type_name() << ": " << e.what() << flush; 49 | } 50 | ); 51 | } 52 | 53 | 54 | void Driver::robust_op(FN op_fn, FN skip_fn) 55 | { 56 | try { 57 | errors_++; 58 | op_fn(); 59 | errors_ = 0; 60 | } catch (DriverInitError &e) { 61 | e.set_context(type_name()); 62 | handle_io_error_(e, skip_fn); 63 | } catch (SystemError &e) { 64 | handle_io_error_(e, skip_fn); 65 | } catch (IOerror &e) { 66 | handle_io_error_(e, skip_fn); 67 | } catch (std::ios_base::failure &e) { 68 | handle_io_error_(IOerror(e.what(), THINKFAN_IO_ERROR_CODE(e)), skip_fn); 69 | } 70 | } 71 | 72 | 73 | void Driver::handle_io_error_(const ExpectedError &e, FN skip_fn) 74 | { 75 | if (optional() || tolerate_errors || errors() < max_errors() || !chk_sanity) 76 | skip_fn(e); 77 | else 78 | throw e; 79 | } 80 | 81 | 82 | unsigned int Driver::errors() const 83 | { return errors_; } 84 | 85 | unsigned int Driver::max_errors() const 86 | { return std::max(max_errors_, static_cast(tolerate_errors)); } 87 | 88 | bool Driver::optional() const 89 | { return optional_; } 90 | 91 | const string &Driver::path() const 92 | { return path_.value(); } 93 | 94 | bool Driver::initialized() const 95 | { return initialized_; } 96 | 97 | bool Driver::available() const 98 | { return path_.has_value(); } 99 | 100 | void Driver::skip_io_error(const ExpectedError &e) 101 | { log(TF_ERR) << e.what() << flush; } 102 | 103 | 104 | 105 | } // namespace thinkfan 106 | -------------------------------------------------------------------------------- /src/fans.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | /******************************************************************** 4 | * config.h: Config data structures and consistency checking. 5 | * (C) 2015, Victor Mataré 6 | * 7 | * this file is part of thinkfan. See thinkfan.c for further information. 8 | * 9 | * thinkfan is free software: you can redistribute it and/or modify 10 | * it under the terms of the GNU General Public License as published by 11 | * the Free Software Foundation, either version 3 of the License, or 12 | * (at your option) any later version. 13 | * 14 | * thinkfan is distributed in the hope that it will be useful, 15 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 16 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 17 | * GNU General Public License for more details. 18 | * 19 | * You should have received a copy of the GNU General Public License 20 | * along with thinkfan. If not, see . 21 | * 22 | * ******************************************************************/ 23 | 24 | #include "thinkfan.h" 25 | #include "driver.h" 26 | #include "hwmon.h" 27 | 28 | namespace thinkfan { 29 | 30 | class Level; 31 | 32 | class FanDriver : public Driver { 33 | protected: 34 | FanDriver(bool optional, const unsigned int watchdog_timeout = 120, opt max_errors = nullopt); 35 | 36 | public: 37 | bool is_default() { return path().length() == 0; } 38 | virtual ~FanDriver() noexcept(false) override; 39 | virtual void set_speed(const Level &level) = 0; 40 | const string ¤t_speed() const; 41 | virtual void ping_watchdog_and_depulse(const Level &) {} 42 | bool operator == (const FanDriver &other) const; 43 | 44 | protected: 45 | void set_speed(const string &level); 46 | 47 | string initial_state_; 48 | string current_speed_; 49 | seconds watchdog_; 50 | secondsf depulse_; 51 | std::chrono::system_clock::time_point last_watchdog_ping_; 52 | 53 | private: 54 | virtual void skip_io_error(const ExpectedError &e) override; 55 | void set_speed_(const string &level); 56 | }; 57 | 58 | 59 | class TpFanDriver : public FanDriver { 60 | public: 61 | TpFanDriver(const string &path, bool optional = false, opt max_errors = nullopt); 62 | 63 | virtual ~TpFanDriver() noexcept(false) override; 64 | void set_watchdog(const unsigned int timeout); 65 | void set_depulse(float duration); 66 | virtual void set_speed(const Level &level) override; 67 | virtual void ping_watchdog_and_depulse(const Level &level) override; 68 | 69 | protected: 70 | virtual void init() override; 71 | virtual string lookup() override; 72 | virtual string type_name() const override; 73 | 74 | private: 75 | const string path_; 76 | }; 77 | 78 | 79 | class HwmonFanDriver : public FanDriver { 80 | public: 81 | HwmonFanDriver(const string &path); 82 | 83 | HwmonFanDriver( 84 | shared_ptr> hwmon_interface, 85 | bool optional, 86 | opt max_errors = nullopt 87 | ); 88 | 89 | virtual ~HwmonFanDriver() noexcept(false) override; 90 | virtual void set_speed(const Level &level) override; 91 | 92 | protected: 93 | virtual void init() override; 94 | virtual string lookup() override; 95 | virtual string type_name() const override; 96 | 97 | private: 98 | shared_ptr> hwmon_interface_; 99 | }; 100 | 101 | 102 | } 103 | -------------------------------------------------------------------------------- /src/driver.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | /******************************************************************** 4 | * driver.h: Common functionality for all drivers 5 | * (C) 2022, Victor Mataré 6 | * 7 | * this file is part of thinkfan. See thinkfan.c for further information. 8 | * 9 | * thinkfan is free software: you can redistribute it and/or modify 10 | * it under the terms of the GNU General Public License as published by 11 | * the Free Software Foundation, either version 3 of the License, or 12 | * (at your option) any later version. 13 | * 14 | * thinkfan is distributed in the hope that it will be useful, 15 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 16 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 17 | * GNU General Public License for more details. 18 | * 19 | * You should have received a copy of the GNU General Public License 20 | * along with thinkfan. If not, see . 21 | * 22 | * ******************************************************************/ 23 | 24 | #include "thinkfan.h" 25 | #include "error.h" 26 | #include 27 | 28 | namespace thinkfan { 29 | 30 | 31 | class Driver { 32 | protected: 33 | Driver(bool optional, unsigned int max_errors); 34 | 35 | virtual ~Driver() noexcept(false) 36 | {} 37 | 38 | template 39 | using FN = std::function; 40 | 41 | public: 42 | void try_init(); 43 | unsigned int errors() const; 44 | unsigned int max_errors() const; 45 | virtual bool optional() const; 46 | 47 | /** @return The identifier returned by @a lookup(). Calling this method before @a lookup() has completed 48 | * will result in an exception. */ 49 | const string &path() const; 50 | 51 | void robust_op(FN op_fn, FN skip_fn); 52 | 53 | template 54 | void robust_io(void (DriverT::*io_func)(ArgTs...), ArgTs &&... args); 55 | 56 | bool initialized() const; 57 | bool available() const; 58 | 59 | private: 60 | unsigned int max_errors_; 61 | unsigned int errors_; 62 | bool optional_; 63 | bool initialized_; 64 | 65 | void handle_io_error_(const ExpectedError &e, FN skip_fn); 66 | 67 | protected: 68 | virtual void init() = 0; 69 | 70 | /** @brief Identify the hardware resource to be associated with this config entry 71 | * @return A string that represents a valid identifier (e.g. a hwmon path) for the driver IFF 72 | * the driver's resource has been found and is ready to be initialized. Otherwise, throw an instance of 73 | * @a thinkfan::ExpectedError. 74 | * The string returned by this will be accessible via the @a path() method. */ 75 | virtual string lookup() = 0; 76 | 77 | /// @return A user-friendly name for the type of driver represented by the implementor 78 | virtual string type_name() const = 0; 79 | 80 | virtual void skip_io_error(const ExpectedError &); 81 | 82 | opt path_; 83 | }; 84 | 85 | 86 | template 87 | void Driver::robust_io(void (DriverT::*io_func)(ArgTs...), ArgTs &&... args) 88 | { 89 | using namespace std::placeholders; 90 | 91 | if (!available() || !initialized()) 92 | try_init(); 93 | 94 | if (initialized()) 95 | robust_op( 96 | [&] () { 97 | (static_cast(this)->*io_func)( 98 | std::forward(args)... 99 | ); 100 | }, 101 | std::bind(&Driver::skip_io_error, this, _1) 102 | ); 103 | } 104 | 105 | 106 | 107 | 108 | } // namespace thinkfan 109 | -------------------------------------------------------------------------------- /src/temperature_state.cpp: -------------------------------------------------------------------------------- 1 | /******************************************************************** 2 | * temperature_state.cpp: Storage & math for current temperatures 3 | * (C) 2022, Victor Mataré 4 | * 5 | * this file is part of thinkfan. 6 | * 7 | * thinkfan is free software: you can redistribute it and/or modify 8 | * it under the terms of the GNU General Public License as published by 9 | * the Free Software Foundation, either version 3 of the License, or 10 | * (at your option) any later version. 11 | * 12 | * thinkfan is distributed in the hope that it will be useful, 13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | * GNU General Public License for more details. 16 | * 17 | * You should have received a copy of the GNU General Public License 18 | * along with thinkfan. If not, see . 19 | * 20 | * ******************************************************************/ 21 | 22 | #include "temperature_state.h" 23 | #include "error.h" 24 | #include 25 | 26 | namespace thinkfan { 27 | 28 | 29 | TemperatureState::TemperatureState(unsigned int num_temps) 30 | : temps_(num_temps, 0), 31 | biases_(num_temps, 0), 32 | biased_temps_(num_temps, 0), 33 | refd_temps_(0), 34 | tmax(biased_temps_.begin()) 35 | {} 36 | 37 | TemperatureState::Ref::Ref(TemperatureState &ts, unsigned int offset) 38 | : temp0_(ts.temps_.begin() + offset), 39 | bias0_(ts.biases_.begin() + offset), 40 | biased_temp0_(ts.biased_temps_.begin() + offset), 41 | temp_(temp0_), 42 | bias_(bias0_), 43 | biased_temp_(biased_temp0_), 44 | tstate_(&ts) 45 | {} 46 | 47 | 48 | 49 | TemperatureState::Ref::Ref() 50 | {} 51 | 52 | void TemperatureState::Ref::restart() 53 | { 54 | temp_ = temp0_; 55 | bias_ = bias0_; 56 | biased_temp_ = biased_temp0_; 57 | } 58 | 59 | 60 | void TemperatureState::Ref::add_temp(int t) 61 | { 62 | int diff = *temp_ > 0 ? 63 | t - *temp_ 64 | : 0; 65 | *temp_ = t; 66 | 67 | if (unlikely(diff > 2)) { 68 | // Apply bias_ if temperature changed quickly 69 | *bias_ = int(float(diff) * bias_level); 70 | 71 | if (tmp_sleeptime > seconds(2)) 72 | tmp_sleeptime = seconds(2); 73 | } 74 | else { 75 | // Slowly return to normal sleeptime 76 | if (unlikely(tmp_sleeptime < sleeptime)) 77 | tmp_sleeptime++; 78 | // slowly reduce the bias_ 79 | #pragma GCC diagnostic push 80 | #pragma GCC diagnostic ignored "-Wfloat-equal" // bias is set to 0 explicitly 81 | if (unlikely(*bias_ != 0)) { 82 | #pragma GCC diagnostic pop 83 | if (std::abs(*bias_) < 0.5f) 84 | *bias_ = 0; 85 | else 86 | *bias_ -= std::copysign(1 + std::abs(*bias_)/5, *bias_); 87 | } 88 | } 89 | 90 | *biased_temp_ = *temp_ + int(*bias_); 91 | 92 | if (*biased_temp_ > *tstate_->tmax) 93 | tstate_->tmax = biased_temp_; 94 | 95 | skip_temp(); 96 | } 97 | 98 | void TemperatureState::Ref::skip_temp() 99 | { 100 | ++temp_; 101 | ++bias_; 102 | ++biased_temp_; 103 | } 104 | 105 | 106 | 107 | const vector & TemperatureState::biased_temps() const 108 | { return biased_temps_; } 109 | 110 | const vector & TemperatureState::temps() const 111 | { return temps_; } 112 | 113 | const vector & TemperatureState::biases() const 114 | { return biases_; } 115 | 116 | void TemperatureState::reset_refd_count() 117 | { refd_temps_ = 0; } 118 | 119 | 120 | TemperatureState::Ref TemperatureState::ref(unsigned int num_temps) 121 | { 122 | if (refd_temps_ + num_temps > temps_.size()) 123 | throw Bug("Attempting to reference uninitialized temperature state"); 124 | 125 | auto ref = TemperatureState::Ref(*this, refd_temps_); 126 | refd_temps_ += num_temps; 127 | return ref; 128 | } 129 | 130 | 131 | } // namespace thinkfan 132 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # thinkfan 2 | Thinkfan is a simple, lightweight fan control program. 3 | 4 | # WARNING 5 | There's only very basic sanity checking on the configuration (semantic 6 | plausibility). You can set the temperature limits as insane as you like. 7 | 8 | Any change to fan behaviour that results in higher temperatures in some parts 9 | of the system will shorten your system's lifetime and/or cause weird hardware 10 | bugs that'll make you pull out your hair. 11 | 12 | **No warranties whatsoever** 13 | 14 | If this program steals your car, kills your horse, smokes your dope or pees 15 | on your carpet... too bad, you're on your own. 16 | 17 | 18 | # Building and installing 19 | To compile thinkfan, you will need to have the following things installed: 20 | - A recent C++ compiler (GCC >= 4.8 or clang) 21 | - pkgconfig or an equivalent (pkgconf or pkg-config) 22 | - cmake (and optionally a cmake GUI if you want to configure interactively) 23 | - optional: libyaml-cpp for YAML support (the -dev or -devel package) 24 | 25 | E.g. on a debian-based system that usually boils down to: 26 | ```bash 27 | sudo apt install -y cmake-curses-gui build-essential cmake g++ libyaml-cpp-dev pkgconfig libsensors-dev 28 | ``` 29 | 30 | on EL/Fedora based system, usually : 31 | ```bash 32 | sudo dnf install -y cmake g++ pkgconfig yaml-cpp-devel lm_sensors-devel 33 | ``` 34 | 35 | 1. In the thinkfan main directory, do 36 | ```bash 37 | mkdir build && cd build 38 | ``` 39 | 40 | 2. Then configure your build, either interactively: 41 | ```bash 42 | ccmake .. 43 | ``` 44 | Or set your build options from the command line. E.g. to configure a build 45 | with full debugging support: 46 | ```bash 47 | cmake -D CMAKE_BUILD_TYPE:STRING=Debug .. 48 | ``` 49 | 50 | `CMAKE_BUILD_TYPE:STRING` can also be `Release`, which produces a fully 51 | optimized binary, or `RelWithDebInfo`, which is also optimized but can 52 | still be debugged with gdb. 53 | 54 | Other options are: 55 | 56 | `USE_NVML:BOOL` (default: `ON`) 57 | Allows thinkfan to read GPU temperatures from the proprietary nVidia 58 | driver. The interface library is loaded dynamically, so it does not 59 | need to be installed when compiling. 60 | 61 | `USE_ATASMART:BOOL` (default: `OFF`) 62 | Enable libatasmart to read temperatures directly from hard disks. Use 63 | this only when you really need it, since libatasmart is unreasonably 64 | CPU-intensive. 65 | 66 | `USE_LM_SENSORS:BOOL` (default: `ON`) 67 | Use LM sensors to read temperatures directly from Linux drivers. 68 | The `libsensors` library needs to be installed for this feature, probably 69 | with required headers and development files (e.g., `libsensors-dev`). 70 | 71 | `USE_YAML:BOOL` (default: `ON`) 72 | Support config file in the new, more flexible YAML format. The old 73 | config format will be deprecated after the thinkfan 1.0 release. New 74 | features will be supported in YAML configs only. See 75 | examples/thinkfan.conf.yaml. Requires libyaml-cpp. 76 | 77 | 78 | 3. To compile simply run: 79 | ```bash 80 | make 81 | ``` 82 | 83 | 4. If you did not change `CMAKE_INSTALL_PREFIX`, thinkfan will be installed 84 | under `/usr/local` by doing: 85 | ```bash 86 | sudo make install 87 | ``` 88 | 89 | CMake will detect whether you use OpenRC or systemd and install some 90 | appropriate service files. With systemd, you can edit the commandline 91 | arguments of the thinkfan service with `systemctl edit thinkfan`. 92 | With OpenRC, we install only a plain initscript (edit `/etc/init.d/thinkfan` 93 | to change options). 94 | 95 | 96 | 97 | # Documentation 98 | - Run `thinkfan -h` 99 | - Manpages: `thinkfan(1)`, `thinkfan.conf(5)` 100 | - Example configs: https://github.com/vmatare/thinkfan/tree/master/examples 101 | - Linux kernel hwmon doc: https://www.kernel.org/doc/html/latest/hwmon/sysfs-interface.html 102 | - Linux kernel thinkpad_acpi doc: https://www.kernel.org/doc/html/latest/admin-guide/laptops/thinkpad-acpi.html 103 | - If all fails: https://github.com/vmatare/thinkfan/issues 104 | -------------------------------------------------------------------------------- /src/error.h: -------------------------------------------------------------------------------- 1 | /******************************************************************** 2 | * error.h: Custom exceptions 3 | * (C) 2015, Victor Mataré 4 | * 5 | * this file is part of thinkfan. See thinkfan.c for further information. 6 | * 7 | * thinkfan is free software: you can redistribute it and/or modify 8 | * it under the terms of the GNU General Public License as published by 9 | * the Free Software Foundation, either version 3 of the License, or 10 | * (at your option) any later version. 11 | * 12 | * thinkfan is distributed in the hope that it will be useful, 13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | * GNU General Public License for more details. 16 | * 17 | * You should have received a copy of the GNU General Public License 18 | * along with thinkfan. If not, see . 19 | * 20 | * ******************************************************************/ 21 | 22 | #ifndef THINKFAN_ERROR_H_ 23 | #define THINKFAN_ERROR_H_ 24 | 25 | #include 26 | #include 27 | 28 | #include "thinkfan.h" 29 | 30 | #ifdef USE_YAML 31 | #include 32 | #include 33 | #endif 34 | 35 | #ifndef MAX_BACKTRACE_DEPTH 36 | #define MAX_BACKTRACE_DEPTH 64 37 | #endif 38 | 39 | namespace thinkfan { 40 | 41 | 42 | std::string demangle(const char* name); 43 | // Cf. http://stackoverflow.com/questions/281818/unmangling-the-result-of-stdtype-infoname 44 | template 45 | std::string type(const T &t) 46 | { return demangle(typeid(t).name()); } 47 | 48 | 49 | class Error : public std::exception { 50 | protected: 51 | string msg_; 52 | string backtrace_; 53 | public: 54 | Error(const string &message = ""); 55 | 56 | virtual const char* what() const noexcept override; 57 | const string &backtrace() const; 58 | }; 59 | 60 | 61 | class Bug : public Error { 62 | public: 63 | Bug(const string &desc = ""); 64 | }; 65 | 66 | class ExpectedError : public Error { 67 | using Error::Error; 68 | }; 69 | 70 | class IOerror : public ExpectedError { 71 | private: 72 | const int code_; 73 | public: 74 | IOerror(const string &message, const int error_code); 75 | int code(); 76 | }; 77 | 78 | 79 | class DriverLost : public ExpectedError { 80 | public: 81 | template 82 | DriverLost(const CauseT &cause) { 83 | msg_ = string("Lost driver ") + cause.what(); 84 | } 85 | }; 86 | 87 | 88 | class SyntaxError : public ExpectedError { 89 | public: 90 | SyntaxError(const string filename, const std::ptrdiff_t offset, const string &input); 91 | }; 92 | 93 | 94 | #ifdef USE_YAML 95 | 96 | class YamlError : public Error { 97 | public: 98 | YamlError(const YAML::Mark &mark = YAML::Mark::null_mark(), const string &msg = ""); 99 | YAML::Mark mark; 100 | }; 101 | 102 | class MissingEntry : public YamlError { 103 | public: 104 | MissingEntry(const string &entry); 105 | }; 106 | 107 | #endif 108 | 109 | 110 | class ConfigError : public ExpectedError { 111 | public: 112 | ConfigError(const string &reason); 113 | void set_filename(const string &filename); 114 | const string &filename() const; 115 | const string &reason() const; 116 | virtual const char* what() const noexcept override; 117 | #ifdef USE_YAML 118 | ConfigError(const string &filename, const YAML::Mark &mark, const string &input, const string &msg); 119 | #endif 120 | 121 | private: 122 | const string reason_; 123 | const string filename_; 124 | }; 125 | 126 | 127 | class ParserOOM : public ExpectedError {}; 128 | class MixedLevelSpecs : public ExpectedError {}; 129 | class LimitLengthMismatch : public ExpectedError {}; 130 | 131 | 132 | class SystemError : public ExpectedError { 133 | public: 134 | using ExpectedError::ExpectedError; 135 | }; 136 | 137 | 138 | class DriverInitError : public SystemError { 139 | public: 140 | using SystemError::SystemError; 141 | 142 | void set_context(const string &context); 143 | }; 144 | 145 | 146 | class InvocationError : public ExpectedError { 147 | public: 148 | InvocationError(const string &message); 149 | }; 150 | 151 | void handle_uncaught(); 152 | 153 | 154 | } 155 | 156 | #endif /* THINKFAN_ERROR_H_ */ 157 | -------------------------------------------------------------------------------- /src/thinkfan.h: -------------------------------------------------------------------------------- 1 | /******************************************************************** 2 | * thinkfan.h: Global (i.e. process-wide) stuff. 3 | * (C) 2015, Victor Mataré 4 | * 5 | * this file is part of thinkfan. See thinkfan.c for further information. 6 | * 7 | * thinkfan is free software: you can redistribute it and/or modify 8 | * it under the terms of the GNU General Public License as published by 9 | * the Free Software Foundation, either version 3 of the License, or 10 | * (at your option) any later version. 11 | * 12 | * thinkfan is distributed in the hope that it will be useful, 13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | * GNU General Public License for more details. 16 | * 17 | * You should have received a copy of the GNU General Public License 18 | * along with thinkfan. If not, see . 19 | * 20 | * ******************************************************************/ 21 | 22 | #ifndef THINKFAN_H_ 23 | #define THINKFAN_H_ 24 | 25 | #include 26 | #include 27 | #include 28 | #include 29 | #include 30 | #include 31 | #include 32 | #include 33 | 34 | #define DEFAULT_CONFIG "/etc/thinkfan.conf" 35 | #define DEFAULT_YAML_CONFIG "/etc/thinkfan.yaml" 36 | 37 | #ifndef DEFAULT_FAN 38 | #define DEFAULT_FAN "/proc/acpi/ibm/fan" 39 | #endif 40 | 41 | #ifndef DEFAULT_SENSOR 42 | #define DEFAULT_SENSOR "/proc/acpi/ibm/thermal" 43 | #endif 44 | 45 | 46 | // Stolen from the gurus 47 | #define likely(x) __builtin_expect((x),1) 48 | #define unlikely(x) __builtin_expect((x),0) 49 | 50 | #if defined(__GNUC__) && __GNUC__ < 5 51 | #define THINKFAN_IO_ERROR_CODE(e) errno 52 | #else 53 | #define THINKFAN_IO_ERROR_CODE(e) e.code().value() 54 | #endif 55 | 56 | namespace thinkfan { 57 | 58 | typedef std::string string; 59 | typedef std::ifstream ifstream; 60 | typedef std::ofstream ofstream; 61 | typedef std::fstream fstream; 62 | typedef std::chrono::duration seconds; 63 | typedef std::chrono::duration secondsf; 64 | 65 | template 66 | using vector = std::vector; 67 | 68 | template 69 | using shared_ptr = std::shared_ptr; 70 | 71 | template 72 | using unique_ptr = std::unique_ptr; 73 | 74 | template 75 | using pair = std::pair; 76 | 77 | template 78 | using numeric_limits = std::numeric_limits; 79 | 80 | template 81 | using opt = std::optional; 82 | 83 | template 84 | using ref = std::reference_wrapper; 85 | 86 | inline constexpr std::nullopt_t nullopt = std::nullopt; 87 | 88 | using std::forward; 89 | 90 | class Config; 91 | class Level; 92 | class Driver; 93 | class FanDriver; 94 | class SensorDriver; 95 | class HwmonSensorDriver; 96 | class HwmonFanDriver; 97 | class TpSensorDriver; 98 | class TpFanDriver; 99 | 100 | #ifdef USE_NVML 101 | class NvmlSensorDriver; 102 | #endif 103 | 104 | #ifdef USE_ATASMART 105 | class AtasmartSensorDriver; 106 | #endif 107 | 108 | #ifdef USE_LM_SENSORS 109 | class LMSensorsDriver; 110 | #endif 111 | 112 | 113 | #if defined(PID_FILE) 114 | 115 | class PidFileHolder { 116 | public: 117 | PidFileHolder(::pid_t pid); 118 | ~PidFileHolder(); 119 | static bool file_exists(); 120 | static void cleanup(); 121 | private: 122 | void remove_file(); 123 | 124 | std::fstream pid_file_; 125 | static PidFileHolder *instance_; 126 | }; 127 | 128 | #else 129 | 130 | class PidFileHolder { 131 | public: 132 | static void cleanup(); 133 | }; 134 | 135 | #endif // defined(PID_FILE) 136 | 137 | 138 | void sleep(thinkfan::seconds duration); 139 | 140 | void noop(); 141 | 142 | // Command line options 143 | extern bool chk_sanity; 144 | extern bool resume_is_safe; 145 | extern bool quiet; 146 | extern bool daemonize; 147 | #ifdef USE_ATASMART 148 | extern bool dnd_disk; 149 | #endif /* USE_ATASMART */ 150 | extern seconds sleeptime, tmp_sleeptime; 151 | extern float bias_level; 152 | extern std::atomic interrupted; 153 | extern vector config_files; 154 | extern float depulse; 155 | extern std::atomic tolerate_errors; 156 | 157 | extern std::condition_variable sleep_cond; 158 | extern std::mutex sleep_mutex; 159 | 160 | 161 | 162 | } 163 | #endif /* THINKFAN_H_ */ 164 | -------------------------------------------------------------------------------- /rcscripts/sysvinit/thinkfan.init: -------------------------------------------------------------------------------- 1 | #! /bin/sh 2 | ### BEGIN INIT INFO 3 | # Provides: thinkfan 4 | # Required-Start: $remote_fs 5 | # Required-Stop: $remote_fs 6 | # Default-Start: 2 3 4 5 7 | # Default-Stop: 0 1 6 8 | # Short-Description: thinkfan initscript 9 | # Description: starts thinkfan if enabled in /etc/default/thinkfan 10 | ### END INIT INFO 11 | 12 | # Author: Evgeni Golov 13 | 14 | # Do NOT "set -e" 15 | 16 | # PATH should only include /usr/* if it runs after the mountnfs.sh script 17 | PATH=/sbin:/usr/sbin:/bin:/usr/bin 18 | DESC="fan control tool" 19 | NAME=thinkfan 20 | DAEMON=/usr/sbin/$NAME 21 | DAEMON_ARGS="-q" 22 | # This one is compiled-in, you can't change it here! 23 | PIDFILE=/var/run/$NAME.pid 24 | SCRIPTNAME=/etc/init.d/$NAME 25 | START=no 26 | 27 | # Exit if the package is not installed 28 | [ -x "$DAEMON" ] || exit 0 29 | 30 | # Read configuration variable file if it is present 31 | [ -r /etc/default/$NAME ] && . /etc/default/$NAME 32 | 33 | # Check if daemon is to be started 34 | [ "$START" = "yes" ] || exit 0 35 | 36 | # Load the VERBOSE setting and other rcS variables 37 | . /lib/init/vars.sh 38 | 39 | # Define LSB log_* functions. 40 | # Depend on lsb-base (>= 3.0-6) to ensure that this file is present. 41 | . /lib/lsb/init-functions 42 | 43 | # 44 | # Function that starts the daemon/service 45 | # 46 | do_start() 47 | { 48 | # Return 49 | # 0 if daemon has been started 50 | # 1 if daemon was already running 51 | # 2 if daemon could not be started 52 | start-stop-daemon --start --quiet --pidfile $PIDFILE --exec $DAEMON --test > /dev/null \ 53 | || return 1 54 | start-stop-daemon --start --quiet --pidfile $PIDFILE --exec $DAEMON -- \ 55 | $DAEMON_ARGS \ 56 | || return 2 57 | # Add code here, if necessary, that waits for the process to be ready 58 | # to handle requests from services started subsequently which depend 59 | # on this one. As a last resort, sleep for some time. 60 | } 61 | 62 | # 63 | # Function that stops the daemon/service 64 | # 65 | do_stop() 66 | { 67 | # Return 68 | # 0 if daemon has been stopped 69 | # 1 if daemon was already stopped 70 | # 2 if daemon could not be stopped 71 | # other if a failure occurred 72 | start-stop-daemon --stop --quiet --retry=TERM/30/KILL/5 --pidfile $PIDFILE --name $NAME 73 | RETVAL="$?" 74 | [ "$RETVAL" = 2 ] && return 2 75 | # Wait for children to finish too if this is a daemon that forks 76 | # and if the daemon is only ever run from this initscript. 77 | # If the above conditions are not satisfied then add some other code 78 | # that waits for the process to drop all resources that could be 79 | # needed by services started subsequently. A last resort is to 80 | # sleep for some time. 81 | start-stop-daemon --stop --quiet --oknodo --retry=0/30/KILL/5 --exec $DAEMON 82 | [ "$?" = 2 ] && return 2 83 | # Many daemons don't delete their pidfiles when they exit. 84 | rm -f $PIDFILE 85 | return "$RETVAL" 86 | } 87 | 88 | # 89 | # Function that sends a SIGHUP to the daemon/service 90 | # 91 | do_reload() { 92 | # 93 | # If the daemon can reload its configuration without 94 | # restarting (for example, when it is sent a SIGHUP), 95 | # then implement that here. 96 | # 97 | start-stop-daemon --stop --signal 1 --quiet --pidfile $PIDFILE --name $NAME 98 | return 0 99 | } 100 | 101 | case "$1" in 102 | start) 103 | [ "$VERBOSE" != no ] && log_daemon_msg "Starting $DESC" "$NAME" 104 | do_start 105 | case "$?" in 106 | 0|1) [ "$VERBOSE" != no ] && log_end_msg 0 ;; 107 | 2) [ "$VERBOSE" != no ] && log_end_msg 1 ;; 108 | esac 109 | ;; 110 | stop) 111 | [ "$VERBOSE" != no ] && log_daemon_msg "Stopping $DESC" "$NAME" 112 | do_stop 113 | case "$?" in 114 | 0|1) [ "$VERBOSE" != no ] && log_end_msg 0 ;; 115 | 2) [ "$VERBOSE" != no ] && log_end_msg 1 ;; 116 | esac 117 | ;; 118 | status) 119 | status_of_proc "$DAEMON" "$NAME" && exit 0 || exit $? 120 | ;; 121 | reload|force-reload) 122 | log_daemon_msg "Reloading $DESC" "$NAME" 123 | do_reload 124 | log_end_msg $? 125 | ;; 126 | restart) 127 | log_daemon_msg "Restarting $DESC" "$NAME" 128 | do_stop 129 | case "$?" in 130 | 0|1) 131 | do_start 132 | case "$?" in 133 | 0) log_end_msg 0 ;; 134 | 1) log_end_msg 1 ;; # Old process is still running 135 | *) log_end_msg 1 ;; # Failed to start 136 | esac 137 | ;; 138 | *) 139 | # Failed to stop 140 | log_end_msg 1 141 | ;; 142 | esac 143 | ;; 144 | *) 145 | #echo "Usage: $SCRIPTNAME {start|stop|restart|reload|force-reload}" >&2 146 | echo "Usage: $SCRIPTNAME {start|stop|restart|status|restart|force-reload}" >&2 147 | exit 3 148 | ;; 149 | esac 150 | 151 | : 152 | -------------------------------------------------------------------------------- /src/message.cpp: -------------------------------------------------------------------------------- 1 | /******************************************************************** 2 | * message.cpp: Logging and error management. 3 | * (C) 2015, Victor Mataré 4 | * 5 | * this file is part of thinkfan. See thinkfan.c for further information. 6 | * 7 | * thinkfan is free software: you can redistribute it and/or modify 8 | * it under the terms of the GNU General Public License as published by 9 | * the Free Software Foundation, either version 3 of the License, or 10 | * (at your option) any later version. 11 | * 12 | * thinkfan is distributed in the hope that it will be useful, 13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | * GNU General Public License for more details. 16 | * 17 | * You should have received a copy of the GNU General Public License 18 | * along with thinkfan. If not, see . 19 | * 20 | * ******************************************************************/ 21 | 22 | #include "thinkfan.h" 23 | #include "message.h" 24 | #include "config.h" 25 | #include "fans.h" 26 | #include 27 | #include 28 | 29 | 30 | namespace thinkfan { 31 | 32 | unique_ptr Logger::instance_(nullptr); 33 | 34 | 35 | LogLevel &operator--(LogLevel &l) 36 | { 37 | if (l == TF_DBG) 38 | l = TF_INF; 39 | else if (l == TF_INF) 40 | l = TF_NFY; 41 | else if (l == TF_NFY) 42 | l = TF_WRN; 43 | else 44 | l = TF_ERR; 45 | return l; 46 | } 47 | 48 | 49 | LogLevel &operator++(LogLevel &l) 50 | { 51 | if (l == TF_ERR) 52 | l = TF_WRN; 53 | else if (l == TF_WRN) 54 | l = TF_NFY; 55 | else if (l == TF_NFY) 56 | l = TF_INF; 57 | else if (l == TF_INF) 58 | l = TF_DBG; 59 | return l; 60 | } 61 | 62 | 63 | Logger &log(LogLevel lvl) 64 | { 65 | Logger::instance().level(lvl); 66 | return Logger::instance(); 67 | } 68 | 69 | 70 | Logger &flush(Logger &l) 71 | { return l.flush(); } 72 | 73 | Logger &log() 74 | { return Logger::instance(); } 75 | 76 | 77 | 78 | 79 | 80 | Logger::Logger() 81 | : syslog_(false), 82 | log_lvl_(DEFAULT_LOG_LVL), 83 | msg_lvl_(DEFAULT_LOG_LVL) 84 | {} 85 | 86 | 87 | Logger &Logger::instance() 88 | { 89 | if (!instance_) instance_ = unique_ptr(new Logger()); 90 | return *instance_; 91 | } 92 | 93 | 94 | LogLevel &Logger::log_lvl() 95 | { return log_lvl_; } 96 | 97 | 98 | Logger::~Logger() 99 | { 100 | flush(); 101 | if (syslog_) closelog(); 102 | } 103 | 104 | 105 | void Logger::enable_syslog() 106 | { 107 | #ifndef DISABLE_SYSLOG 108 | openlog("thinkfan", LOG_CONS, LOG_USER); 109 | syslog_ = true; 110 | #endif //DISABLE_SYSLOG 111 | } 112 | 113 | 114 | Logger &Logger::flush() 115 | { 116 | if (msg_pfx_.length() == 0) 117 | return *this; 118 | if (msg_lvl_ <= log_lvl_) { 119 | if (syslog_) 120 | syslog(msg_lvl_, "%s", msg_pfx_.c_str()); 121 | else 122 | std::cerr << msg_pfx_ << std::endl; 123 | } 124 | msg_pfx_ = ""; 125 | 126 | return *this; 127 | } 128 | 129 | 130 | Logger &Logger::level(const LogLevel &lvl) 131 | { 132 | flush(); 133 | if (!syslog_ && msg_lvl_ != lvl && lvl >= log_lvl_ && msg_lvl_ >= log_lvl_) 134 | msg_pfx_ = "\n"; 135 | else 136 | msg_pfx_ = ""; 137 | 138 | if (lvl == TF_WRN) 139 | msg_pfx_ += "WARNING: "; 140 | else if (lvl == TF_ERR) 141 | msg_pfx_ += "ERROR: "; 142 | 143 | this->msg_lvl_ = lvl; 144 | return *this; 145 | } 146 | 147 | 148 | Logger &Logger::operator<<( const std::string &msg) 149 | { msg_pfx_ += msg; return *this; } 150 | 151 | Logger &Logger::operator<< (const int i) 152 | { msg_pfx_ += std::to_string(i); return *this; } 153 | 154 | Logger &Logger::operator<< (const unsigned int i) 155 | { msg_pfx_ += std::to_string(i); return *this; } 156 | 157 | Logger &Logger::operator<< (const float &i) 158 | { msg_pfx_ += std::to_string(i); return *this; } 159 | 160 | Logger &Logger::operator<< (const char *msg) 161 | { msg_pfx_ += msg; return *this; } 162 | 163 | Logger &Logger::operator<< (char *msg) 164 | { msg_pfx_ += msg; return *this; } 165 | 166 | 167 | Logger &Logger::operator<< (Logger & (*pf_flush)(Logger &)) 168 | { return pf_flush(*this); } 169 | 170 | 171 | Logger &Logger::operator<< (const TemperatureState &ts) 172 | { 173 | msg_pfx_ += "Temperatures(bias): "; 174 | 175 | vector::const_iterator bias_it; 176 | vector::const_iterator temp_it; 177 | 178 | for (temp_it = ts.temps().cbegin(), bias_it = ts.biases().cbegin(); 179 | temp_it != ts.temps().cend() && bias_it != ts.biases().cend(); 180 | ++temp_it, ++bias_it) 181 | msg_pfx_ += std::to_string(*temp_it) + "(" + std::to_string(int(*bias_it)) + "), "; 182 | 183 | msg_pfx_.pop_back(); msg_pfx_.pop_back(); 184 | return *this; 185 | } 186 | 187 | 188 | Logger &Logger::operator<< (const vector> &fan_configs) 189 | { 190 | msg_pfx_ += "Fans: "; 191 | for (const unique_ptr &fan_cf : fan_configs) 192 | msg_pfx_ += fan_cf->fan()->current_speed() + ", "; 193 | 194 | msg_pfx_.pop_back(); msg_pfx_.pop_back(); 195 | return *this; 196 | } 197 | 198 | 199 | 200 | 201 | } 202 | -------------------------------------------------------------------------------- /src/config.h: -------------------------------------------------------------------------------- 1 | /******************************************************************** 2 | * config.h: Config data structures and consistency checking. 3 | * (C) 2015, Victor Mataré 4 | * 5 | * this file is part of thinkfan. See thinkfan.c for further information. 6 | * 7 | * thinkfan is free software: you can redistribute it and/or modify 8 | * it under the terms of the GNU General Public License as published by 9 | * the Free Software Foundation, either version 3 of the License, or 10 | * (at your option) any later version. 11 | * 12 | * thinkfan is distributed in the hope that it will be useful, 13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | * GNU General Public License for more details. 16 | * 17 | * You should have received a copy of the GNU General Public License 18 | * along with thinkfan. If not, see . 19 | * 20 | * ******************************************************************/ 21 | 22 | #ifndef THINKFAN_CONFIG_H_ 23 | #define THINKFAN_CONFIG_H_ 24 | 25 | #include "temperature_state.h" 26 | 27 | #include 28 | 29 | #include "thinkfan.h" 30 | 31 | namespace thinkfan { 32 | 33 | class FanConfig { 34 | public: 35 | FanConfig(unique_ptr && = nullptr); 36 | virtual ~FanConfig() = default; 37 | virtual void init_fanspeed(const TemperatureState &) = 0; 38 | virtual bool set_fanspeed(const TemperatureState &) = 0; 39 | virtual void ensure_consistency(const Config &) const = 0; 40 | void set_fan(unique_ptr &&); 41 | const unique_ptr &fan() const; 42 | 43 | private: 44 | unique_ptr fan_; 45 | }; 46 | 47 | 48 | class StepwiseMapping : public FanConfig { 49 | public: 50 | StepwiseMapping(unique_ptr && = nullptr); 51 | virtual ~StepwiseMapping() override = default; 52 | virtual void init_fanspeed(const TemperatureState &) override; 53 | virtual bool set_fanspeed(const TemperatureState &) override; 54 | virtual void ensure_consistency(const Config &) const override; 55 | void add_level(unique_ptr &&level); 56 | const vector> &levels() const; 57 | 58 | private: 59 | vector> levels_; 60 | vector>::const_iterator cur_lvl_; 61 | }; 62 | 63 | 64 | class Level { 65 | protected: 66 | string level_s_; 67 | int level_n_; 68 | vector lower_limit_; 69 | vector upper_limit_; 70 | public: 71 | Level(int level, int lower_limit, int upper_limit); 72 | Level(string level, int lower_limit, int upper_limit); 73 | Level(int level, const vector &lower_limit, const vector &upper_limit); 74 | Level(string level, const vector &lower_limit, const vector &upper_limit); 75 | 76 | virtual ~Level() = default; 77 | 78 | const vector &lower_limit() const; 79 | const vector &upper_limit() const; 80 | 81 | virtual bool up(const TemperatureState &) const = 0; 82 | virtual bool down(const TemperatureState &) const = 0; 83 | 84 | virtual void ensure_consistency(const Config &) const = 0; 85 | 86 | const string &str() const; 87 | int num() const; 88 | 89 | static int string_to_int(string &level); 90 | }; 91 | 92 | 93 | 94 | class SimpleLevel : public Level { 95 | public: 96 | SimpleLevel(int level, int lower_limit, int upper_limit); 97 | SimpleLevel(string level, int lower_limit, int upper_limit); 98 | virtual bool up(const TemperatureState &) const override; 99 | virtual bool down(const TemperatureState &) const override; 100 | virtual void ensure_consistency(const Config &) const override; 101 | }; 102 | 103 | 104 | class ComplexLevel : public Level { 105 | public: 106 | ComplexLevel(int level, const vector &lower_limit, const vector &upper_limit); 107 | ComplexLevel(string level, const vector &lower_limit, const vector &upper_limit); 108 | virtual bool up(const TemperatureState &) const override; 109 | virtual bool down(const TemperatureState &) const override; 110 | virtual void ensure_consistency(const Config &) const override; 111 | 112 | private: 113 | static string format_limit(const vector &limit); 114 | }; 115 | 116 | 117 | class Config { 118 | public: 119 | Config() = default; 120 | ~Config() = default; 121 | 122 | static const Config *read_config(const vector &filenames); 123 | void add_sensor(unique_ptr &&sensor); 124 | void add_fan_config(unique_ptr &&fan_cfg); 125 | void ensure_consistency() const; 126 | void init_fans() const; 127 | TemperatureState init_sensors() const; 128 | void init_temperature_refs(TemperatureState &tstate) const; 129 | void init(TemperatureState &ts) const; 130 | 131 | unsigned int num_temps() const; 132 | const vector> &sensors() const; 133 | const vector> &fan_configs() const; 134 | 135 | string src_file; 136 | private: 137 | static const Config *try_read_config(const string &data); 138 | void try_init_driver(Driver &drv) const; 139 | vector> sensors_; 140 | vector> temp_mappings_; 141 | }; 142 | 143 | 144 | } 145 | 146 | #endif /* THINKFAN_CONFIG_H_ */ 147 | -------------------------------------------------------------------------------- /src/parser.h: -------------------------------------------------------------------------------- 1 | /******************************************************************** 2 | * parser.h: Recursive descent parser for the config. 3 | * (C) 2015, Victor Mataré 4 | * 5 | * this file is part of thinkfan. See thinkfan.c for further information. 6 | * 7 | * thinkfan is free software: you can redistribute it and/or modify 8 | * it under the terms of the GNU General Public License as published by 9 | * the Free Software Foundation, either version 3 of the License, or 10 | * (at your option) any later version. 11 | * 12 | * thinkfan is distributed in the hope that it will be useful, 13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | * GNU General Public License for more details. 16 | * 17 | * You should have received a copy of the GNU General Public License 18 | * along with thinkfan. If not, see . 19 | * 20 | * ******************************************************************/ 21 | 22 | #ifndef THINKFAN_PARSER_H_ 23 | #define THINKFAN_PARSER_H_ 24 | 25 | #include 26 | #include 27 | #include 28 | 29 | #include "sensors.h" 30 | #include "fans.h" 31 | 32 | namespace thinkfan { 33 | 34 | class SimpleLevel; 35 | class ComplexLevel; 36 | class Config; 37 | static const string tpacpi_path = "/proc/acpi/ibm"; 38 | 39 | class ErrorTracker { 40 | protected: 41 | static const char *max_addr_; 42 | }; 43 | 44 | template 45 | class Parser : ErrorTracker { 46 | public: 47 | Parser() {} 48 | 49 | virtual ~Parser() = default; 50 | 51 | const char *get_max_addr() const 52 | { return max_addr_; } 53 | 54 | ResultT *parse(const char *&input) { 55 | const char *start = input; 56 | ResultT *rv = _parse(input); 57 | if (input > max_addr_) max_addr_ = input; 58 | if (!rv) input = start; 59 | return rv; 60 | } 61 | 62 | bool match(const char *&input) { 63 | ResultT *result = parse(input); 64 | if (result) { 65 | delete result; 66 | return true; 67 | } 68 | return false; 69 | } 70 | 71 | protected: 72 | virtual ResultT *_parse(const char *&input) = 0; 73 | }; 74 | 75 | 76 | class RegexParser : public Parser { 77 | private: 78 | regex_t *expr_; 79 | unsigned int data_idx_; 80 | string re_str; 81 | public: 82 | RegexParser(const string expr, const unsigned int data_idx = 0, bool match_nl = false); 83 | virtual ~RegexParser() override; 84 | virtual string *_parse(const char *&input) override; 85 | }; 86 | 87 | 88 | class IntParser : public Parser { 89 | public: 90 | virtual int *_parse(const char *&input) override; 91 | }; 92 | 93 | 94 | class CommentParser : public Parser { 95 | private: 96 | RegexParser comment_parser_; 97 | public: 98 | CommentParser(); 99 | virtual string *_parse(const char *&input) override; 100 | }; 101 | 102 | 103 | class KeywordParser : public RegexParser { 104 | public: 105 | KeywordParser(const std::string keyword); 106 | }; 107 | 108 | 109 | class FanParser : public Parser { 110 | public: 111 | virtual FanDriver *_parse(const char *&input) override; 112 | }; 113 | 114 | 115 | class IntListParser : public Parser> { 116 | private: 117 | IntParser int_parser_; 118 | RegexParser dot_parser_; 119 | bool allow_dot_; 120 | public: 121 | IntListParser(bool allow_dot = false); 122 | virtual vector *_parse(const char *&input) override; 123 | }; 124 | 125 | 126 | class EnclosureParser : public Parser { 127 | public: 128 | EnclosureParser(std::initializer_list bracket_pairs, bool nl = true); 129 | virtual string *_parse(const char *&input) override; 130 | 131 | bool open(const char *&input); 132 | bool close(const char *&input); 133 | string *content(const char *&input); 134 | private: 135 | string closing_; 136 | string content_; 137 | vector brackets_; 138 | bool nl_; 139 | }; 140 | 141 | 142 | class BracketParser : public EnclosureParser { 143 | public: 144 | BracketParser(); 145 | }; 146 | 147 | 148 | class SensorParser : public Parser { 149 | public: 150 | virtual SensorDriver *_parse(const char *&input) override; 151 | private: 152 | BracketParser bracket_parser_; 153 | }; 154 | 155 | 156 | class TupleParser : public Parser> { 157 | public: 158 | TupleParser(bool allow_dot = false) : int_list_parser_(allow_dot) {} 159 | virtual vector *_parse(const char *&input) override; 160 | private: 161 | BracketParser bracket_parser_; 162 | IntListParser int_list_parser_; 163 | }; 164 | 165 | 166 | class SimpleLevelParser : public Parser { 167 | public: 168 | SimpleLevelParser() {} 169 | virtual SimpleLevel *_parse(const char *&input) override; 170 | private: 171 | BracketParser bracket_parser_; 172 | }; 173 | 174 | 175 | class ComplexLevelParser : public Parser { 176 | public: 177 | ComplexLevelParser() {} 178 | virtual ComplexLevel *_parse(const char *&input) override; 179 | private: 180 | BracketParser bracket_parser_; 181 | }; 182 | 183 | 184 | class ConfigParser : public Parser { 185 | private: 186 | FanParser parser_fan; 187 | SensorParser parser_sensor; 188 | SimpleLevelParser parser_simple_lvl; 189 | ComplexLevelParser parser_complex_lvl; 190 | public: 191 | ConfigParser(); 192 | 193 | Config *parse_config(const char *&input) 194 | { return parse(input); } 195 | 196 | protected: 197 | virtual Config *_parse(const char *&input) override; 198 | }; 199 | 200 | 201 | } /* namespace thinkfan */ 202 | 203 | 204 | 205 | 206 | 207 | 208 | #endif /* THINKFAN_PARSER_H_ */ 209 | 210 | 211 | 212 | 213 | -------------------------------------------------------------------------------- /src/sensors.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | /******************************************************************** 4 | * config.h: Config data structures and consistency checking. 5 | * (C) 2015, Victor Mataré 6 | * 2021, Koutheir Attouchi 7 | * 8 | * this file is part of thinkfan. See thinkfan.c for further information. 9 | * 10 | * thinkfan is free software: you can redistribute it and/or modify 11 | * it under the terms of the GNU General Public License as published by 12 | * the Free Software Foundation, either version 3 of the License, or 13 | * (at your option) any later version. 14 | * 15 | * thinkfan is distributed in the hope that it will be useful, 16 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 17 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 18 | * GNU General Public License for more details. 19 | * 20 | * You should have received a copy of the GNU General Public License 21 | * along with thinkfan. If not, see . 22 | * 23 | * ******************************************************************/ 24 | 25 | #include "thinkfan.h" 26 | #include "error.h" 27 | #include "driver.h" 28 | #include "hwmon.h" 29 | #include "libsensors.h" 30 | #include "temperature_state.h" 31 | 32 | #ifdef USE_ATASMART 33 | #include 34 | #endif /* USE_ATASMART */ 35 | 36 | #ifdef USE_NVML 37 | #include 38 | #endif /* USE_NVML */ 39 | 40 | 41 | #include 42 | 43 | namespace thinkfan { 44 | 45 | class ExpectedError; 46 | 47 | template 48 | using optional = std::optional; 49 | 50 | 51 | class SensorDriver : public Driver { 52 | protected: 53 | SensorDriver(bool optional, opt> correction = nullopt, opt max_errors = nullopt); 54 | 55 | public: 56 | virtual ~SensorDriver() noexcept(false) override; 57 | unsigned int num_temps() const { return *num_temps_; } 58 | void set_correction(const vector &correction); 59 | bool operator == (const SensorDriver &other) const; 60 | 61 | void read_temps(); 62 | void init_temp_state_ref(TemperatureState::Ref &&); 63 | 64 | protected: 65 | virtual void init() override; 66 | void set_num_temps(unsigned int n); 67 | static inline int readstream(const string &path); 68 | virtual void skip_io_error(const ExpectedError &e) override; 69 | virtual void read_temps_() = 0; 70 | 71 | vector correction_; 72 | TemperatureState::Ref temp_state_; 73 | 74 | /** @brief Protocol: Throw SensorLost(e) or nothing 75 | * @param e The original error */ 76 | private: 77 | opt num_temps_; 78 | void check_correction_length(); 79 | }; 80 | 81 | 82 | class HwmonSensorDriver : public SensorDriver { 83 | public: 84 | HwmonSensorDriver(const string &path, bool optional); 85 | 86 | HwmonSensorDriver( 87 | shared_ptr> hwmon_interface, 88 | bool optional, 89 | opt correction = nullopt, 90 | opt max_errors = nullopt 91 | ); 92 | 93 | protected: 94 | virtual void init() override; 95 | virtual void read_temps_() override; 96 | virtual string lookup() override; 97 | virtual string type_name() const override; 98 | 99 | private: 100 | shared_ptr> hwmon_interface_; 101 | }; 102 | 103 | 104 | class TpSensorDriver : public SensorDriver { 105 | public: 106 | TpSensorDriver( 107 | string conf_path, 108 | bool optional, 109 | opt> temp_indices = nullopt, 110 | opt> correction = nullopt, 111 | opt max_errors = nullopt 112 | ); 113 | 114 | protected: 115 | virtual void init() override; 116 | virtual void read_temps_() override; 117 | virtual string lookup() override; 118 | virtual string type_name() const override; 119 | 120 | private: 121 | std::char_traits::off_type skip_bytes_; 122 | static const string skip_prefix_; 123 | vector in_use_; 124 | const opt> temp_indices_; 125 | const string conf_path_; 126 | }; 127 | 128 | 129 | #ifdef USE_ATASMART 130 | class AtasmartSensorDriver : public SensorDriver { 131 | public: 132 | AtasmartSensorDriver(string device_path, bool optional, opt> correction = nullopt, opt max_errors = nullopt); 133 | virtual ~AtasmartSensorDriver(); 134 | 135 | protected: 136 | virtual void init() override; 137 | virtual void read_temps_() override; 138 | virtual string lookup() override; 139 | virtual string type_name() const override; 140 | 141 | private: 142 | SkDisk *disk_; 143 | const string device_path_; 144 | }; 145 | #endif /* USE_ATASMART */ 146 | 147 | 148 | #ifdef USE_NVML 149 | class NvmlSensorDriver : public SensorDriver { 150 | public: 151 | NvmlSensorDriver(string bus_id, bool optional, opt> correction = nullopt, opt max_errors = nullopt); 152 | virtual ~NvmlSensorDriver() noexcept(false) override; 153 | 154 | protected: 155 | virtual void init() override; 156 | virtual void read_temps_() override; 157 | virtual string lookup() override; 158 | virtual string type_name() const override; 159 | 160 | private: 161 | const string bus_id_; 162 | nvmlDevice_t device_; 163 | void *nvml_so_handle_; 164 | 165 | // Pointers to dynamically loaded functions from libnvidia-ml.so 166 | nvmlReturn_t (*dl_nvmlInit_v2)(); 167 | nvmlReturn_t (*dl_nvmlDeviceGetHandleByPciBusId_v2)(const char *, nvmlDevice_t *); 168 | nvmlReturn_t (*dl_nvmlDeviceGetName)(nvmlDevice_t, char *, unsigned int); 169 | nvmlReturn_t (*dl_nvmlDeviceGetTemperature)(nvmlDevice_t, nvmlTemperatureSensors_t, unsigned int *); 170 | nvmlReturn_t (*dl_nvmlShutdown)(); 171 | 172 | }; 173 | #endif /* USE_NVML */ 174 | 175 | 176 | #ifdef USE_LM_SENSORS 177 | 178 | class LMSensorsDriver : public SensorDriver { 179 | public: 180 | LMSensorsDriver( 181 | string chip_name, 182 | std::vector feature_names, 183 | bool optional, 184 | opt> correction = nullopt, 185 | opt max_errors = nullopt 186 | ); 187 | virtual ~LMSensorsDriver() override; 188 | 189 | const string &chip_name() const; 190 | const vector &feature_names() const; 191 | void set_unavailable(); 192 | 193 | protected: 194 | virtual void init() override; 195 | virtual void read_temps_() override; 196 | virtual string lookup() override; 197 | virtual string type_name() const override; 198 | 199 | private: 200 | const string chip_name_; 201 | const std::vector feature_names_; 202 | shared_ptr libsensors_iface_; 203 | }; 204 | 205 | #endif /* USE_LM_SENSORS */ 206 | 207 | } 208 | 209 | -------------------------------------------------------------------------------- /src/error.cpp: -------------------------------------------------------------------------------- 1 | /******************************************************************** 2 | * error.cpp: Custom exceptions 3 | * (C) 2015, Victor Mataré 4 | * 5 | * this file is part of thinkfan. See thinkfan.c for further information. 6 | * 7 | * thinkfan is free software: you can redistribute it and/or modify 8 | * it under the terms of the GNU General Public License as published by 9 | * the Free Software Foundation, either version 3 of the License, or 10 | * (at your option) any later version. 11 | * 12 | * thinkfan is distributed in the hope that it will be useful, 13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | * GNU General Public License for more details. 16 | * 17 | * You should have received a copy of the GNU General Public License 18 | * along with thinkfan. If not, see . 19 | * 20 | * ******************************************************************/ 21 | 22 | #include "error.h" 23 | #include "message.h" 24 | 25 | #include 26 | #if defined(__GLIBC__) 27 | #include 28 | #endif 29 | 30 | #include 31 | #include 32 | #include 33 | 34 | #ifdef __GNUG__ 35 | #include 36 | #include 37 | #include 38 | #endif 39 | 40 | namespace thinkfan { 41 | 42 | 43 | static string make_backtrace() 44 | { 45 | #if defined(__GLIBC__) 46 | string backtrace_; 47 | void *bt_buffer[MAX_BACKTRACE_DEPTH]; 48 | int stack_depth = ::backtrace(bt_buffer, MAX_BACKTRACE_DEPTH); 49 | if (stack_depth == MAX_BACKTRACE_DEPTH) 50 | log(TF_ERR) << "Max backtrace depth reached. Backtrace may be incomplete." << flush; 51 | 52 | char **bt_pretty = backtrace_symbols(bt_buffer, stack_depth); 53 | for (int i=0; i < stack_depth; ++i) { 54 | string line(bt_pretty[i]); 55 | string::size_type i1 = line.rfind('('); 56 | string::size_type i2 = line.rfind('+'); 57 | if (i1 != string::npos && i2 != string::npos && i2 - i1 > 1 58 | && line[i1+1] != '+') // line contains a function name to demangle 59 | line = line.substr(0, i1+1) + demangle(line.substr(i1+1, i2-i1-1).c_str()) + line.substr(i2); 60 | backtrace_ += line; 61 | backtrace_ += '\n'; 62 | } 63 | free(bt_pretty); 64 | return backtrace_; 65 | #else 66 | return "[not supported by C library]"; 67 | #endif 68 | } 69 | 70 | 71 | #ifdef __GNUG__ 72 | // Cf. http://stackoverflow.com/questions/281818/unmangling-the-result-of-stdtype-infoname 73 | std::string demangle(const char* name) { 74 | 75 | int status = -4; // some arbitrary value to eliminate the compiler warning 76 | 77 | std::unique_ptr res { 78 | abi::__cxa_demangle(name, nullptr, nullptr, &status), 79 | std::free 80 | }; 81 | 82 | return (status==0) ? res.get() : name ; 83 | } 84 | 85 | #else 86 | 87 | // does nothing if not g++ 88 | std::string demangle(const char* name) { 89 | return name; 90 | } 91 | 92 | #endif 93 | 94 | 95 | Error::Error(const string &message) 96 | : msg_(message), 97 | backtrace_(make_backtrace()) 98 | {} 99 | 100 | 101 | const string &Error::backtrace() const 102 | { return backtrace_; } 103 | 104 | 105 | const char* Error::what() const noexcept 106 | { return msg_.c_str(); } 107 | 108 | 109 | IOerror::IOerror(const string &message, const int error_code) 110 | : ExpectedError(message + std::strerror(error_code)), 111 | code_(error_code) 112 | {} 113 | 114 | 115 | int IOerror::code() 116 | { return code_; } 117 | 118 | 119 | Bug::Bug(const string &desc) 120 | : Error(desc) 121 | {} 122 | 123 | 124 | void handle_uncaught() 125 | { 126 | std::string err = std::strerror(errno); 127 | try { 128 | std::rethrow_exception(std::current_exception()); 129 | } catch (const std::exception &e) { 130 | log(TF_ERR) << "Unhandled " << demangle(typeid(e).name()) << ": " 131 | << e.what() << "." << flush 132 | << "errno = " << err << "." << flush 133 | << flush << "Backtrace:" << flush 134 | << make_backtrace() << flush 135 | #if not defined(DISABLE_EXCEPTION_CATCHING) 136 | << MSG_BUG << flush 137 | #endif 138 | ; 139 | } 140 | // We can expect to be killed by SIGABRT after this function returns. 141 | PidFileHolder::cleanup(); 142 | } 143 | 144 | 145 | SyntaxError::SyntaxError(const string filename, const std::ptrdiff_t offset, const string &input) 146 | { 147 | unsigned int line = 1; 148 | msg_ += filename + ":"; 149 | std::string::size_type line_start = 0; 150 | unsigned int i = 0; 151 | for (i = 0; i < offset; ++i) { 152 | if (input[i] == '\n') { 153 | ++line; 154 | line_start = i + 1; 155 | } 156 | } 157 | msg_ += std::to_string(line) + ": Invalid syntax:\n"; 158 | std::string::size_type line_end = input.find('\n', line_start) - line_start; 159 | msg_ += input.substr(line_start, line_end) + '\n'; 160 | msg_ += std::string(offset - line_start, ' ') + "^\n"; 161 | } 162 | 163 | 164 | ConfigError::ConfigError(const string &reason) 165 | : ExpectedError(reason) 166 | , reason_(reason) 167 | {} 168 | 169 | void ConfigError::set_filename(const string &filename) 170 | { msg_ = filename + ":\n" + msg_; } 171 | 172 | const string &ConfigError::filename() const 173 | { return filename_; } 174 | 175 | const string &ConfigError::reason() const 176 | { return reason_; } 177 | 178 | const char* ConfigError::what() const noexcept 179 | { return msg_.c_str(); } 180 | 181 | 182 | #ifdef USE_YAML 183 | 184 | ConfigError::ConfigError(const string &filename, const YAML::Mark &mark, const string &input, const string &msg) 185 | : reason_(msg) 186 | , filename_(filename) 187 | { 188 | msg_ = filename + ":"; 189 | // Another workaround for Ubuntu's libyaml-cpp0.5v5 190 | if (mark.pos == -1 && mark.line == -1 && mark.column == -1) { 191 | msg_ += string(" ") + msg + "."; 192 | #ifdef HAVE_OLD_YAMLCPP 193 | msg_+= "\nYou have an ancient libyaml-cpp which can't give line numbers on errors. Please complain to your Linux distribution."; 194 | #endif 195 | } 196 | else { 197 | msg_ += std::to_string(mark.line + 1) + ":\n"; 198 | std::stringstream s(input); 199 | std::array line; 200 | for (int i = 0; i <= mark.line; i++) 201 | s.getline(&line[0], 1024); 202 | 203 | msg_ += line.data() + string("\n"); 204 | msg_ += string(static_cast(mark.column), ' ') + "^\n"; 205 | msg_ += msg + "."; 206 | } 207 | } 208 | 209 | YamlError::YamlError(const YAML::Mark &mark, const string &msg) 210 | : Error(msg), 211 | mark(mark) 212 | {} 213 | 214 | 215 | MissingEntry::MissingEntry(const string &entry) 216 | : YamlError(YAML::Mark::null_mark(), string("Missing `") + entry + "' entry.") 217 | {} 218 | 219 | #endif 220 | 221 | 222 | InvocationError::InvocationError(const string &message) 223 | : ExpectedError("Invalid command line: " + message) {} 224 | 225 | void DriverInitError::set_context(const string &context) 226 | { msg_ = context + ": " + msg_; } 227 | 228 | 229 | } 230 | -------------------------------------------------------------------------------- /src/libsensors.cpp: -------------------------------------------------------------------------------- 1 | /******************************************************************** 2 | * libsensors.cpp: State management for libsensors 3 | * (C) 2022, Victor Mataré 4 | * 2021, Koutheir Attouchi 5 | * 6 | * this file is part of thinkfan. See thinkfan.c for further information. 7 | * 8 | * thinkfan is free software: you can redistribute it and/or modify 9 | * it under the terms of the GNU General Public License as published by 10 | * the Free Software Foundation, either version 3 of the License, or 11 | * (at your option) any later version. 12 | * 13 | * thinkfan is distributed in the hope that it will be useful, 14 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | * GNU General Public License for more details. 17 | * 18 | * You should have received a copy of the GNU General Public License 19 | * along with thinkfan. If not, see . 20 | * 21 | * ******************************************************************/ 22 | 23 | #include "libsensors.h" 24 | #include "error.h" 25 | #include "sensors.h" 26 | #include "message.h" 27 | 28 | namespace thinkfan { 29 | 30 | std::weak_ptr LibsensorsInterface::instance_; 31 | 32 | 33 | LibsensorsInterface::LibsensorsInterface() 34 | : libsensors_initialized_(false) 35 | { 36 | ::sensors_parse_error = parse_error_callback; 37 | ::sensors_parse_error_wfn = parse_error_wfn_callback; 38 | ::sensors_fatal_error = fatal_error_callback; 39 | 40 | log(TF_DBG) << "Initialized LM sensors." << flush; 41 | } 42 | 43 | LibsensorsInterface::~LibsensorsInterface() 44 | { 45 | if (libsensors_initialized_) 46 | ::sensors_cleanup(); 47 | } 48 | 49 | 50 | LibsensorsInterface::InitGuard::InitGuard(LMSensorsDriver *client) 51 | : client_(client) 52 | , iface_(LibsensorsInterface::instance_.lock()) 53 | { 54 | if (!iface_->libsensors_initialized_) { 55 | int err; 56 | if ((err = ::sensors_init(nullptr))) 57 | throw SystemError(string("Failed to initialize LM sensors driver: ") + sensors_strerror(err)); 58 | iface_->libsensors_initialized_ = true; 59 | } 60 | } 61 | 62 | 63 | LibsensorsInterface::InitGuard::~InitGuard() 64 | { 65 | if (iface_->libsensors_initialized_ && iface_->clients_[client_].features.empty()) { 66 | 67 | // Make all clients unavailable (they have to lookup again!) 68 | for (auto drv_entry : iface_->clients_) 69 | drv_entry.first->set_unavailable(); 70 | iface_->clients_.clear(); 71 | 72 | ::sensors_cleanup(); 73 | iface_->libsensors_initialized_ = false; 74 | } 75 | } 76 | 77 | 78 | shared_ptr LibsensorsInterface::instance() 79 | { 80 | shared_ptr rv; 81 | if (instance_.expired()) { 82 | rv.reset(new LibsensorsInterface()); 83 | instance_ = rv; 84 | } 85 | else 86 | rv = instance_.lock(); 87 | 88 | return rv; 89 | } 90 | 91 | 92 | string LibsensorsInterface::lookup_client_features(LMSensorsDriver *client) 93 | { 94 | chip_features cf; 95 | InitGuard ig(client); 96 | 97 | cf.chip = find_chip_by_name(client->chip_name()); 98 | 99 | for (const string& feature_name : client->feature_names()) { 100 | auto feature = find_feature_by_name(*cf.chip, feature_name); 101 | if (!feature) 102 | throw SystemError("LM sensors chip '" + client->chip_name() 103 | + "' does not have the feature '" + feature_name + "'"); 104 | 105 | auto sub_feature = ::sensors_get_subfeature(cf.chip, feature, ::SENSORS_SUBFEATURE_TEMP_INPUT); 106 | if (!sub_feature) 107 | throw SystemError("LM sensors feature ID '" + feature_name 108 | + "' of the chip '" + client->chip_name() 109 | + "' does not have a temperature input sensor"); 110 | cf.features.push_back({feature, sub_feature}); 111 | 112 | log(TF_DBG) << "Initialized LM sensors temperature input of feature '" 113 | + feature_name + "' of chip '" + client->chip_name() + "'." << flush; 114 | } 115 | 116 | clients_.insert({client, cf}); 117 | return cf.chip->path; 118 | } 119 | 120 | 121 | vector LibsensorsInterface::get_temps(LMSensorsDriver *client) 122 | { 123 | chip_features &cf = clients_.at(client); 124 | vector rv; 125 | 126 | for (auto chip_feature : cf.features) { 127 | auto sub_feature = chip_feature.second; 128 | double real_value = MIN_CELSIUS_TEMP; 129 | 130 | int err = ::sensors_get_value(cf.chip, sub_feature->number, &real_value); 131 | if (err) 132 | throw SystemError( 133 | string("temperature input value of feature '") + chip_feature.first->name 134 | + "' of chip '" + client->chip_name() 135 | + "' is unavailable: " + ::sensors_strerror(err) 136 | ); 137 | else if (real_value < MIN_CELSIUS_TEMP) // Make sure the reported value is physically valid. 138 | throw SystemError( 139 | string("Invalid temperature on feature '") + chip_feature.first->name 140 | + "' of chip '" + client->chip_name() 141 | + "': " + std::to_string(real_value) 142 | ); 143 | 144 | rv.push_back(real_value); 145 | } 146 | 147 | return rv; 148 | } 149 | 150 | 151 | const ::sensors_chip_name* LibsensorsInterface::find_chip_by_name( 152 | const string& chip_name 153 | ) { 154 | int state = 0; 155 | for (;;) { 156 | auto chip = ::sensors_get_detected_chips(nullptr, &state); 157 | if (!chip) 158 | break; 159 | 160 | if (chip_name == get_chip_name(*chip)) 161 | return chip; 162 | } 163 | 164 | throw SystemError("LM sensors chip '" + chip_name + "' was not found"); 165 | } 166 | 167 | 168 | string LibsensorsInterface::get_chip_name(const ::sensors_chip_name& chip) 169 | { 170 | int len = sensors_snprintf_chip_name(nullptr, 0, &chip); 171 | if (len < 0) { 172 | const char *msg = ::sensors_strerror(len); 173 | throw SystemError(string("Failed to get LM sensors chip name: ") + msg); 174 | } 175 | 176 | vector buffer(len + 1); 177 | int w_sz = sensors_snprintf_chip_name(buffer.data(), size_t(len + 1), &chip); 178 | if (w_sz < 0) { 179 | const char *msg = ::sensors_strerror(w_sz); 180 | throw SystemError(string("Failed to get LM sensors chip name: ") + msg); 181 | } else if (w_sz >= (len + 1)) { 182 | throw SystemError("LM sensors chip name is too long"); 183 | } 184 | 185 | return string(buffer.data(), w_sz); 186 | } 187 | 188 | 189 | const ::sensors_feature* LibsensorsInterface::find_feature_by_name( 190 | const ::sensors_chip_name& chip, 191 | const string& feature_name 192 | ) { 193 | int state = 0; 194 | 195 | for (;;) { 196 | auto feature = ::sensors_get_features(&chip, &state); 197 | if (!feature) 198 | break; 199 | 200 | char *label = ::sensors_get_label(&chip, feature); 201 | bool label_matches = (feature_name == label); 202 | free(label); 203 | 204 | if (label_matches) 205 | return feature; 206 | } 207 | 208 | return nullptr; 209 | } 210 | 211 | 212 | void LibsensorsInterface::parse_error_callback(const char *err, int line_no) 213 | { 214 | log(TF_ERR) << "LM sensors parsing error: " << err << " in line " 215 | << std::to_string(line_no); 216 | } 217 | 218 | 219 | void LibsensorsInterface::parse_error_wfn_callback(const char *err, const char *file_name, int line_no) 220 | { 221 | log(TF_ERR) << "LM sensors parsing error: " << err << " in file '" 222 | << file_name << "' at line " << std::to_string(line_no); 223 | } 224 | 225 | 226 | void LibsensorsInterface::fatal_error_callback(const char *proc, const char *err) 227 | { 228 | log(TF_ERR) << "LM sensors fatal error in " << proc << ": " << err; 229 | 230 | // libsensors documentation for sensors_fatal_error() requires this 231 | // function to never return. 232 | // 233 | // We call abort() in order to generate a core dump in addition to reporting failure. 234 | abort(); 235 | } 236 | 237 | } 238 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.10) 2 | 3 | project(thinkfan LANGUAGES CXX) 4 | include(GNUInstallDirs) 5 | 6 | execute_process( 7 | COMMAND git describe --tags 8 | WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} 9 | RESULT_VARIABLE GIT_DESCRIBE_STATUS 10 | OUTPUT_VARIABLE THINKFAN_VERSION 11 | OUTPUT_STRIP_TRAILING_WHITESPACE 12 | ECHO_OUTPUT_VARIABLE 13 | ) 14 | if(NOT GIT_DESCRIBE_STATUS EQUAL 0) 15 | set(THINKFAN_VERSION 2.0.0) 16 | endif() 17 | 18 | # Generate absolute paths or something 19 | cmake_policy(SET CMP0015 NEW) 20 | 21 | find_package(PkgConfig) 22 | find_package(Threads) 23 | pkg_check_modules(SYSTEMD "systemd") 24 | pkg_check_modules(OPENRC "openrc") 25 | 26 | pkg_check_modules(YAML_CPP "yaml-cpp") 27 | 28 | if(YAML_CPP_FOUND AND YAML_CPP_VERSION VERSION_LESS "0.5.3") 29 | message(WARNING "yaml-cpp version ${YAML_CPP_VERSION} is very old, buggy and lacks some features. Thinkfan will not always be able to point out the location of errors in the YAML config.") 30 | add_definitions(-DHAVE_OLD_YAMLCPP) 31 | endif() 32 | 33 | pkg_check_modules(ATASMART "libatasmart") 34 | 35 | find_library(LM_SENSORS_LIB NAMES "libsensors.so" "libsensors.so.5") 36 | find_path(LM_SENSORS_INC NAMES "sensors/sensors.h") 37 | 38 | if(SYSTEMD_FOUND) 39 | set(PID_FILE "/run/thinkfan.pid") 40 | else() 41 | set(PID_FILE "/var/run/thinkfan.pid") 42 | endif() 43 | 44 | 45 | # 46 | # Defaults to OFF because libatasmart seems to be horribly inefficient 47 | # 48 | option(USE_ATASMART "Enable reading temperatures from HDDs via S.M.A.R.T" OFF) 49 | 50 | # 51 | # Defaults to ON because it seems reasonably fast. The libnvidia-ml.so is 52 | # loaded at runtime, so we don't add a compile-time dependency on the 53 | # proprietary nVidia driver. 54 | # 55 | option(USE_NVML "Get temperatures directly from nVidia GPUs via their proprietary NVML API" ON) 56 | 57 | # 58 | # Defaults to ON. 59 | # 60 | option(USE_LM_SENSORS "Get temperatures from LM sensors" ON) 61 | 62 | # 63 | # The shiny new YAML config parser. Depends on yaml-cpp. 64 | # 65 | option(USE_YAML "Enable the new YAML-based config format" ON) 66 | 67 | 68 | option(DISABLE_BUGGER "Disable bug detection, i.e. dont't catch segfaults and unhandled exceptions" OFF) 69 | option(DISABLE_SYSLOG "Disable logging to syslog, always log to stdout" OFF) 70 | option(DISABLE_EXCEPTION_CATCHING "Terminate with SIGABRT on all exceptions, causing a core dump on every error" OFF) 71 | 72 | 73 | set(SRC_FILES src/thinkfan.cpp src/config.cpp src/fans.cpp src/sensors.cpp 74 | src/driver.cpp 75 | src/hwmon.cpp 76 | src/libsensors.cpp 77 | src/temperature_state.cpp 78 | src/message.cpp src/parser.cpp src/error.cpp) 79 | 80 | if(USE_YAML) 81 | if(NOT YAML_CPP_FOUND) 82 | message(FATAL_ERROR "USE_YAML enabled but yaml-cpp not found. Please install yaml-cpp-devel (RedHat) or libyaml-cpp-dev (Debian)!") 83 | endif() 84 | set(SRC_FILES ${SRC_FILES} src/yamlconfig.cpp) 85 | endif(USE_YAML) 86 | 87 | 88 | # 89 | # Set default build type 90 | # 91 | if(NOT CMAKE_BUILD_TYPE) 92 | set(CMAKE_BUILD_TYPE "RelWithDebInfo" CACHE STRING "Options are: Debug Release RelWithDebInfo MinSizeRel." 93 | FORCE) 94 | endif(NOT CMAKE_BUILD_TYPE) 95 | 96 | add_compile_options(-Wall) 97 | set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -O0 -g3 -DDEBUG") 98 | 99 | add_executable(thinkfan ${SRC_FILES}) 100 | 101 | if (PID_FILE) 102 | target_compile_definitions(thinkfan PRIVATE -DPID_FILE=\"${PID_FILE}\") 103 | endif() 104 | target_compile_definitions(thinkfan PRIVATE -DVERSION="${THINKFAN_VERSION}") 105 | 106 | # std::condition_variable::wait_for doesn't block if not explicitly linked against libpthread 107 | # https://stackoverflow.com/questions/41394670/c-condition-variable-wait-for-returns-instantly 108 | # https://gcc.gnu.org/bugzilla/show_bug.cgi?id=58929 109 | target_link_libraries(thinkfan PRIVATE ${CMAKE_THREAD_LIBS_INIT}) 110 | 111 | set_property(TARGET thinkfan PROPERTY CXX_STANDARD 17) 112 | 113 | if(USE_ATASMART) 114 | if(NOT ATASMART_FOUND) 115 | message(FATAL_ERROR "USE_ATASMART enabled but libatasmart not found. Please install libatasmart[-devel]!") 116 | else() 117 | target_compile_definitions(thinkfan PRIVATE -DUSE_ATASMART) 118 | target_link_libraries(thinkfan PRIVATE atasmart) 119 | endif() 120 | endif(USE_ATASMART) 121 | 122 | if(USE_NVML) 123 | target_include_directories(thinkfan PRIVATE "include") 124 | target_compile_definitions(thinkfan PRIVATE -DUSE_NVML) 125 | target_link_libraries(thinkfan PRIVATE dl) 126 | endif(USE_NVML) 127 | 128 | if(USE_LM_SENSORS) 129 | if(LM_SENSORS_LIB MATCHES "LM_SENSORS_LIB-NOTFOUND") 130 | message(FATAL_ERROR "USE_LM_SENSORS enabled but libsensors not found. Please install libsensors-dev!") 131 | elseif(LM_SENSORS_INC MATCHES "LM_SENSORS_INC-NOTFOUND") 132 | message(FATAL_ERROR "USE_LM_SENSORS enabled but sensors/sensors.h not found. Please install libsensors-dev!") 133 | else() 134 | target_compile_definitions(thinkfan PRIVATE -DUSE_LM_SENSORS) 135 | target_include_directories(thinkfan PRIVATE ${LM_SENSORS_INC}) 136 | target_link_libraries(thinkfan PRIVATE ${LM_SENSORS_LIB}) 137 | endif() 138 | endif(USE_LM_SENSORS) 139 | 140 | if(USE_YAML) 141 | target_compile_definitions(thinkfan PRIVATE -DUSE_YAML) 142 | target_include_directories(thinkfan PRIVATE ${YAML_CPP_INCLUDE_DIRS}) 143 | target_link_libraries(thinkfan PRIVATE ${YAML_CPP_LIBRARIES}) 144 | endif(USE_YAML) 145 | 146 | if(${CMAKE_SYSTEM_PROCESSOR} STREQUAL "riscv64") 147 | target_link_libraries(thinkfan PRIVATE -latomic) 148 | endif() 149 | 150 | if(SYSTEMD_FOUND) 151 | target_compile_definitions(thinkfan PRIVATE -DHAVE_SYSTEMD) 152 | endif() 153 | 154 | if(DISABLE_BUGGER) 155 | target_compile_definitions(thinkfan PRIVATE -DDISABLE_BUGGER) 156 | endif(DISABLE_BUGGER) 157 | if(DISABLE_SYSLOG) 158 | target_compile_definitions(thinkfan PRIVATE -DDISABLE_SYSLOG) 159 | endif(DISABLE_SYSLOG) 160 | if(DISABLE_EXCEPTION_CATCHING) 161 | target_compile_definitions(thinkfan PRIVATE -DDISABLE_EXCEPTION_CATCHING) 162 | endif(DISABLE_EXCEPTION_CATCHING) 163 | 164 | configure_file(src/thinkfan.1.cmake thinkfan.1) 165 | configure_file(src/thinkfan.conf.5.cmake thinkfan.conf.5) 166 | configure_file(src/thinkfan.conf.legacy.5.cmake thinkfan.conf.legacy.5) 167 | 168 | install(TARGETS thinkfan DESTINATION "${CMAKE_INSTALL_SBINDIR}") 169 | install(FILES COPYING README.md examples/thinkfan.yaml DESTINATION "${CMAKE_INSTALL_DOCDIR}") 170 | install(FILES ${CMAKE_BINARY_DIR}/thinkfan.1 DESTINATION "${CMAKE_INSTALL_MANDIR}/man1") 171 | install(FILES ${CMAKE_BINARY_DIR}/thinkfan.conf.5 DESTINATION "${CMAKE_INSTALL_MANDIR}/man5") 172 | install(FILES ${CMAKE_BINARY_DIR}/thinkfan.conf.legacy.5 DESTINATION "${CMAKE_INSTALL_MANDIR}/man5") 173 | 174 | if(SYSTEMD_FOUND) 175 | configure_file(rcscripts/systemd/thinkfan.service.cmake 176 | rcscripts/systemd/thinkfan.service) 177 | install(FILES 178 | rcscripts/systemd/thinkfan-sleep.service 179 | rcscripts/systemd/thinkfan-wakeup.service 180 | "${CMAKE_BINARY_DIR}/rcscripts/systemd/thinkfan.service" 181 | DESTINATION "${CMAKE_INSTALL_PREFIX}/lib/systemd/system") 182 | if(NOT EXISTS "/etc/systemd/system/thinkfan.service.d/override.conf") 183 | install(FILES 184 | rcscripts/systemd/override.conf 185 | DESTINATION "/etc/systemd/system/thinkfan.service.d") 186 | else() 187 | install(FILES 188 | rcscripts/systemd/override.conf 189 | DESTINATION "/etc/systemd/system/thinkfan.service.d" 190 | RENAME "default.conf") 191 | endif() 192 | endif(SYSTEMD_FOUND) 193 | 194 | if(OPENRC_FOUND) 195 | configure_file(rcscripts/openrc/thinkfan.cmake 196 | rcscripts/openrc/thinkfan) 197 | install(FILES 198 | "${CMAKE_BINARY_DIR}/rcscripts/openrc/thinkfan" 199 | PERMISSIONS OWNER_READ OWNER_WRITE OWNER_EXECUTE GROUP_READ GROUP_EXECUTE WORLD_READ WORLD_EXECUTE 200 | DESTINATION "/etc/init.d") 201 | endif(OPENRC_FOUND) 202 | -------------------------------------------------------------------------------- /src/hwmon.cpp: -------------------------------------------------------------------------------- 1 | /******************************************************************** 2 | * hwmon.cpp: Helper functionality for sysfs hwmon interface 3 | * (C) 2022, Victor Mataré 4 | * 5 | * this file is part of thinkfan. See thinkfan.c for further information. 6 | * 7 | * thinkfan is free software: you can redistribute it and/or modify 8 | * it under the terms of the GNU General Public License as published by 9 | * the Free Software Foundation, either version 3 of the License, or 10 | * (at your option) any later version. 11 | * 12 | * thinkfan is distributed in the hope that it will be useful, 13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | * GNU General Public License for more details. 16 | * 17 | * You should have received a copy of the GNU General Public License 18 | * along with thinkfan. If not, see . 19 | * 20 | * ******************************************************************/ 21 | 22 | #include "hwmon.h" 23 | #include "message.h" 24 | #include "error.h" 25 | 26 | #include 27 | #include 28 | #include 29 | #include 30 | #include 31 | #include 32 | 33 | namespace thinkfan { 34 | 35 | 36 | static int filter_hwmon_dirs(const struct dirent *entry) 37 | { 38 | return (entry->d_type == DT_DIR || entry->d_type == DT_LNK) 39 | && (!strncmp("hwmon", entry->d_name, 5) || !strcmp("device", entry->d_name)); 40 | } 41 | 42 | 43 | static int filter_subdirs(const struct dirent *entry) 44 | { 45 | return (entry->d_type & DT_DIR || entry->d_type == DT_LNK) 46 | && string(entry->d_name) != "." && string(entry->d_name) != ".." 47 | && string(entry->d_name) != "subsystem"; 48 | } 49 | 50 | 51 | template 52 | vector HwmonInterface::find_files(const string &path, const vector &indices) 53 | { 54 | vector rv; 55 | for (unsigned int idx : indices) { 56 | const string fpath(path + "/" + filename(idx)); 57 | std::ifstream f(fpath); 58 | if (f.is_open() && f.good()) 59 | rv.push_back(fpath); 60 | else 61 | throw IOerror("Can't find hwmon file: " + fpath, errno); 62 | } 63 | return rv; 64 | } 65 | 66 | template<> 67 | string HwmonInterface::filename(unsigned int index) 68 | { return "temp" + std::to_string(index) + "_input"; } 69 | 70 | template<> 71 | string HwmonInterface::filename(unsigned int index) 72 | { return "pwm" + std::to_string(index); } 73 | 74 | 75 | 76 | template 77 | HwmonInterface::HwmonInterface() 78 | {} 79 | 80 | template 81 | HwmonInterface::HwmonInterface(const string &base_path, opt name, opt model, opt> indices) 82 | : base_path_(base_path) 83 | , name_(name) 84 | , model_(model) 85 | , indices_(indices) 86 | {} 87 | 88 | template 89 | vector HwmonInterface::find_hwmons_by_name( 90 | const string &path, 91 | const string &name, 92 | unsigned char depth 93 | ) { 94 | const unsigned char max_depth = 5; 95 | vector result; 96 | 97 | ifstream f(path + "/name"); 98 | if (f.is_open() && f.good()) { 99 | string tmp; 100 | if ((f >> tmp) && tmp == name) { 101 | result.push_back(path); 102 | return result; 103 | } 104 | } 105 | if (depth >= max_depth) { 106 | return result; // don't recurse to subdirs 107 | } 108 | 109 | struct dirent **entries; 110 | int nentries = ::scandir(path.c_str(), &entries, filter_subdirs, nullptr); 111 | if (nentries == -1) { 112 | return result; 113 | } 114 | for (int i = 0; i < nentries; i++) { 115 | auto subdir = path + "/" + entries[i]->d_name; 116 | free(entries[i]); 117 | 118 | struct stat statbuf; 119 | int err = stat(path.c_str(), &statbuf); 120 | if (err || (statbuf.st_mode & S_IFMT) != S_IFDIR) 121 | continue; 122 | 123 | auto found = find_hwmons_by_name(subdir, name, depth + 1); 124 | result.insert(result.end(), found.begin(), found.end()); 125 | } 126 | free(entries); 127 | 128 | return result; 129 | } 130 | 131 | template 132 | vector HwmonInterface::find_hwmons_by_model( 133 | const string &path, 134 | const string &model, 135 | unsigned char depth 136 | ) { 137 | const unsigned char max_depth = 5; 138 | vector result; 139 | 140 | ifstream f(path + "/model"); 141 | if (f.is_open() && f.good()) { 142 | string tmp; 143 | if (getline(f, tmp)) { 144 | tmp = tmp.erase(tmp.find_last_not_of(" \t\n\r\f\v") + 1); 145 | if (tmp == model) { 146 | result.push_back(path); 147 | return result; 148 | } 149 | } 150 | } 151 | if (depth >= max_depth) { 152 | return result; // don't recurse to subdirs 153 | } 154 | 155 | struct dirent **entries; 156 | int nentries = ::scandir(path.c_str(), &entries, filter_subdirs, nullptr); 157 | if (nentries == -1) { 158 | return result; 159 | } 160 | for (int i = 0; i < nentries; i++) { 161 | auto subdir = path + "/" + entries[i]->d_name; 162 | free(entries[i]); 163 | 164 | struct stat statbuf; 165 | int err = stat(path.c_str(), &statbuf); 166 | if (err || (statbuf.st_mode & S_IFMT) != S_IFDIR) 167 | continue; 168 | 169 | auto found = find_hwmons_by_model(subdir, model, depth + 1); 170 | result.insert(result.end(), found.begin(), found.end()); 171 | } 172 | free(entries); 173 | 174 | return result; 175 | } 176 | 177 | template 178 | vector HwmonInterface::find_hwmons_by_indices( 179 | const string &path, 180 | const vector &indices, 181 | unsigned char depth 182 | ) { 183 | constexpr unsigned char max_depth = 3; 184 | 185 | try { 186 | return find_files(path, indices); 187 | } 188 | catch (IOerror &) { 189 | if (depth <= max_depth) { 190 | struct dirent **entries; 191 | int nentries = ::scandir(path.c_str(), &entries, filter_hwmon_dirs, alphasort); 192 | if (nentries < 0) 193 | throw IOerror("Error scanning " + path + ": ", errno); 194 | 195 | vector rv; 196 | for (int i = 0; i < nentries; i++) { 197 | rv = HwmonInterface::find_hwmons_by_indices( 198 | path + "/" + entries[i]->d_name, 199 | indices, 200 | depth + 1 201 | ); 202 | if (rv.size()) 203 | break; 204 | } 205 | for (int i = 0; i < nentries; i++) 206 | free(entries[i]); 207 | free(entries); 208 | 209 | return rv; 210 | } 211 | else 212 | throw DriverInitError("Could not find an `hwmon*' directory or `temp*_input' file in " + path + "."); 213 | } 214 | } 215 | 216 | 217 | 218 | template 219 | string HwmonInterface::lookup() 220 | { 221 | if (!paths_it_) { 222 | if (!base_path_) 223 | throw Bug("Can't lookup sensor because it has no base path"); 224 | 225 | string path = *base_path_; 226 | 227 | if (name_) { 228 | vector paths = find_hwmons_by_name(path, name_.value(), 1); 229 | if (paths.size() != 1) { 230 | string msg(path + ": "); 231 | if (paths.size() == 0) { 232 | msg += "Could not find a hwmon with this name: " + name_.value(); 233 | } else { 234 | msg += MSG_MULTIPLE_HWMONS_FOUND; 235 | for (string hwmon_path : paths) 236 | msg += " " + hwmon_path; 237 | } 238 | throw DriverInitError(msg); 239 | } 240 | path = paths[0]; 241 | } 242 | if (model_) { 243 | vector paths = find_hwmons_by_model(path, model_.value(), 1); 244 | if (paths.size() != 1) { 245 | string msg(path + ": "); 246 | if (paths.size() == 0) { 247 | msg += "Could not find a hwmon with this model: " + model_.value(); 248 | } else { 249 | msg += MSG_MULTIPLE_HWMONS_FOUND; 250 | for (string hwmon_path : paths) 251 | msg += " " + hwmon_path; 252 | } 253 | throw DriverInitError(msg); 254 | } 255 | path = paths[0]; 256 | } 257 | if (indices_) { 258 | found_paths_ = find_hwmons_by_indices(path, indices_.value(), 0); 259 | if (found_paths_.size() == 0) 260 | throw DriverInitError(path + ": " + "Could not find any hwmons in " + path); 261 | } 262 | else 263 | found_paths_.push_back(path); 264 | 265 | paths_it_.emplace(found_paths_.begin()); 266 | } 267 | 268 | if (*paths_it_ >= found_paths_.end()) 269 | throw Bug(string(__func__) + ": found_paths_ iterator out of bounds"); 270 | 271 | return *paths_it_.value()++; 272 | } 273 | 274 | 275 | 276 | template class HwmonInterface; 277 | template class HwmonInterface; 278 | 279 | 280 | 281 | 282 | 283 | } // namespace thinkfan 284 | -------------------------------------------------------------------------------- /examples/thinkfan.yaml: -------------------------------------------------------------------------------- 1 | ############################################################################## 2 | # thinkfan Example Config File 3 | # ============================ 4 | # 5 | # Please read the config manpage thinkfan.conf(5) before playing around with 6 | # this. 7 | # 8 | # This is NOT a working config file that can just be copied. It is only meant 9 | # to give a rough idea what can be done. In particular, don't copy & paste the 10 | # fan speed config! Think about what you're doing. 11 | # 12 | # If you don't know what temperatures are right for your system, you should 13 | # not be using thinkfan! 14 | ############################################################################## 15 | 16 | 17 | ############################################################################## 18 | # Sensor Drivers and Temperature Inputs 19 | # ===================================== 20 | # 21 | # ATTENTION: The order in which sensors are specified here is significant when 22 | # specifying the fan speeds further below! 23 | # 24 | # There are multiple ways in which a temperature input can be specified. An 25 | # example for each is given below. 26 | # 27 | # The "correction:" and "optional:" keywords may be specified on any type of 28 | # sensor. 29 | 30 | sensors: 31 | # LM Sensors 32 | # ========== 33 | # Temperatures can be read directly from Linux drivers through the LM sensors. 34 | # 35 | # To configure this, install "lm-sensors" and "libsensors", then 36 | # run "sensors-detect", then run "sensors". 37 | # To build thinkfan from sources, you'll also need to install "libsensors-dev" 38 | # or equivalent package for your distribution. 39 | # 40 | # For example, the following output of "sensors": 41 | # ... 42 | # thinkpad-isa-0000 43 | # Adapter: ISA adapter 44 | # fan1: 2618 RPM 45 | # fan2: 2553 RPM 46 | # CPU: +63.0 C 47 | # GPU 1: +55.0 C 48 | # temp3: +68.0 C 49 | # temp4: +0.0 C 50 | # temp5: +60.0 C 51 | # temp6: +64.0 C 52 | # temp7: +67.0 C 53 | # temp8: +0.0 C 54 | # ... 55 | # would result in the following configuration: 56 | - chip: thinkpad-isa-0000 57 | ids: [ CPU, "GPU 1", temp3, temp4, temp5, temp6, temp7, temp8 ] 58 | 59 | # hwmon: Full path to a temperature file (single sensor). 60 | # ======================================================= 61 | # Disadvantage is that the index in "hwmon0" depends on the load order of 62 | # the driver modules, which may change across bootups on some systems. 63 | - hwmon: /sys/class/hwmon/hwmon0/temp1_input 64 | 65 | # hwmon: Path to a complete driver folder 66 | # ======================================= 67 | # Individual sensors need to be picked out with the "indices:" keyword. 68 | # This can be used with a stable path that does not depend on driver load 69 | # order. However certain drivers may not appear under such a stable path. 70 | - hwmon: /sys/devices/pci0000:00/0000:00:03.1/0000:27:00.0/hwmon 71 | indices: [1, 2, 5, 6] # adds 4 temperature sensors 72 | correction: [0, 0, 0, -5] # add -5 °C to temp6_input 73 | 74 | # hwmon: Base path with name-based search 75 | # ======================================= 76 | # Thinkfan will search under the given path for a hwmon driver that has a 77 | # file called "name" which contains the given name. This method should work 78 | # with all hwmon drivers and is robust against driver load order. 79 | - hwmon: /sys/class/hwmon 80 | name: k10temp 81 | indices: [1] 82 | 83 | # Sensors can also be optional, e.g. in case of removable hardware 84 | - hwmon: /sys/class/block/sdc/device/hwmon 85 | indices: [1] 86 | optional: true # don't exit if the sensor can't be read 87 | 88 | # atasmart: Read the temperature from a hard disk via S.M.A.R.T 89 | # ============================================================= 90 | # Note that this is unreasonably CPU-intensive. Since Linux 5.6, the kernel 91 | # can report the temperatures of hard disks via the hwmon interface (see the 92 | # example above), which should be preferred if available. 93 | # 94 | # This is only available if thinkfan was compiled with USE_ATASMART enabled. 95 | - atasmart: /dev/sda 96 | 97 | # tpacpi: Legacy interface to the thinkpad_acpi driver 98 | # ==================================================== 99 | # Particularly on older Thinkpad laptops, this interface may give access to 100 | # 8-16 temperature sensors, but it may be hard to tell where/what exactly 101 | # they measure. 102 | # Some documentation for older models may be found at the thinkpad wiki: 103 | # https://www.thinkwiki.org/wiki/Thermal_Sensors 104 | # 105 | # Note that the hwmon interface is to be preferred nowadays. 106 | - tpacpi: /proc/acpi/ibm/thermal 107 | # Some of the temperature entries in /proc/acpi/ibm/thermal may be 108 | # irrelevant or unused, so individual ones can be selected: 109 | indices: [1, 2, 3, 4] 110 | 111 | # nvml: The proprietary nVidia driver 112 | # =================================== 113 | # Temperatures can be read directly from nVidia GPUs that run with the 114 | # proprietary driver. The "nvml:" entry must specify the PCI bus ID of the 115 | # GPU (can be found with lspci) 116 | # 117 | # Note that this does not work with the open-source "nouveau" driver. Open 118 | # source drivers should support the hwmon interface instead (see above). 119 | - nvml: 27:00.0 120 | 121 | ############################################################################## 122 | 123 | 124 | ############################################################################## 125 | # Fan Drivers 126 | # =========== 127 | # 128 | # Currently, thinkfan supports only one fan, but support for multiple fans is 129 | # in development and will be released soon. For the time being, the examples 130 | # given below are mutually exclusive. 131 | # 132 | fans: 133 | # hwmon: Full path to a PWM file 134 | # ============================== 135 | # Also subject to the potential problem with driver load order (see above) 136 | - hwmon: /sys/class/hwmon/hwmon0/pwm1 137 | 138 | # hwmon: Path to a complete driver folder 139 | # ======================================= 140 | - hwmon: /sys/class/graphics/fb0/device/hwmon 141 | indices: [1] # Use pwm1 142 | 143 | # hwmon: Base path with name-based search 144 | # ======================================= 145 | - hwmon: /sys/class/hwmon 146 | name: amdgpu 147 | indices: [1] 148 | 149 | # tpacpi: Thinkpad-specific fan interface 150 | # ======================================= 151 | # Currently, this is the only way to use disengaged and automatic mode on 152 | # thinkpads. 153 | - tpacpi: /proc/acpi/ibm/fan 154 | 155 | ############################################################################## 156 | 157 | 158 | ############################################################################## 159 | # Fan Speeds (simple mode) 160 | # ======================== 161 | # 162 | # In simple mode, each entry is a [FANSPEED, LOWER_LIMIT, UPPER_LIMIT] tuple. 163 | # This is a quick way to configure a small system like a laptop, where the 164 | # temperature ratings for all monitored devices are similar. Only the highest 165 | # temperature found across all sensors will be compared against these limits. 166 | # All other temperatures are ignored. 167 | # 168 | # Correction values on individual sensors (see above) may be used to equalize 169 | # small discrepancies in temperature ratings. 170 | # 171 | # The FANSPEED values in this example are valid for the thinkpad_acpi fan 172 | # driver only (see above) 173 | # 174 | levels: 175 | - [0, 0, 50] 176 | - ["level auto", 45, 75] 177 | - ["level disengaged", 70, 255] 178 | 179 | ############################################################################## 180 | 181 | 182 | ############################################################################## 183 | # Fan Speeds (detailed mode) 184 | # ========================== 185 | # 186 | # It is generally advisable to configure the temperature limits for each 187 | # sensor individually. 188 | # 189 | # The speed values used here range from 0 to 255, which is valid for the PWM 190 | # control files used by hwmon-based drivers. 191 | # 192 | # The temperatures specified in upper_limit and lower_limit apply to the 193 | # sensors in the same order in which they were specified in the "sensors:" 194 | # section above, and their length must match the total number of sensors that 195 | # have been configured. 196 | # 197 | levels: 198 | - speed: 0 199 | upper_limit: [50, 50, 50] 200 | 201 | - speed: 100 202 | lower_limit: [45, 45, 45] 203 | upper_limit: [65, 65, 65] 204 | 205 | - speed: 255 206 | lower_limit: [60, 60, 60] 207 | 208 | ############################################################################## 209 | 210 | 211 | -------------------------------------------------------------------------------- /src/thinkfan.1.cmake: -------------------------------------------------------------------------------- 1 | .TH THINKFAN "1" "April 2022" "thinkfan @THINKFAN_VERSION@" "thinkfan" 2 | .SH NAME 3 | thinkfan \- A simple fan control program 4 | .SH SYNOPSIS 5 | .SY thinkfan 6 | .OP \-hnqDd 7 | .OP \-b BIAS 8 | .OP \-c CONFIG 9 | .OP \-s SECONDS 10 | .OP \-p \fR[\fIDELAY\fR]\fI 11 | .YS 12 | 13 | 14 | 15 | .SH DESCRIPTION 16 | 17 | Thinkfan reads temperatures from a configured set of sensors and then sets fan 18 | speeds according to temperature limits set in the config file. 19 | 20 | .HP 21 | \fBWARNING\fR: Thinkfan does only very basic sanity checking on the 22 | configuration. This means that a bad configuration can increase thermal wear 23 | on the hardware or even cause destruction from overheating! 24 | 25 | 26 | .SS Supported sensors 27 | 28 | .TP 29 | \(bu /proc/acpi/ibm/thermal 30 | This is provided by the thinkpad_acpi kernel module on older Thinkpads. 31 | 32 | .TP 33 | \(bu temp*_input files in sysfs (hwmon interface) 34 | May be provided by any hwmon drivers, including thinkpad_acpi on modern 35 | Thinkpads. 36 | 37 | .TP 38 | \(bu Hard disks with S.M.A.R.T. support (libatasmart) 39 | Available if thinkfan was compiled with 40 | .B \-DUSE_ATASMART=ON. 41 | Note that modern Linux kernels can also expose S.M.A.R.T. hard disk 42 | temperatures via the hwmon interface in sysfs (and therefore also via 43 | lm_sensors), which is generally preferrable because it is more efficient. 44 | 45 | .TP 46 | \(bu From the proprietary nVidia driver 47 | When the proprietary nVidia driver is used, no hwmon for the card will be 48 | available. In this situation, thinkfan can use the proprietary NVML API to get 49 | temperatures. 50 | 51 | .TP 52 | \(bu Via lm_sensors (libsensors interface) 53 | This is a modern and more reliable alternative to the sysfs hwmon interface 54 | mentioned above. It's basically a standardized abstraction for sysfs hwmon 55 | where sensors can always be identified uniquely, even when the load order of 56 | kernel modules changes. 57 | 58 | .SS Supported fans 59 | Thinkfan can control any number of fans, which can be specified in two ways: 60 | 61 | .TP 62 | \(bu /proc/acpi/ibm/fan 63 | Provided by the thinkpad_acpi kernel module. Note that the kernel module needs 64 | to be loaded with the option "fan_control=1" to enable userspace fan control. 65 | On some models, "experimental=1" may also be required. See the 66 | .B SEE ALSO 67 | section at the bottom of this manpage for a link to the official thinkpad_acpi 68 | documentation. 69 | 70 | .TP 71 | \(bu pwm*_enable and pwm? files in sysfs 72 | Provided by all modern hardware monitoring drivers, including thinkpad_acpi. 73 | 74 | 75 | .SS Mapping temperatures to fan speeds 76 | 77 | There are two general modes of operation: 78 | 79 | .TP 80 | \(bu Detailed mode 81 | In detailed mode, temperature limits are defined for each sensor thinkfan knows 82 | about. Setting suitable limits for each sensor in your system will probably 83 | require a bit of experimentation and good knowledge about your hardware, but 84 | it's the safest way of keeping each component within its specified temperature 85 | range. See the example configs to learn about the syntax. 86 | 87 | .TP 88 | \(bu Simple mode 89 | In simple mode, thinkfan uses only the highest temperature found in the 90 | system. That may be dangerous, e.g. for hard disks. 91 | That's why you should provide a correction value (i.e. add 10\-15 \[char176]C) 92 | for the sensor that has the temperature of your hard disk (or battery...). 93 | See the example config files for details about that. 94 | 95 | 96 | 97 | 98 | .SH CONFIGURATION 99 | All of the features described above are configured in the thinkfan config 100 | file. Its default location is 101 | .B /etc/thinkfan.conf 102 | or 103 | .BR /etc/thinkfan.yaml 104 | (see also the 105 | .B -c 106 | option below). 107 | An example configuration is provided with the source package. It is intended 108 | purely for illustration of various scenarios and is not suitable as a basis 109 | for actually functional config. For a complete reference see the config man 110 | page 111 | .BR thinkfan.conf (5). 112 | 113 | 114 | 115 | .SH OPTIONS 116 | 117 | .TP 118 | .B \-h 119 | Show a short help message 120 | 121 | .TP 122 | .BI \-s " SECONDS" 123 | Maximum seconds between temperature updates (default: 5) 124 | 125 | .TP 126 | .BI \-b " BIAS" 127 | Floating point number (\-10 to 30) to smooth out or amplify quick temperature 128 | changes. 129 | If a sensor's temperature increases by more than 2 \[char176]C during one 130 | cycle, we calculate an offset value as follows: 131 | 132 | \fBoffset\fR = \fBdelta_t\fR * \fIBIAS\fR / 10 133 | 134 | This offset is then added to the actual temperature: 135 | 136 | \fBbiased_t\fR = \fBcurrent_t\fR + \fBoffset\fR 137 | 138 | If \fBdelta_t\fR stays below 2 \[char176]C in subsequent loops, \fBoffset\fR 139 | will be reduced back to 0 in increments of sgn(\fIBIAS\fR) * (1 + 140 | abs(\fIBIAS\fR/5)). 141 | 142 | This means that a negative \fIBIAS\fR will even out short and sudden 143 | temperature spikes like those seen on some on\-DIE sensors, while positive 144 | values will exaggerate increasing temperatures to compensate e.g. for sensors 145 | that respond slowly because they are attached to heavy heatsinks. 146 | 147 | Use DANGEROUS mode 148 | to remove the \-10 to +30 limit. Note that you can't have a space between \-b 149 | and a negative argument, because otherwise getopt will interpret things like 150 | \-10 as an option and fail (i.e. write 151 | .B \-b\-10 152 | instead of 153 | .BR "\-b \-10" ). 154 | 155 | The default is 0. 156 | 157 | .TP 158 | .BI \-c " FILE" 159 | Load a different configuration file. 160 | By default, thinkfan first tries to load /etc/thinkfan.yaml, and 161 | /etc/thinkfan.conf after that. 162 | The former must be in YAML format, while the latter can be either YAML or the 163 | old legacy syntax. 164 | 165 | If this option is specified, thinkfan attempts to load the config only from 166 | .IR FILE . 167 | If its name ends in \*(lq.yaml\*(rq, it must be in YAML format. 168 | Otherwise, it can be either YAML or legacy syntax. 169 | See 170 | .BR thinkfan.conf (5) 171 | and 172 | .BR thinkfan.conf.legacy (5) 173 | for details. 174 | 175 | .TP 176 | .B \-n 177 | Do not become a daemon and log to terminal instead of syslog 178 | 179 | .TP 180 | .B \-q 181 | Be quiet, i.e. reduce logging level from the default. Can be specified 182 | multiple times until only errors are displayed/logged. 183 | 184 | .TP 185 | .B \-v 186 | Be more verbose. Can be specified multiple times until every message is 187 | displayed/logged. 188 | 189 | .TP 190 | .BR "\-p " [\fISECONDS\fR] 191 | Use the pulsing\-fan workaround (for older Thinkpads). Takes an optional 192 | floating\-point argument (0\-10s) as depulsing duration. Default 0.5s. 193 | 194 | .TP 195 | .B \-d 196 | Do not read temperature from sleeping disks. Instead, 0 \[char176]C is used as that 197 | disk's temperature. This is needed if reading the temperature causes your 198 | disk to wake up unnecessarily. 199 | NOTE: This option is only available if thinkfan was built with \-D USE_ATASMART. 200 | 201 | .TP 202 | .B \-D 203 | DANGEROUS mode: Disable all sanity checks. May damage your hardware!! 204 | 205 | 206 | 207 | .SH SIGNALS 208 | SIGINT and SIGTERM simply interrupt operation and should cause thinkfan to 209 | terminate cleanly. 210 | .P 211 | SIGHUP makes thinkfan reload its config. If there's any problem with the new 212 | config, we keep the old one. 213 | .P 214 | SIGUSR1 causes thinkfan to dump all currently known temperatures either to 215 | syslog, or to the console (if running with the \-n option). 216 | .P 217 | SIGPWR tells thinkfan that the system is about to go to sleep. Thinkfan will 218 | then allow sensor read errors for the next 4 loops because many sensors will 219 | take a few seconds before they are available again after waking up from a 220 | sleep state (suspend or hibernate). If the shipped systemd service file 221 | .B thinkfan-sleep.service 222 | is installed, it should take care of sending this singal when going to sleep. 223 | On non-systemd distributions, other mechanisms may have to be used. 224 | .P 225 | SIGUSR2 tells thinkfan to re-initialize fan control. This is required by most 226 | fan drivers after waking up from suspend because they tend to reset fan 227 | control to automatic mode on wakeup. Similar to SIGPWR, the systemd service 228 | file 229 | .B thinkfan-wakeup.service 230 | should take care of sending this signal on wakeup on systemd systems. On 231 | non-systemd distributions, other mechanisms may have to be used. 232 | 233 | 234 | .SH RETURN VALUE 235 | 236 | .TP 237 | .B 0 238 | Normal exit 239 | 240 | .TP 241 | .B 1 242 | Runtime error 243 | 244 | .TP 245 | .B 2 246 | Unexpected runtime error 247 | 248 | .TP 249 | .B 3 250 | Invalid commandline option 251 | 252 | 253 | 254 | .SH SEE ALSO 255 | .nf 256 | The thinkfan config manpage: 257 | .BR thinkfan.conf (5) 258 | 259 | Example configs shipped with the source distribution, also available at: 260 | .hy 0 261 | https://github.com/vmatare/thinkfan/tree/master/examples 262 | 263 | The Linux hwmon user interface documentation: 264 | https://www.kernel.org/doc/html/latest/hwmon/sysfs\-interface.html 265 | 266 | The thinkpad_acpi interface documentation: 267 | https://www.kernel.org/doc/html/latest/admin\-guide/laptops/thinkpad\-acpi.html 268 | 269 | 270 | 271 | .SH BUGS 272 | If thinkfan tells you to, or if you feel like it, report issues at the Github 273 | issue tracker: 274 | 275 | .hy 0 276 | https://github.com/vmatare/thinkfan/issues 277 | 278 | -------------------------------------------------------------------------------- /src/fans.cpp: -------------------------------------------------------------------------------- 1 | /******************************************************************** 2 | * drivers.cpp: Interface to the kernel drivers. 3 | * (C) 2015, Victor Mataré 4 | * 5 | * this file is part of thinkfan. See thinkfan.c for further information. 6 | * 7 | * thinkfan is free software: you can redistribute it and/or modify 8 | * it under the terms of the GNU General Public License as published by 9 | * the Free Software Foundation, either version 3 of the License, or 10 | * (at your option) any later version. 11 | * 12 | * thinkfan is distributed in the hope that it will be useful, 13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | * GNU General Public License for more details. 16 | * 17 | * You should have received a copy of the GNU General Public License 18 | * along with thinkfan. If not, see . 19 | * 20 | * ******************************************************************/ 21 | 22 | #include "fans.h" 23 | #include "error.h" 24 | #include "message.h" 25 | #include "config.h" 26 | 27 | #include 28 | #include 29 | #include 30 | #include 31 | 32 | #ifdef USE_NVML 33 | #include 34 | #endif 35 | 36 | namespace thinkfan { 37 | 38 | /*---------------------------------------------------------------------------- 39 | | FanDriver: Superclass of TpFanDriver and HwmonFanDriver. Can set the speed | 40 | | on its own since an implementation-specific string representation is | 41 | | provided by its subclasses. | 42 | ----------------------------------------------------------------------------*/ 43 | 44 | FanDriver::FanDriver(bool optional, unsigned int watchdog_timeout, opt max_errors) 45 | : Driver(optional, max_errors.value_or(0)), 46 | current_speed_("_"), 47 | watchdog_(watchdog_timeout), 48 | depulse_(0) 49 | {} 50 | 51 | FanDriver::~FanDriver() noexcept(false) 52 | {} 53 | 54 | void FanDriver::set_speed(const string &level) 55 | { robust_io(&FanDriver::set_speed_, level); } 56 | 57 | void FanDriver::skip_io_error(const ExpectedError &) 58 | {} 59 | 60 | 61 | void FanDriver::set_speed_(const string &level) 62 | { 63 | std::ofstream f_out(path()); 64 | if(!(f_out << level << std::flush)) { 65 | int err = errno; 66 | if (err == EPERM) 67 | throw SystemError(MSG_FAN_EPERM(path())); 68 | else 69 | throw IOerror(MSG_FAN_CTRL(level, path()), err); 70 | } 71 | current_speed_ = level; 72 | } 73 | 74 | 75 | bool FanDriver::operator == (const FanDriver &other) const 76 | { 77 | return typeid(*this) == typeid(other) 78 | && this->path() == other.path() 79 | && this->depulse_ == other.depulse_ 80 | && this->watchdog_ == other.watchdog_; 81 | } 82 | 83 | const string &FanDriver::current_speed() const 84 | { return current_speed_; } 85 | 86 | 87 | /*---------------------------------------------------------------------------- 88 | | TpFanDriver: Driver for fan control via thinkpad_acpi, typically in | 89 | | /proc/acpi/ibm/fan. Supports fan watchdog and depulsing (an alleged remedy | 90 | | for noise oscillation with old & worn-out fans). | 91 | ----------------------------------------------------------------------------*/ 92 | 93 | TpFanDriver::TpFanDriver(const std::string &path, bool optional, opt max_errors) 94 | : FanDriver(optional, 120, max_errors) 95 | , path_(path) 96 | {} 97 | 98 | 99 | TpFanDriver::~TpFanDriver() noexcept(false) 100 | { 101 | if (!initialized()) 102 | return; 103 | 104 | std::ofstream f(path()); 105 | if (!(f.is_open() && f.good())) { 106 | log(TF_ERR) << MSG_FAN_RESET(path()) << strerror(errno) << flush; 107 | return; 108 | } 109 | 110 | if (!initial_state_.empty()) { 111 | log(TF_DBG) << path() << ": Restoring initial state: " << initial_state_ << "." << flush; 112 | if (!(f << "level " << initial_state_ << std::flush)) 113 | log(TF_ERR) << MSG_FAN_RESET(path()) << strerror(errno) << flush; 114 | } 115 | } 116 | 117 | 118 | void TpFanDriver::set_watchdog(const unsigned int timeout) 119 | { watchdog_ = std::chrono::duration(timeout); } 120 | 121 | 122 | void TpFanDriver::set_depulse(float duration) 123 | { depulse_ = std::chrono::duration(duration); } 124 | 125 | 126 | void TpFanDriver::set_speed(const Level &level) 127 | { 128 | FanDriver::set_speed(level.str()); 129 | last_watchdog_ping_ = std::chrono::system_clock::now(); 130 | } 131 | 132 | 133 | void TpFanDriver::ping_watchdog_and_depulse(const Level &level) 134 | { 135 | if (depulse_ > std::chrono::milliseconds(0)) { 136 | FanDriver::set_speed("level disengaged"); 137 | std::this_thread::sleep_for(depulse_); 138 | set_speed(level); 139 | } 140 | else if (last_watchdog_ping_ + watchdog_ - sleeptime <= std::chrono::system_clock::now()) { 141 | log(TF_DBG) << "Watchdog ping" << flush; 142 | set_speed(level); 143 | } 144 | } 145 | 146 | 147 | void TpFanDriver::init() 148 | { 149 | bool ctrl_supported = false; 150 | std::fstream f(path()); 151 | if (!(f.is_open() && f.good())) 152 | throw IOerror(MSG_FAN_INIT(path()), errno); 153 | 154 | std::string line; 155 | line.resize(256); 156 | 157 | while (f.getline(&*line.begin(), 255)) { 158 | if (initial_state_.empty() && line.rfind("level:") != string::npos) { 159 | // remember initial level, restore it in d'tor 160 | string::size_type offs = line.find_last_of(" \t") + 1; 161 | if (offs != string::npos) { 162 | // Cut of at bogus \000 char that may occur before EOL 163 | initial_state_ = line.substr(offs, line.find_first_of('\000') - offs); 164 | } 165 | log(TF_DBG) << path() << ": Saved initial state: " << initial_state_ << "." << flush; 166 | } 167 | else if (line.rfind("commands:") != std::string::npos && line.rfind("level ") != std::string::npos) { 168 | ctrl_supported = true; 169 | } 170 | } 171 | 172 | if (!ctrl_supported) 173 | throw SystemError(MSG_FAN_MODOPTS); 174 | 175 | if (initial_state_.empty()) 176 | throw SystemError(MSG_FAN_INIT(path()) + "Failed to read initial state."); 177 | 178 | f.close(); 179 | f.open(path()); 180 | 181 | if (!(f << "watchdog " << watchdog_.count() << std::flush)) 182 | throw IOerror(MSG_FAN_INIT(path()), errno); 183 | } 184 | 185 | 186 | string TpFanDriver::lookup() 187 | { 188 | std::fstream f(path_); 189 | if (!(f.is_open() && f.good())) 190 | throw IOerror(MSG_FAN_INIT(path_), errno); 191 | return path_; 192 | } 193 | 194 | string TpFanDriver::type_name() const 195 | { return "tpacpi fan driver"; } 196 | 197 | 198 | /*---------------------------------------------------------------------------- 199 | | HwmonFanDriver: Driver for PWM fans, typically somewhere in sysfs. | 200 | ----------------------------------------------------------------------------*/ 201 | 202 | HwmonFanDriver::HwmonFanDriver(const string &path) 203 | : HwmonFanDriver( 204 | std::make_shared>(path, nullopt, nullopt, nullopt), 205 | false, 206 | 0 207 | ) 208 | {} 209 | 210 | HwmonFanDriver::HwmonFanDriver( 211 | shared_ptr> hwmon_interface, 212 | bool optional, 213 | opt max_errors 214 | ) 215 | : FanDriver(optional, 0, max_errors) 216 | , hwmon_interface_(hwmon_interface) 217 | {} 218 | 219 | 220 | HwmonFanDriver::~HwmonFanDriver() noexcept(false) 221 | { 222 | if (!initialized()) 223 | return; 224 | 225 | std::ofstream f(path() + "_enable"); 226 | if (!(f.is_open() && f.good())) { 227 | log(TF_ERR) << MSG_FAN_RESET(path()) << strerror(errno) << flush; 228 | return; 229 | } 230 | 231 | if (!initial_state_.empty()) { 232 | log(TF_DBG) << path() << ": Restoring initial state: " << initial_state_ << "." << flush; 233 | if (!(f << initial_state_ << std::flush)) 234 | log(TF_ERR) << MSG_FAN_RESET(path()) << strerror(errno) << flush; 235 | } 236 | } 237 | 238 | 239 | void HwmonFanDriver::init() 240 | { 241 | std::fstream f(path() + "_enable"); 242 | if (!(f.is_open() && f.good())) 243 | throw IOerror(MSG_FAN_INIT(path()), errno); 244 | 245 | if (initial_state_.empty()) { 246 | std::string line; 247 | line.resize(64); 248 | if (!f.getline(&*line.begin(), 63)) 249 | throw IOerror(MSG_FAN_INIT(path()), errno); 250 | initial_state_ = line; 251 | log(TF_DBG) << path() << ": Saved initial state: " << initial_state_ << "." << flush; 252 | } 253 | 254 | if (!(f << "1" << std::flush)) 255 | throw IOerror(MSG_FAN_INIT(path()), errno); 256 | } 257 | 258 | string HwmonFanDriver::lookup() 259 | { return hwmon_interface_->lookup(); } 260 | 261 | string HwmonFanDriver::type_name() const 262 | { return "hwmon fan driver"; } 263 | 264 | 265 | void HwmonFanDriver::set_speed(const Level &level) 266 | { 267 | try { 268 | FanDriver::set_speed(std::to_string(level.num())); 269 | } catch (IOerror &e) { 270 | if (e.code() == EINVAL) { 271 | // This happens when the hwmon kernel driver is reset to automatic control 272 | // e.g. after the system has woken up from suspend. 273 | // In that case, we need to re-initialize and try once more. 274 | init(); 275 | FanDriver::set_speed(std::to_string(level.num())); 276 | log(TF_WRN) << path() << ": WARNING: Userspace fan control had to be automatically re-initialized." << flush; 277 | #if defined(HAVE_SYSTEMD) 278 | log(TF_WRN) << "This should have been taken care of when enabling the thinkfan systemd service." << flush 279 | << "If thinkfan.service is enabled, the following services should also have be enabled as a dependency:" << flush 280 | << "thinkfan-hibernate.service, thinkfan-hybrid-suspend.service and thinkfan-suspend.service" << flush; 281 | #else 282 | log(TF_WRN) << "Please arrange for a SIGUSR2 to be sent to thinkfan after resuming from suspend." << flush; 283 | #endif 284 | } else { 285 | throw; 286 | } 287 | } 288 | } 289 | 290 | 291 | 292 | 293 | } // namespace thinkfan 294 | -------------------------------------------------------------------------------- /src/thinkfan.conf.legacy.5.cmake: -------------------------------------------------------------------------------- 1 | .TH THINKFAN.CONF.LEGACY 5 2020-04-09 "thinkfan @THINKFAN_VERSION@" 2 | .SH NAME 3 | thinkfan.conf.legacy \- the old, backwards-compatible config syntax for 4 | thinkfan 5 | .BR thinkfan (1) 6 | 7 | .SH DESCRIPTION 8 | The thinkfan config file specifies one or more temperature input(s), exactly 9 | one fan to control and the fan levels. 10 | A fan level associates a certain fan speed to a lower and an upper 11 | temperature bound. 12 | If the temperature reaches the upper bound, we switch to the next fan level, 13 | and if it drops below the lower bound, we switch to the previous fan level. 14 | Temperature bounds can either be a single temperature (\fIsimple mode\fR) or 15 | consist of multiple temperatures (\fIcomplex mode\fR). 16 | In simple mode, only the highest of all known temperature is compared to the 17 | upper & lower bound. 18 | If you have devices with very different temperature ratings (e.g. CPU vs. 19 | mechanical hard drives), you should specify correction values to equalize 20 | their temperature ranges, or better: use complex mode. 21 | In complex mode, the upper and lower bounds of each fan level are specified 22 | for each sensor individually. 23 | Thinkfan then switches to the next fan level if one of the upper bounds is 24 | reached, and to the previous fan level if all temperatures have dropped below 25 | their respective lower bounds. 26 | 27 | .SH THERMAL SENSORS 28 | Multiple sensor keywords can be combined in one config file, but note that the 29 | ordering is significant with respect to the upper and lower fan level bounds if 30 | you use \fIcomplex mode\fR. 31 | I.e. if 32 | .B /proc/acpi/ibm/thermal 33 | contains 16 temperatures and you specify an 34 | .B hwmon 35 | sensor after the 36 | .B tp_thermal 37 | statement, the 38 | .B hwmon 39 | sensor will be the 17th temperature. 40 | . 41 | After each sensor path, an optional 42 | .I correction-value 43 | can be specified. 44 | This value (can be negative) is always added to the temperature reading from 45 | that sensor. 46 | Correction values should be specified if you use 47 | .B Simple Mode 48 | with components that have a different temperature rating, like hard disks and 49 | CPUs. 50 | Note though that 51 | .B Complex Mode 52 | is generally the better solution since it gives you full control over fan 53 | levels and temperature ranges for each sensor, instead of just adding a fixed 54 | value to equalize temperature ranges. 55 | 56 | .TP 57 | .BI "tp_thermal /proc/acpi/ibm/thermal" " \fR[\fB (\fIcorrection-value \fR...\fB) \fR]\fP" 58 | Use the thermal sensors provided by the 59 | .B thinkpad_acpi 60 | kernel module on older thinkpads. These normally reside in 61 | .B /proc/acpi/ibm/thermal, 62 | so this keyword will hardly be used with other paths. 63 | This file usually contains 8-16 temperatures, some of which may be 64 | reserved for removable hardware or completely unused. Unused temperature slots 65 | always contain the value -128. Since this file contains all temperatures the 66 | .B thinkpad_acpi 67 | module knows about, there cannot be more than one 68 | .B tp_thermal 69 | statement in a config file. 70 | 71 | .TP 72 | .BI hwmon " sysfs-path \fR[ \fB(\fIcorrection-value\fB) \fR]\fP" 73 | Use a standard hwmon temperature input that may be provided by all kinds of 74 | kernel drivers. 75 | .I sysfs-path 76 | is usually a file named \[oq]temp*_input\[cq], somewhere under /sys, so you 77 | can search for them e.g. with \[oq]find /sys -type f -name "temp*_input"\[cq]. 78 | Each of these files contains one temperature, so you need to add a 79 | .B hwmon 80 | statement for each device whose temperature you wish to control. 81 | 82 | .TP 83 | .BI atasmart " device-path \fR[ \fB(\fIcorrection-value\fB) \fR]\fP" 84 | NOTE: only available if thinkfan was compiled with USE_ATASMART enabled. 85 | . 86 | .IP 87 | Read the temperature directly from a hard disk using S.M.A.R.T. See also the 88 | .B -d 89 | option in 90 | .BR thinkfan (1) 91 | that prevents thinkfan from waking up sleeping (mechanical) disks to read 92 | their temperature. 93 | 94 | .TP 95 | .BI nv_thermal " pci-bus-id \fR[ \fB(\fIcorrection-value\fB) \fR]\fP" 96 | NOTE: only available if thinkfan was compiled with USE_NVML enabled. 97 | . 98 | .IP 99 | Read the temperature of an nVidia graphics card from the proprietary nVidia 100 | driver. This does not work with the open-source Nouveau driver, it depends 101 | specifially on libnvidia-ml.so that is usually installed with the binary nVidia 102 | driver. 103 | The correct 104 | .I pci-bus-id 105 | can be retrieved using e.g. lspci with: \[oq]lspci | grep -i vga\[cq]. 106 | Most open-source graphics drivers (radeon, nouveau, possibly others too) can 107 | instead be used with the 108 | .B hwmon 109 | keyword described above. 110 | 111 | .SH FANS 112 | Currently, thinkfan can control only one fan at a time. 113 | In theory, you can run multiple instances of the program simultaneously (with 114 | multiple config files) to control multiple fans, but that requires enabling 115 | DANGEROUS mode and will likely break most init scripts. 116 | It is an error to have more than one fan statement per config file. 117 | 118 | .TP 119 | .B tp_fan /proc/acpi/ibm/fan 120 | Use the fan control provided by the 121 | .B thinkpad_acpi 122 | kernel module, which needs to be loaded with the option 123 | .BR fan_control=1 . 124 | The path is defined by the 125 | .B thinkpad_acpi 126 | kernel module and will hardly change. Besides the fan levels ranging from 0 to 127 | 7, it also supports the 128 | .B disengaged 129 | and 130 | .B auto 131 | modes. 132 | . 133 | .IP 134 | The 135 | .B auto 136 | mode should delegate fan control to the firmware, so it can be regarded as a 137 | \[oq]default\[cq] mode that does not change the fan behavior. 138 | This is useful for example if you only want to change the fan behavior at high 139 | and/or low temperatures. 140 | . 141 | .IP 142 | The 143 | .B disengaged 144 | or 145 | .B full-speed 146 | mode effectively disables the fan RPM limiter. 147 | Fan speed will slowly ramp up until the fan uses the maximum electrical power 148 | available from the embedded controller. Use this only to prevent potentially 149 | destructive overheating, since it runs the fan outside of specifications and 150 | wears its bearings down quickly. 151 | 152 | .TP 153 | .BI pwm_fan " sysfs-path" 154 | Control a sysfs PWM fan. 155 | Many hwmon drivers that provide a \[oq]temp*_input\[cq] file also allow fan control, 156 | although there may also be drivers that are specific to either temperature 157 | reading or fan control. 158 | You can search for a PWM control file e.g. with \[oq]find /sys -type f -name 159 | "pwm?"\[cq]. 160 | Note that with PWM, fan levels usually range from 0 to 255, although besides a 161 | file like \fBpwm1\fR there may also be \fBpwm1_min\fR and \fBpwm1_max\fR that specify 162 | different (soft or recommended?) limits for a particular fan. 163 | 164 | 165 | .SH FAN LEVELS 166 | Defining the fan levels is the meat of the config file. Here you make use of 167 | your previously defined temperature inputs to set the lower and upper bounds 168 | for the fan speeds. 169 | You cannot mix simple fan levels with complex fan levels. 170 | The general syntax of a simple fan level is: 171 | .RS 172 | .PP 173 | \fB( \fIfan-level \fR[\fB,\fR] \fIlower-bound \fR[\fB,\fR] \fIupper-bound\fB ) 174 | .RE 175 | .PP 176 | The 177 | .I fan-level 178 | is either a numeric value (0-7 or 0-255, depending on whether a 179 | .B tp_fan 180 | or a 181 | .B pwm_fan 182 | is used) or a string enclosed in double quotes. 183 | When a 184 | .B tp_fan 185 | is used, specifying 186 | .B 0 187 | has the same effect as specifying \fB"level 0\fR". 188 | In addition to the numeric fan levels, 189 | .B tp_fan 190 | also supports \fB"level auto"\fR and \fB"level disengaged"\fR or \fB"level 191 | full-speed"\fR. 192 | See above for an explanation of what these mean. 193 | The format of 194 | .I lower-bound 195 | and 196 | .I upper-bound 197 | depends on whether you want to use 198 | .B Simple Mode 199 | or 200 | .B Complex Mode. 201 | 202 | 203 | .SS Simple Mode 204 | In simple mode, the 205 | .I lower-bound 206 | and 207 | .I upper-bound 208 | of a fan level are each specified as a single temperature value. 209 | Both are compared only to the highest temperature found in all of the 210 | configured thermal sensors. 211 | Using this mode of operation makes sense e.g. if all temperature readings come 212 | from the on-DIE thermal sensors of a multicore processor. 213 | The fan speed will affect all of these temperatures in the same way because 214 | they share a single thermal connection to the heatsink, so it makes sense to 215 | ignore all but the highest of these temperatures. 216 | As a rule of thumb, if your thermal sensors cover multiple devices you should 217 | use Complex Mode, or at least specify correction values to account for 218 | different temperature ratings. 219 | 220 | .SS Complex Mode 221 | In complex mode, both the 222 | .I lower-bound 223 | and 224 | .I upper-bound 225 | are lists of temperatures, the length of which must match the number of 226 | temperature readings thinkfan knows about. 227 | Each bound must be enclosed in braces, with individual values separated by 228 | commas or spaces, so the specific syntax of a complex mode fan level is: 229 | .RS 230 | .PP 231 | .nf 232 | .B "{ \fIfan-level" 233 | .B " ( \fIlower-1 \fR[\fIlower-2\fR ...] \fB)" 234 | .B " ( \fIupper-1 \fR[\fIupper-2\fR ...] \fB)" 235 | .B "}" 236 | .fi 237 | .RE 238 | .PP 239 | The optional commas have been omitted here for readability, and the curly 240 | braces are interchangeable with round braces. 241 | Note that it is not possible to mix simple fan levels with complex fan levels. 242 | .P 243 | Complex mode is generally the preferred mode of operation since it allows you 244 | to specify precisely what the fan should to to keep each component within its 245 | specified temperature range. 246 | 247 | 248 | .SH SEE ALSO 249 | .BR thinkfan (1) 250 | .P 251 | Example configs shipped with the source distribution, also available at 252 | .IR https://github.com/vmatare/thinkfan/tree/master/examples . 253 | 254 | -------------------------------------------------------------------------------- /src/message.h: -------------------------------------------------------------------------------- 1 | /******************************************************************** 2 | * message.h: Logging and error management. 3 | * (C) 2015, Victor Mataré 4 | * 5 | * this file is part of thinkfan. See thinkfan.c for further information. 6 | * 7 | * thinkfan is free software: you can redistribute it and/or modify 8 | * it under the terms of the GNU General Public License as published by 9 | * the Free Software Foundation, either version 3 of the License, or 10 | * (at your option) any later version. 11 | * 12 | * thinkfan is distributed in the hope that it will be useful, 13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | * GNU General Public License for more details. 16 | * 17 | * You should have received a copy of the GNU General Public License 18 | * along with thinkfan. If not, see . 19 | * 20 | * ******************************************************************/ 21 | 22 | #ifndef MESSAGE_H 23 | #define MESSAGE_H 24 | 25 | #include 26 | #include 27 | #include 28 | 29 | #include "thinkfan.h" 30 | #include "temperature_state.h" 31 | 32 | namespace thinkfan { 33 | 34 | enum LogLevel { 35 | TF_ERR = LOG_ERR, 36 | TF_WRN = LOG_WARNING, 37 | TF_NFY = LOG_NOTICE, 38 | TF_INF = LOG_INFO, 39 | TF_DBG = LOG_DEBUG 40 | }; 41 | 42 | #ifdef DEBUG 43 | #define DEFAULT_LOG_LVL TF_DBG 44 | #else 45 | #define DEFAULT_LOG_LVL TF_NFY 46 | #endif 47 | 48 | LogLevel &operator--(LogLevel &l); 49 | LogLevel &operator++(LogLevel &l); 50 | 51 | class ExpectedError; 52 | class FanConfig; 53 | 54 | class Logger { 55 | private: 56 | Logger(); 57 | static unique_ptr instance_; 58 | public: 59 | ~Logger(); 60 | void enable_syslog(); 61 | Logger &level(const LogLevel &lvl); 62 | Logger &flush(); 63 | static Logger &instance(); 64 | LogLevel &log_lvl(); 65 | 66 | Logger &operator<< (const std::string &msg); 67 | Logger &operator<< (const unsigned int i); 68 | Logger &operator<< (const int i); 69 | Logger &operator<< (const float &d); 70 | Logger &operator<< (Logger & (*pf_flush)(Logger &)); 71 | Logger &operator<< (const char *msg); 72 | Logger &operator<< (char *msg); 73 | 74 | Logger &operator<< (const TemperatureState &); 75 | Logger &operator<< (const vector> &); 76 | 77 | template 78 | Logger &operator<< (const ListT &l) { 79 | msg_pfx_ += "("; 80 | for (auto elem : l) { 81 | msg_pfx_ += std::to_string(elem) + ", "; 82 | } 83 | msg_pfx_.pop_back(); msg_pfx_.pop_back(); 84 | msg_pfx_ += ")"; 85 | return *this; 86 | } 87 | 88 | private: 89 | bool syslog_; 90 | LogLevel log_lvl_; 91 | LogLevel msg_lvl_; 92 | std::string msg_pfx_; 93 | std::exception_ptr exception_; 94 | }; 95 | 96 | 97 | Logger &flush(Logger &l); 98 | Logger &log(); 99 | Logger &log(LogLevel lvl); 100 | 101 | template void error(const ArgTs &... args) { 102 | if (chk_sanity) 103 | throw ErrT(args...); 104 | else 105 | log(TF_ERR) << ErrT(args...).what() << flush; 106 | } 107 | 108 | 109 | } // namespace thinkfan 110 | 111 | 112 | #ifdef USE_ATASMART 113 | #define DND_DISK_HELP \ 114 | "\n -d Don't read S.M.A.R.T. temperature from sleeping disks" 115 | #else 116 | #define DND_DISK_HELP "" 117 | #endif 118 | 119 | #define MSG_TITLE "thinkfan " VERSION ": A minimalist fan control program" 120 | 121 | #define MSG_USAGE \ 122 | "Usage: thinkfan [-hnqDd [-b BIAS] [-c CONFIG] [-s SECONDS] [-p [SECONDS]]]" \ 123 | "\n -h This help message" \ 124 | "\n -s Maximum cycle time in seconds (Integer. Default: 5)" \ 125 | "\n -b Floating point number (-10 to 30) to control rising temperature" \ 126 | "\n exaggeration (see thinkfan(5)). Default: 0.0" \ 127 | "\n -c Load different configuration file (default: /etc/thinkfan.conf)" \ 128 | "\n -n Do not become a daemon and log to terminal instead of syslog" \ 129 | "\n -q Be more quiet. Can be specified up to three times so that only errors" \ 130 | "\n are logged." \ 131 | "\n -v Enable verbose logging (e.g. log temperatures continuously)." \ 132 | "\n -p Use the pulsing-fan workaround (for worn out fans). Takes an optional" \ 133 | "\n floating-point argument (0 ~ 10s) as depulsing duration. Default 0.5s." \ 134 | DND_DISK_HELP \ 135 | "\n -D DANGEROUS mode: Disable all sanity checks. May result in undefined" \ 136 | "\n behaviour!\n" 137 | 138 | #define MSG_FILE_HDR(file, line_count, line) file + ":" + std::to_string(line_count) + ":" + line 139 | #define MSG_RELOAD_CONF "Received SIGHUP: reloading config..." 140 | #define MSG_SANITY "Sanity checks are on. Exiting." 141 | #define MSG_INSANITY "Sanity checks are off. Continuing." 142 | #define MSG_CONFIG(path) \ 143 | "Config as read from " + path + ":\nFan level\tLow\tHigh" 144 | #define MSG_CONF_ITEM(level, low, high) " " + std::to_string(level) + "\t\t" + std::to_string(low) + "\t" + std::to_string(high) 145 | #define MSG_TERM "Cleaning up and resetting fan control." 146 | #define MSG_DEPULSE(delay, time) "Disengaging the fan controller for " \ 147 | << time << " seconds every " << delay << " seconds" 148 | #define MSG_SYSFS_SAFE "Using safe but wasteful way of setting PWM value. Check README to know more." 149 | #define MSG_RUNNING PID_FILE " already exists. Either thinkfan is " \ 150 | "already running, or it was killed by SIGKILL. If you're sure thinkfan" \ 151 | " is not running, delete " PID_FILE " manually." 152 | #define MSG_SENSOR_LOST "A sensor has vanished! Exiting since there's no " \ 153 | "safe way of handling this." 154 | 155 | #define TRACKER_URL "https://github.com/vmatare/thinkfan/issues" 156 | #define MSG_BUG "This is probably a bug. Please consider reporting this at " TRACKER_URL ". Thanks." 157 | 158 | 159 | #define MSG_NO_SENSOR "No sensors in config file." 160 | #define MSG_T_GET(file) string(__func__) + ": Failed to read temperature(s) from " + file + ": " 161 | #define MSG_T_INVALID(s, d) s + ": Invalid temperature: " + std::to_string(d) 162 | #define MSG_SENSOR_INIT(file) string(__func__) + ": Initializing sensor in " + file + ": " 163 | #define MSG_MULTIPLE_HWMONS_FOUND "Found multiple hwmons with this name: " 164 | 165 | 166 | #define MSG_FAN_MODOPTS \ 167 | "Kernel module thinkpad_acpi: Fan_control seems disabled. See this link:\n" \ 168 | "https://www.kernel.org/doc/html/latest/admin-guide/laptops/thinkpad-acpi.html?highlight=thinkpad_acpi#fan-control-and-monitoring-fan-speed-fan-enable-disable\n" \ 169 | "Or execute the following as root:\n" \ 170 | "echo \"options thinkpad_acpi fan_control=1\" > /etc/modprobe.d/99-thinkfan.conf" 171 | #define MSG_FAN_CTRL(str, fan) string(__func__) + ": Writing \"" + str + "\" to " + fan + ": " 172 | #define MSG_FAN_INIT(fan) string(__func__) + ": Initializing fan control in " + fan + ": " 173 | #define MSG_FAN_RESET(fan) string(__func__) + ": Resetting fan control in " + fan + ": " 174 | #define MSG_FAN_EPERM(fan) string(__func__) + ": No permission to write to " + fan \ 175 | + ". Thinkfan needs to be run as root!" 176 | 177 | 178 | #define MSG_OPT_S_15(t) std::to_string(t) + " seconds of not realizing "\ 179 | "rising temperatures may be dangerous!" 180 | #define MSG_OPT_S_1(t) "A sleeptime of " + std::to_string(t) + " seconds doesn't make much " \ 181 | "sense." 182 | #define MSG_OPT_S "option -s requires an int argument!" 183 | #define MSG_OPT_S_INVAL(x) string("invalid argument to option -s: ") + x 184 | #define MSG_OPT_B "bias must be between -10 and 30!" 185 | #define MSG_OPT_B_NOARG "option -b requires an argument!" 186 | #define MSG_OPT_B_INVAL(x) string("invalid argument to option -b: ") + x 187 | #define MSG_OPT_P(x) string("invalid argument to option -p: ") + x 188 | 189 | 190 | #define MSG_CONF_DEFAULT_FAN "Using default fan control in " DEFAULT_FAN "." 191 | #define MSG_CONF_NOBIAS(t) "You're using simple temperature limits" \ 192 | " without correction values, and your fan will only start at " << t << " °C. This can " \ 193 | "be dangerous for your hard drive." 194 | #define MSG_CONF_NUM_TEMPS(n, i) "You have " << std::to_string(n) << " sensors but your temper" \ 195 | "ature limits only have " << std::to_string(i) << " entries. Excess sensors will be ignored." 196 | #define MSG_CONF_SENSOR_DEPRECATED "The `sensor' keyword is deprecated. " \ 197 | "Please use the `hwmon' or `tp_thermal' keywords instead!" 198 | #define MSG_CONF_FAN_DEPRECATED "Guessing the fan type from the path" \ 199 | " is deprecated. Please use `tp_fan' or `pwm_fan' to make things clear." 200 | #define MSG_CONF_RELOAD_ERR "Error reloading config. Keeping old one." 201 | #define MSG_CONF_NOFAN "Could not find any fan speed settings in" \ 202 | " the config file. Please read AND UNDERSTAND the documentation!" 203 | #define MSG_CONF_LOWHIGH "Your LOWER limit is not lesser than your " \ 204 | "UPPER limit. That doesn't make sense." 205 | #define MSG_CONF_OVERLAP "LOWER limit doesn't overlap with previous UPPER limit" 206 | #define MSG_CONF_FAN "Thinkfan can't use more than one fan" 207 | #define MSG_CONF_LVLORDER "Fan levels are not ordered correctly" 208 | #define MSG_CONF_PARSE "Syntax error" 209 | #define MSG_CONF_LVL0 "The LOWER limit of the first fan level cannot con" \ 210 | "tain any values greater than 0!" 211 | #define MSG_CONF_LVLFORMAT(s) "Unrecognized fan level string: " + s 212 | #define MSG_CONF_LCOUNT "The number of limits must either be 1 or equal to the" \ 213 | " number of temperatures" 214 | #define MSG_CONF_LONG_LIMIT "You have configured more temperature limits " \ 215 | "than sensors. That doesn't make sense" 216 | #define MSG_CONF_LIMITLEN "Inconsistent limit length" 217 | #define MSG_CONF_CORRECTION_LEN(path, clen, ntemp) string("Sensor ") + path + " has " \ 218 | + std::to_string(ntemp) + " temperatures," \ 219 | " but you have " + std::to_string(clen) + " correction values for it." 220 | #define MSG_CONF_ATASMART_UNSUPP "S.M.A.R.T support is not compiled in. Recompile with -DUSE_ATASMART or " \ 221 | "contact your distribution's package maintainer." 222 | #define MSG_CONF_NVML_UNSUPP "NVML support is not compiled in. Recompile with -DUSE_NVML or " \ 223 | "contact your distribution's package maintainer." 224 | #define MSG_TEMP_COUNT(t_conf, t_found) "Your config requires at least " << t_conf << " temperatures, " \ 225 | "but only " << t_found << " temperatures were found." 226 | #define MSG_CONF_MAXLVL(n) "You're using a PWM fan, but your highest fan level is only " + std::to_string(n) \ 227 | + ". Enable DANGEROUS mode if you're really sure you never need to max out your fan" 228 | #define MSG_CONF_TP_LVL7(n, max) "Your highest fan level is " + std::to_string(n) + \ 229 | ", but fan levels greater than " + std::to_string(max) + " are not supported by thinkpad_acpi" 230 | 231 | #define MSG_CONF_MISSING_LOWER_LIMIT "You must specify a lower limit on all but the first fan level" 232 | #define MSG_CONF_MISSING_UPPER_LIMIT "You must specify an upper limit on all but the last fan level" 233 | 234 | 235 | #endif 236 | -------------------------------------------------------------------------------- /src/parser.cpp: -------------------------------------------------------------------------------- 1 | /******************************************************************** 2 | * parser.cpp: Recursive descent parser for the config. 3 | * (C) 2015, Victor Mataré 4 | * 5 | * this file is part of thinkfan. See thinkfan.c for further information. 6 | * 7 | * thinkfan is free software: you can redistribute it and/or modify 8 | * it under the terms of the GNU General Public License as published by 9 | * the Free Software Foundation, either version 3 of the License, or 10 | * (at your option) any later version. 11 | * 12 | * thinkfan is distributed in the hope that it will be useful, 13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | * GNU General Public License for more details. 16 | * 17 | * You should have received a copy of the GNU General Public License 18 | * along with thinkfan. If not, see . 19 | * 20 | * ******************************************************************/ 21 | 22 | #include "error.h" 23 | #include "parser.h" 24 | #include "config.h" 25 | #include "message.h" 26 | #include 27 | 28 | namespace thinkfan { 29 | 30 | using namespace std; 31 | 32 | 33 | const char *ErrorTracker::max_addr_ = nullptr; 34 | 35 | static RegexParser separator_parser("^([[:space:]]*,)|^([[:space:]]+)", 0); 36 | static CommentParser comment_parser; 37 | static RegexParser space_parser("^[[:space:]]+", 0, true); 38 | static RegexParser blank_parser("^[[:blank:]]+", 0, false); 39 | 40 | 41 | RegexParser::RegexParser(const string expr, const unsigned int data_idx, bool match_nl) 42 | : data_idx_(data_idx), 43 | re_str(expr) 44 | { 45 | this->expr_ = new regex_t; 46 | if (regcomp(this->expr_, re_str.c_str(), REG_EXTENDED | (match_nl ? 0 : REG_NEWLINE))) { 47 | throw Bug("RegexParser: Invalid regular expression"); 48 | } 49 | } 50 | 51 | 52 | RegexParser::~RegexParser() 53 | { 54 | regfree(expr_); 55 | delete expr_; 56 | } 57 | 58 | 59 | string *RegexParser::_parse(const char *&input) 60 | { 61 | regmatch_t matches[data_idx_ + 1]; 62 | string *rv = nullptr; 63 | 64 | int err; 65 | if (!(err = regexec(this->expr_, input, this->data_idx_ + 1, matches, 0))) { 66 | int so = matches[data_idx_].rm_so; 67 | int eo = matches[data_idx_].rm_eo; 68 | if (so != -1 && matches[0].rm_so == 0) { 69 | rv = new string(input, so, eo - so); 70 | input += matches->rm_eo; 71 | } 72 | } 73 | else if (err == REG_ESPACE) { 74 | delete rv; 75 | throw ParserOOM(); 76 | } 77 | return rv; 78 | } 79 | 80 | 81 | int *IntParser::_parse(const char *&input) 82 | { 83 | char *end; 84 | long l = strtol(input, &end, 0); 85 | if (end == input || l < numeric_limits::min() || l > numeric_limits::max()) 86 | return nullptr; 87 | 88 | input = end; 89 | return new int(static_cast(l)); 90 | } 91 | 92 | 93 | CommentParser::CommentParser() 94 | : comment_parser_("^[[:space:]]*#(.*)$", 1) 95 | {} 96 | 97 | 98 | string *CommentParser::_parse(const char *&input) 99 | { 100 | space_parser.match(input); 101 | string *rv = nullptr; 102 | string *tmp; 103 | while ((tmp = comment_parser_.parse(input))) { 104 | if (!rv) 105 | rv = tmp; 106 | else { 107 | *rv += *tmp; 108 | delete tmp; 109 | } 110 | space_parser.match(input); 111 | } 112 | return rv; 113 | } 114 | 115 | 116 | KeywordParser::KeywordParser(const string keyword) 117 | : RegexParser("^[[:space:]]*" + keyword + "[[:space:]]+([^[:space:]]+|\"[^\"]+\")", 1) 118 | {} 119 | 120 | 121 | FanDriver *FanParser::_parse(const char *&input) 122 | { 123 | FanDriver *fan = nullptr; 124 | unique_ptr path; 125 | 126 | if ((path = unique_ptr(KeywordParser("fan").parse(input)))) 127 | throw ConfigError(MSG_CONF_FAN_DEPRECATED); 128 | else if ((path = unique_ptr(KeywordParser("tp_fan").parse(input)))) 129 | fan = new TpFanDriver(*path); 130 | else if ((path = unique_ptr(KeywordParser("pwm_fan").parse(input)))) 131 | fan = new HwmonFanDriver(*path); 132 | 133 | return fan; 134 | } 135 | 136 | 137 | SensorDriver *SensorParser::_parse(const char *&input) 138 | { 139 | SensorDriver *sensor = nullptr; 140 | unique_ptr path; 141 | 142 | if ((path = unique_ptr(KeywordParser("sensor").parse(input)))) 143 | throw ConfigError(MSG_CONF_SENSOR_DEPRECATED); 144 | else if ((path = unique_ptr(KeywordParser("tp_thermal").parse(input)))) 145 | sensor = new TpSensorDriver(*path, false); 146 | else if ((path = unique_ptr(KeywordParser("hwmon").parse(input)))) 147 | sensor = new HwmonSensorDriver(*path, false); 148 | else if ((path = unique_ptr(KeywordParser("atasmart").parse(input)))) { 149 | #ifdef USE_ATASMART 150 | sensor = new AtasmartSensorDriver(*path, false); 151 | #else 152 | error(MSG_CONF_ATASMART_UNSUPP); 153 | #endif /* USE_ATASMART */ 154 | } 155 | else if ((path = unique_ptr(KeywordParser("nv_thermal").parse(input)))) { 156 | #ifdef USE_NVML 157 | sensor = new NvmlSensorDriver(*path, false); 158 | #else 159 | error(MSG_CONF_NVML_UNSUPP); 160 | #endif /* USE_NVML */ 161 | } 162 | 163 | if (sensor) { 164 | blank_parser.match(input); 165 | unique_ptr> correction(TupleParser().parse(input)); 166 | if (correction) 167 | sensor->set_correction(*correction); 168 | } 169 | 170 | return sensor; 171 | } 172 | 173 | 174 | IntListParser::IntListParser(bool allow_dot) 175 | : dot_parser_("^\\."), 176 | allow_dot_(allow_dot) 177 | {} 178 | 179 | 180 | vector *IntListParser::_parse(const char *&input) 181 | { 182 | unique_ptr> rv(new vector()); 183 | unique_ptr dot; 184 | unique_ptr i; 185 | 186 | do { 187 | dot.reset(nullptr); 188 | 189 | if ( (void)i.reset(int_parser_.parse(input)), i) 190 | rv->push_back(*i); 191 | else if (allow_dot_ && ( (void)dot.reset(dot_parser_.parse(input)), dot)) 192 | rv->push_back(numeric_limits::max()); 193 | else 194 | break; 195 | if (!(separator_parser.match(input) || comment_parser.match(input))) 196 | break; 197 | 198 | comment_parser.match(input); 199 | } while(i || dot); 200 | 201 | if (rv->size() > 0) 202 | return rv.release(); 203 | return nullptr; 204 | } 205 | 206 | 207 | EnclosureParser::EnclosureParser(initializer_list bracket_pairs, bool nl) 208 | : brackets_(bracket_pairs), 209 | nl_(nl) 210 | { 211 | if (bracket_pairs.size() < 2 || bracket_pairs.size() % 2 != 0) 212 | throw Bug("EnclosureParser: Unbalanced bracket definition"); 213 | } 214 | 215 | 216 | string *EnclosureParser::_parse(const char *&input) 217 | { 218 | blank_parser.match(input); 219 | vector::const_iterator it = brackets_.begin(); 220 | unique_ptr rv; 221 | do { 222 | string re_pfx("^"); 223 | if (*it == "(" || *it == "{") 224 | re_pfx += "\\"; 225 | RegexParser re_o(re_pfx + *it++); 226 | rv.reset(re_o.parse(input)); 227 | if (rv) { 228 | closing_ = re_pfx + *it; 229 | content_ = "^[^" + *it + "]*"; 230 | } 231 | ++it; 232 | } while (!rv && it != brackets_.end()); 233 | 234 | return rv.release(); 235 | } 236 | 237 | 238 | bool EnclosureParser::open(const char *&input) 239 | { return match(input); } 240 | 241 | 242 | bool EnclosureParser::close(const char *&input) 243 | { return RegexParser(closing_).match(input); } 244 | 245 | 246 | string *EnclosureParser::content(const char *&input) 247 | { return RegexParser(content_, 0, nl_).parse(input); } 248 | 249 | 250 | BracketParser::BracketParser() 251 | : EnclosureParser({"(", ")", "{", "}"}) 252 | {} 253 | 254 | 255 | vector *TupleParser::_parse(const char *&input) 256 | { 257 | if (!bracket_parser_.open(input)) 258 | return nullptr; 259 | 260 | unique_ptr> rv(int_list_parser_.parse(input)); 261 | if (bracket_parser_.close(input)) 262 | return rv.release(); 263 | else 264 | return nullptr; 265 | } 266 | 267 | 268 | SimpleLevel *SimpleLevelParser::_parse(const char *&input) 269 | { 270 | if (!bracket_parser_.open(input)) 271 | return nullptr; 272 | 273 | unique_ptr> ints; 274 | 275 | comment_parser.match(input); 276 | 277 | EnclosureParser quot({"\"", "\""}); 278 | if (quot.open(input)) { 279 | unique_ptr lvl_str(quot.content(input)); 280 | if (quot.close(input) && lvl_str && 281 | (comment_parser.match(input) 282 | || separator_parser.match(input)) 283 | ) { 284 | comment_parser.match(input); 285 | ints.reset(IntListParser().parse(input)); 286 | if (ints && ints->size() == 2 && bracket_parser_.close(input)) 287 | return new SimpleLevel(*lvl_str, (*ints)[0], (*ints)[1]); 288 | } 289 | } 290 | else { 291 | ints.reset(IntListParser().parse(input)); 292 | if (ints && ints->size() == 3 && bracket_parser_.close(input)) 293 | return new SimpleLevel((*ints)[0], (*ints)[1], (*ints)[2]); 294 | } 295 | 296 | return nullptr; 297 | } 298 | 299 | 300 | ComplexLevel *ComplexLevelParser::_parse(const char *&input) 301 | { 302 | unique_ptr lvl_str; 303 | unique_ptr> lower_lim, upper_lim; 304 | unique_ptr lvl_int; 305 | 306 | if (!bracket_parser_.open(input)) 307 | return nullptr; 308 | 309 | comment_parser.match(input); 310 | space_parser.match(input); 311 | 312 | EnclosureParser quot({"\"", "\""}); 313 | if (quot.open(input)) { 314 | lvl_str.reset(quot.content(input)); 315 | if (!quot.close(input)) 316 | return nullptr; 317 | } 318 | else if (!(lvl_int = unique_ptr(IntParser().parse(input)))) 319 | return nullptr; 320 | 321 | TupleParser tp(true); 322 | RegexParser sep_parser("^[[:space:]]+|^[[:space:]]*,?[[:space:]]*", 0, true); 323 | 324 | comment_parser.match(input); 325 | sep_parser.match(input); 326 | comment_parser.match(input); 327 | 328 | lower_lim = unique_ptr>(tp.parse(input)); 329 | comment_parser.match(input); 330 | sep_parser.match(input); 331 | comment_parser.match(input); 332 | 333 | upper_lim = unique_ptr>(tp.parse(input)); 334 | space_parser.match(input); 335 | comment_parser.match(input); 336 | 337 | if (!(lower_lim && upper_lim)) 338 | return nullptr; 339 | 340 | if (!bracket_parser_.close(input)) 341 | return nullptr; 342 | 343 | if (lvl_str) 344 | return new ComplexLevel(*lvl_str, *lower_lim, *upper_lim); 345 | else if (lvl_int) 346 | return new ComplexLevel(*lvl_int, *lower_lim, *upper_lim); 347 | else 348 | return nullptr; 349 | } 350 | 351 | 352 | ConfigParser::ConfigParser() 353 | : parser_fan(), 354 | parser_sensor() 355 | {} 356 | 357 | 358 | Config *ConfigParser::_parse(const char *&input) 359 | { 360 | // Use smart pointers here since we may cause an exception (rv->add_*()...) 361 | unique_ptr rv(new Config()); 362 | unique_ptr fan_cfg(new StepwiseMapping()); 363 | 364 | bool some_match = false; 365 | do { 366 | some_match = comment_parser.match(input) 367 | || space_parser.match(input); 368 | unique_ptr fan { parser_fan.parse(input) }; 369 | if (fan) { 370 | fan_cfg->set_fan(std::move(fan)); 371 | some_match = true; 372 | } 373 | unique_ptr sensor { parser_sensor.parse(input) }; 374 | if (sensor) { 375 | rv->add_sensor(std::move(sensor)); 376 | some_match = true; 377 | } 378 | unique_ptr simple_lvl { parser_simple_lvl.parse(input) }; 379 | if (simple_lvl) { 380 | fan_cfg->add_level(std::move(simple_lvl)); 381 | some_match = true; 382 | } 383 | unique_ptr complex_lvl { parser_complex_lvl.parse(input) }; 384 | if (complex_lvl) { 385 | fan_cfg->add_level(std::move(complex_lvl)); 386 | some_match = true; 387 | } 388 | } while(*input != 0 && some_match); 389 | 390 | rv->add_fan_config(std::move(fan_cfg)); 391 | 392 | if (*input != 0 && !some_match) return nullptr; 393 | 394 | return rv.release(); 395 | } 396 | 397 | } 398 | 399 | 400 | 401 | 402 | 403 | -------------------------------------------------------------------------------- /src/thinkfan.cpp: -------------------------------------------------------------------------------- 1 | /******************************************************************** 2 | * thinkfan.cpp: Main program. 3 | * (C) 2015, Victor Mataré 4 | * 5 | * this file is part of thinkfan. 6 | * 7 | * thinkfan is free software: you can redistribute it and/or modify 8 | * it under the terms of the GNU General Public License as published by 9 | * the Free Software Foundation, either version 3 of the License, or 10 | * (at your option) any later version. 11 | * 12 | * thinkfan is distributed in the hope that it will be useful, 13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | * GNU General Public License for more details. 16 | * 17 | * You should have received a copy of the GNU General Public License 18 | * along with thinkfan. If not, see . 19 | * 20 | * ******************************************************************/ 21 | 22 | #include "error.h" 23 | 24 | #include 25 | #include 26 | #include 27 | #include 28 | #include 29 | 30 | #include 31 | #include 32 | #include 33 | #include 34 | #include 35 | #include 36 | 37 | #include 38 | 39 | #include "thinkfan.h" 40 | #include "config.h" 41 | #include "message.h" 42 | #include "error.h" 43 | #include "sensors.h" 44 | #include "fans.h" 45 | #include "temperature_state.h" 46 | 47 | 48 | namespace thinkfan { 49 | 50 | bool chk_sanity(true); 51 | bool quiet(false); 52 | bool daemonize(true); 53 | seconds sleeptime(5); 54 | seconds tmp_sleeptime = sleeptime; 55 | float bias_level(0); 56 | float depulse = 0; 57 | static TemperatureState temp_state(0); 58 | std::atomic tolerate_errors(0); 59 | 60 | std::condition_variable sleep_cond; 61 | std::mutex sleep_mutex; 62 | 63 | #ifdef USE_YAML 64 | vector config_files { DEFAULT_YAML_CONFIG, DEFAULT_CONFIG }; 65 | #else 66 | vector config_files { DEFAULT_CONFIG }; 67 | #endif 68 | 69 | std::atomic interrupted(0); 70 | 71 | #ifdef USE_ATASMART 72 | /** Do Not Disturb disk, i.e. don't get temperature from a sleeping disk */ 73 | bool dnd_disk = false; 74 | #endif /* USE_ATASMART */ 75 | 76 | 77 | #if defined(PID_FILE) 78 | 79 | PidFileHolder *PidFileHolder::instance_ = nullptr; 80 | 81 | PidFileHolder::PidFileHolder(::pid_t pid) 82 | : pid_file_(PID_FILE, std::ios_base::in) 83 | { 84 | if (!pid_file_.fail()) 85 | error(MSG_RUNNING); 86 | if (instance_) 87 | throw Bug("Attempt to initialize PID file twice"); 88 | pid_file_.close(); 89 | pid_file_.open(PID_FILE, std::ios_base::out | std::ios_base::trunc); 90 | if (!(pid_file_ << pid << std::flush)) 91 | error("Writing to " PID_FILE ": ", errno); 92 | instance_ = this; 93 | } 94 | 95 | PidFileHolder::~PidFileHolder() 96 | { remove_file(); } 97 | 98 | bool PidFileHolder::file_exists() 99 | { return !ifstream(PID_FILE).fail(); } 100 | 101 | 102 | void PidFileHolder::remove_file() 103 | { 104 | if (pid_file_.is_open()) { 105 | pid_file_.close(); 106 | if (::unlink(PID_FILE) == -1) 107 | log(TF_ERR) << "Deleting " PID_FILE ": " << errno << "." << flush; 108 | } 109 | } 110 | 111 | 112 | void PidFileHolder::cleanup() 113 | { 114 | if (instance_) 115 | instance_->remove_file(); 116 | } 117 | 118 | #else 119 | 120 | void PidFileHolder::cleanup() 121 | {} 122 | 123 | #endif // defined(PID_FILE) 124 | 125 | 126 | void sleep(thinkfan::seconds duration) { 127 | auto until = std::chrono::steady_clock::now() + duration; 128 | 129 | std::unique_lock sleep_locked(sleep_mutex); 130 | sleep_cond.wait_until(sleep_locked, until, [] () { 131 | return interrupted != 0; 132 | } ); 133 | } 134 | 135 | 136 | void sig_handler(int signum) { 137 | switch(signum) { 138 | case SIGHUP: 139 | case SIGINT: 140 | case SIGTERM: 141 | interrupted = signum; 142 | sleep_cond.notify_all(); 143 | break; 144 | case SIGUSR1: 145 | log(TF_NFY) << temp_state << flush; 146 | break; 147 | #ifndef DISABLE_BUGGER 148 | case SIGSEGV: 149 | // Let's hope memory isn't too fucked up to get through with this ;) 150 | throw Bug("Segmentation fault."); 151 | #endif 152 | case SIGUSR2: 153 | interrupted = signum; 154 | sleep_cond.notify_all(); 155 | log(TF_NFY) << "Received SIGUSR2: Re-initializing fan control." << flush; 156 | break; 157 | case SIGPWR: 158 | tolerate_errors = 4; 159 | log(TF_NFY) << "Going to sleep: Will allow sensor read errors for the next " 160 | << std::to_string(tolerate_errors) << " loops." << flush; 161 | } 162 | } 163 | 164 | 165 | void run(const Config &config) 166 | { 167 | tmp_sleeptime = sleeptime; 168 | 169 | for (const unique_ptr &sensor : config.sensors()) 170 | sensor->read_temps(); 171 | 172 | // Set initial fan level 173 | for (auto &fan_config : config.fan_configs()) 174 | fan_config->init_fanspeed(temp_state); 175 | log(TF_NFY) << temp_state << " -> " << config.fan_configs() << flush; 176 | 177 | bool did_something = false; 178 | while (likely(!interrupted)) { 179 | sleep(tmp_sleeptime); 180 | 181 | if (unlikely(interrupted)) 182 | break; 183 | 184 | for (const unique_ptr &sensor : config.sensors()) 185 | sensor->read_temps(); 186 | 187 | if (unlikely(tolerate_errors) > 0) 188 | tolerate_errors--; 189 | 190 | for (auto &fan_config : config.fan_configs()) 191 | did_something |= fan_config->set_fanspeed(temp_state); 192 | 193 | if (unlikely(did_something)) 194 | log(TF_NFY) << temp_state << " -> " << config.fan_configs() << flush; 195 | 196 | did_something = false; 197 | } 198 | } 199 | 200 | 201 | int set_options(int argc, char **argv) 202 | { 203 | const char *optstring = "c:s:b:p::hqDznv" 204 | #ifdef USE_ATASMART 205 | "d"; 206 | #else 207 | ; 208 | #endif 209 | opterr = 0; 210 | int opt; 211 | while ((opt = getopt(argc, argv, optstring)) != -1) { 212 | switch(opt) { 213 | case 'h': 214 | log(TF_NFY) << MSG_TITLE << flush << MSG_USAGE << flush; 215 | return 1; 216 | #ifdef USE_ATASMART 217 | case 'd': 218 | dnd_disk = true; 219 | break; 220 | #endif 221 | case 'c': 222 | config_files = vector({optarg}); 223 | break; 224 | case 'q': 225 | --Logger::instance().log_lvl(); 226 | break; 227 | case 'v': 228 | ++Logger::instance().log_lvl(); 229 | break; 230 | case 'D': 231 | chk_sanity = false; 232 | break; 233 | case 'z': 234 | log(TF_WRN) << "Option -z is not needed anymore and therefore deprecated." << flush; 235 | break; 236 | case 'n': 237 | daemonize = false; 238 | break; 239 | case 's': 240 | if (optarg) { 241 | try { 242 | size_t invalid; 243 | int s; 244 | string arg(optarg); 245 | s = int(std::stoul(arg, &invalid)); 246 | if (invalid < arg.length()) 247 | throw InvocationError(MSG_OPT_S_INVAL(optarg)); 248 | if (s > 15) 249 | throw InvocationError(MSG_OPT_S_15(s)); 250 | else if (s < 0) 251 | throw InvocationError("Negative sleep time? Seriously?"); 252 | else if (s < 1) 253 | throw InvocationError(MSG_OPT_S_1(s)); 254 | sleeptime = seconds(static_cast(s)); 255 | } catch (std::invalid_argument &) { 256 | throw InvocationError(MSG_OPT_S_INVAL(optarg)); 257 | } catch (std::out_of_range &) { 258 | throw InvocationError(MSG_OPT_S_INVAL(optarg)); 259 | } 260 | } 261 | else throw InvocationError(MSG_OPT_S); 262 | break; 263 | case 'b': 264 | if (optarg) { 265 | try { 266 | size_t invalid; 267 | float b; 268 | string arg(optarg); 269 | b = std::stof(arg, &invalid); 270 | if (invalid < arg.length()) 271 | error(MSG_OPT_B_INVAL(optarg)); 272 | if (b < -10 || b > 30) 273 | error(MSG_OPT_B); 274 | bias_level = b / 10; 275 | } catch (std::invalid_argument &) { 276 | throw InvocationError(MSG_OPT_B_INVAL(optarg)); 277 | } catch (std::out_of_range &) { 278 | throw InvocationError(MSG_OPT_B_INVAL(optarg)); 279 | } 280 | } 281 | else throw InvocationError(MSG_OPT_B_NOARG); 282 | break; 283 | case 'p': 284 | if (optarg) { 285 | size_t invalid; 286 | string arg(optarg); 287 | depulse = std::stof(arg, &invalid); 288 | if (invalid < arg.length() || depulse > 10 || depulse < 0) 289 | error(MSG_OPT_P(optarg)); 290 | } 291 | else depulse = 0.5f; 292 | break; 293 | default: 294 | throw InvocationError(string("Unknown option: -") + static_cast(optopt)); 295 | } 296 | } 297 | if (depulse > 0) 298 | log(TF_NFY) << MSG_DEPULSE(depulse, sleeptime.count()) << flush; 299 | 300 | return 0; 301 | } 302 | 303 | 304 | void noop() 305 | {} 306 | 307 | 308 | 309 | } // namespace thinkfan 310 | 311 | 312 | int main(int argc, char **argv) { 313 | using namespace thinkfan; 314 | 315 | struct sigaction handler; 316 | #if defined(PID_FILE) 317 | unique_ptr pid_file; 318 | #endif 319 | 320 | if (!isatty(fileno(stdout))) { 321 | Logger::instance().enable_syslog(); 322 | } 323 | 324 | #if not defined(DISABLE_BUGGER) 325 | std::set_terminate(handle_uncaught); 326 | #endif 327 | 328 | memset(&handler, 0, sizeof(handler)); 329 | handler.sa_handler = sig_handler; 330 | 331 | if (sigaction(SIGHUP, &handler, nullptr) 332 | || sigaction(SIGINT, &handler, nullptr) 333 | || sigaction(SIGTERM, &handler, nullptr) 334 | || sigaction(SIGUSR1, &handler, nullptr) 335 | || sigaction(SIGPWR, &handler, nullptr) 336 | #if not defined(DISABLE_BUGGER) 337 | || sigaction(SIGSEGV, &handler, nullptr) 338 | #endif 339 | || sigaction(SIGUSR2, &handler, nullptr)) { 340 | string msg = strerror(errno); 341 | log(TF_ERR) << "sigaction: " << msg; 342 | return 1; 343 | } 344 | 345 | #if not defined(DISABLE_EXCEPTION_CATCHING) 346 | try { 347 | #endif // DISABLE_EXCEPTION_CATCHING 348 | switch (set_options(argc, argv)) { 349 | case 1: 350 | return 0; 351 | case 0: 352 | break; 353 | default: 354 | return 3; 355 | } 356 | 357 | #if defined(PID_FILE) 358 | if (PidFileHolder::file_exists()) 359 | error(MSG_RUNNING); 360 | #endif 361 | 362 | if (daemonize) { 363 | LogLevel old_lvl = Logger::instance().log_lvl(); 364 | { 365 | // Test the config before forking 366 | unique_ptr test_cfg(Config::read_config(config_files)); 367 | 368 | Logger::instance().log_lvl() = TF_ERR; 369 | 370 | test_cfg->init(temp_state); 371 | 372 | for (auto &sensor : test_cfg->sensors()) 373 | sensor->read_temps(); 374 | 375 | // Own scope so the config gets destroyed before forking 376 | } 377 | Logger::instance().log_lvl() = old_lvl; 378 | 379 | 380 | pid_t child_pid = ::fork(); 381 | if (child_pid < 0) { 382 | string msg(strerror(errno)); 383 | error("Can't fork(): " + msg); 384 | } 385 | else if (child_pid > 0) { 386 | log(TF_NFY) << "Daemon PID: " << child_pid << flush; 387 | return 0; 388 | } 389 | else { 390 | Logger::instance().enable_syslog(); 391 | #if defined(PID_FILE) 392 | // Own PID file only in the child... 393 | pid_file.reset(new PidFileHolder(::getpid())); 394 | #endif 395 | } 396 | } 397 | #if defined(PID_FILE) 398 | else { 399 | // ... or when we're not forking at all 400 | pid_file.reset(new PidFileHolder(::getpid())); 401 | } 402 | #endif 403 | 404 | // Load the config for real after forking & enabling syslog 405 | unique_ptr config(Config::read_config(config_files)); 406 | 407 | do { 408 | config->init(temp_state); 409 | run(*config); 410 | 411 | if (interrupted == SIGHUP) { 412 | log(TF_NFY) << MSG_RELOAD_CONF << flush; 413 | try { 414 | unique_ptr config_new(Config::read_config(config_files)); 415 | config.swap(config_new); 416 | } catch(ExpectedError &) { 417 | log(TF_ERR) << MSG_CONF_RELOAD_ERR << flush; 418 | } catch(std::exception &e) { 419 | log(TF_ERR) << "read_config: " << e.what() << flush; 420 | log(TF_ERR) << MSG_CONF_RELOAD_ERR << flush; 421 | } 422 | interrupted = 0; 423 | } 424 | else if (interrupted == SIGUSR2) { 425 | config->init_fans(); 426 | interrupted = 0; 427 | } 428 | } while (!interrupted); 429 | 430 | log(TF_NFY) << MSG_TERM << flush; 431 | #if not defined(DISABLE_EXCEPTION_CATCHING) 432 | } 433 | catch (InvocationError &e) { 434 | log(TF_ERR) << e.what() << flush; 435 | log(TF_NFY) << MSG_USAGE << flush; 436 | } 437 | catch (ExpectedError &e) { 438 | log(TF_ERR) << e.what() << flush; 439 | return 1; 440 | } 441 | catch (Bug &e) { 442 | log(TF_ERR) << e.what() << flush << 443 | "Backtrace:" << flush << 444 | e.backtrace() << flush << 445 | MSG_BUG << flush; 446 | return 2; 447 | } 448 | #endif // DISABLE_EXCEPTION_CATCHING 449 | 450 | return 0; 451 | } 452 | 453 | 454 | 455 | 456 | -------------------------------------------------------------------------------- /src/config.cpp: -------------------------------------------------------------------------------- 1 | /******************************************************************** 2 | * config.cpp: Config data structures and consistency checking. 3 | * (C) 2015, Victor Mataré 4 | * 5 | * this file is part of thinkfan. See thinkfan.c for further information. 6 | * 7 | * thinkfan is free software: you can redistribute it and/or modify 8 | * it under the terms of the GNU General Public License as published by 9 | * the Free Software Foundation, either version 3 of the License, or 10 | * (at your option) any later version. 11 | * 12 | * thinkfan is distributed in the hope that it will be useful, 13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | * GNU General Public License for more details. 16 | * 17 | * You should have received a copy of the GNU General Public License 18 | * along with thinkfan. If not, see . 19 | * 20 | * ******************************************************************/ 21 | 22 | #include "config.h" 23 | #include 24 | #include 25 | #include 26 | #include 27 | #include 28 | #include 29 | #include "parser.h" 30 | #include "message.h" 31 | #include "thinkfan.h" 32 | 33 | #ifdef USE_YAML 34 | #include "yamlconfig.h" 35 | #endif 36 | 37 | namespace thinkfan { 38 | 39 | 40 | FanConfig::FanConfig(unique_ptr &&fan_drv) 41 | : fan_(std::move(fan_drv)) 42 | {} 43 | 44 | const unique_ptr &FanConfig::fan() const 45 | { return fan_; } 46 | 47 | void FanConfig::set_fan(unique_ptr &&fan) 48 | { fan_ = std::move(fan); } 49 | 50 | 51 | 52 | StepwiseMapping::StepwiseMapping(unique_ptr &&fan_drv) 53 | : FanConfig(std::move(fan_drv)) 54 | {} 55 | 56 | const vector> &StepwiseMapping::levels() const 57 | { return levels_; } 58 | 59 | void StepwiseMapping::init_fanspeed(const TemperatureState &ts) 60 | { 61 | cur_lvl_ = --levels().end(); 62 | while (cur_lvl_ != levels().begin() && (*cur_lvl_)->down(ts)) 63 | cur_lvl_--; 64 | fan()->set_speed(**cur_lvl_); 65 | } 66 | 67 | bool StepwiseMapping::set_fanspeed(const TemperatureState &ts) 68 | { 69 | if (unlikely(cur_lvl_ != --levels().end() && (*cur_lvl_)->up(ts))) { 70 | while (cur_lvl_ != --levels().end() && (*cur_lvl_)->up(ts)) 71 | cur_lvl_++; 72 | fan()->set_speed(**cur_lvl_); 73 | return true; 74 | } 75 | else if (unlikely(cur_lvl_ != levels().begin() && (*cur_lvl_)->down(ts))) { 76 | while (cur_lvl_ != levels().begin() && (*cur_lvl_)->down(ts)) 77 | cur_lvl_--; 78 | fan()->set_speed(**cur_lvl_); 79 | tmp_sleeptime = sleeptime; 80 | return true; 81 | } 82 | else { 83 | fan()->ping_watchdog_and_depulse(**cur_lvl_); 84 | return false; 85 | } 86 | } 87 | 88 | 89 | void StepwiseMapping::ensure_consistency(const Config &config) const 90 | { 91 | if (levels().size() == 0) 92 | throw ConfigError("No fan levels specified."); 93 | 94 | if (!fan()) 95 | throw ConfigError("No fan specified in stepwise mapping."); 96 | 97 | for (auto &lvl : levels()) 98 | lvl->ensure_consistency(config); 99 | 100 | int maxlvl = (*levels_.rbegin())->num(); 101 | if (dynamic_cast(fan().get()) && maxlvl < 128) 102 | error(MSG_CONF_MAXLVL((*levels_.rbegin())->num())); 103 | else if (dynamic_cast(fan().get()) 104 | && maxlvl != std::numeric_limits::max() 105 | && maxlvl > 7 106 | && maxlvl != 127) 107 | error(MSG_CONF_TP_LVL7(maxlvl, 7)); 108 | } 109 | 110 | 111 | void StepwiseMapping::add_level(unique_ptr &&level) 112 | { 113 | if (levels_.size() > 0) { 114 | const unique_ptr &last_lvl = levels_.back(); 115 | if (level->num() != std::numeric_limits::max() 116 | && level->num() != std::numeric_limits::min() 117 | && last_lvl->num() > level->num()) 118 | error(MSG_CONF_LVLORDER); 119 | 120 | if (last_lvl->upper_limit().size() != level->upper_limit().size()) 121 | error(MSG_CONF_LVLORDER); 122 | 123 | for (vector::const_iterator mit = last_lvl->upper_limit().begin(), oit = level->lower_limit().begin(); 124 | mit != last_lvl->upper_limit().end() && oit != level->lower_limit().end(); 125 | ++mit, ++oit) 126 | { 127 | if (*mit < *oit) error(MSG_CONF_OVERLAP); 128 | } 129 | } 130 | 131 | levels_.push_back(std::move(level)); 132 | } 133 | 134 | 135 | 136 | 137 | 138 | const Config *Config::read_config(const vector &filenames) 139 | { 140 | const Config *rv = nullptr; 141 | for (auto it = filenames.begin(); it != filenames.end(); ++it) { 142 | try { 143 | rv = try_read_config(*it); 144 | break; 145 | } catch (IOerror &e) { 146 | if (e.code() != ENOENT || it+1 >= filenames.end()) 147 | throw; 148 | } 149 | } 150 | 151 | return rv; 152 | } 153 | 154 | 155 | const Config *Config::try_read_config(const string &filename) 156 | { 157 | Config *rv = nullptr; 158 | 159 | ifstream f_in(filename); 160 | if (!(f_in.is_open() && f_in.good())) 161 | throw IOerror(filename + ": ", errno); 162 | if (!f_in.seekg(0, f_in.end)) 163 | throw IOerror(filename + ": ", errno); 164 | ifstream::pos_type f_size = f_in.tellg(); 165 | if (!f_in.seekg(0, f_in.beg)) 166 | throw IOerror(filename + ": ", errno); 167 | string f_data; 168 | f_data.resize(f_size, 0); 169 | if (!f_in.read(&*f_data.begin(), f_size)) 170 | throw IOerror(filename + ": ", errno); 171 | 172 | #ifdef USE_YAML 173 | try { 174 | YAML::Node root = YAML::Load(f_data); 175 | 176 | // Copy the return value first. Workaround for https://github.com/vmatare/thinkfan/issues/42 177 | // due to bug in ancient yaml-cpp: https://github.com/jbeder/yaml-cpp/commit/97d56c3f3608331baaee26e17d2f116d799a7edc 178 | try { 179 | YAML::wtf_ptr rv_tmp = root.as>(); 180 | rv = rv_tmp.release(); 181 | } catch (IOerror &e) { 182 | // An IOerror while processing the config means that an invalid sensor or fan path was specified. 183 | // That's a user error, wrap it and let it escalate. 184 | throw ConfigError(e.what()); 185 | } 186 | #if not defined(DISABLE_EXCEPTION_CATCHING) 187 | } catch(YamlError &e) { 188 | throw ConfigError(filename, e.mark, f_data, e.what()); 189 | } catch(YAML::RepresentationException &e) { 190 | throw ConfigError(filename, e.mark, f_data, "Invalid entry"); 191 | #endif 192 | } catch(YAML::ParserException &e) { 193 | string::size_type ext_off = filename.rfind('.'); 194 | if (ext_off != string::npos) { 195 | string ext = filename.substr(filename.rfind('.')); 196 | std::for_each(ext.begin(), ext.end(), [] (char &c) { 197 | c = std::toupper(c, std::locale()); 198 | } ); 199 | if (ext == ".YAML") 200 | throw ConfigError(filename, e.mark, f_data, e.what()); 201 | else { 202 | log(TF_INF) << "Config file " << filename << " could not be parsed as YAML." << flush; 203 | log(TF_DBG) << ConfigError(filename, e.mark, f_data, e.what()).what() << flush; 204 | log(TF_INF) << "Attempting to parse with legacy syntax because filename does not end in .yaml..." << flush; 205 | } 206 | } 207 | #endif //USE_YAML 208 | 209 | ConfigParser parser; 210 | 211 | const char *input = f_data.c_str(); 212 | const char *start = input; 213 | 214 | rv = parser.parse_config(input); 215 | 216 | if (!rv) { 217 | throw SyntaxError(filename, parser.get_max_addr() - start, f_data); 218 | } 219 | #ifdef USE_YAML 220 | } 221 | #endif //USE_YAML 222 | 223 | rv->src_file = filename; 224 | 225 | return rv; 226 | } 227 | 228 | 229 | void Config::ensure_consistency() const 230 | { 231 | // Consistency checks which require the complete config 232 | 233 | if (fan_configs().empty()) 234 | throw ConfigError("No fans are configured in " + src_file); 235 | 236 | for (const unique_ptr &fan_cfg : fan_configs()) 237 | try { 238 | fan_cfg->ensure_consistency(*this); 239 | } catch (ConfigError &err) { 240 | err.set_filename(src_file); 241 | throw; 242 | } 243 | 244 | if (sensors().size() < 1) 245 | throw ConfigError(src_file + ": " + MSG_NO_SENSOR); 246 | } 247 | 248 | 249 | 250 | void Config::add_sensor(unique_ptr &&sensor) 251 | { sensors_.push_back(std::move(sensor)); } 252 | 253 | 254 | unsigned int Config::num_temps() const 255 | { 256 | unsigned int count = 0; 257 | for (auto &sensor : sensors()) 258 | count += sensor->num_temps(); 259 | return count; 260 | } 261 | 262 | 263 | const vector> &Config::sensors() const 264 | { return sensors_; } 265 | 266 | const vector> &Config::fan_configs() const 267 | { return temp_mappings_; } 268 | 269 | void Config::add_fan_config(unique_ptr &&fan_cfg) 270 | { temp_mappings_.push_back(std::move(fan_cfg)); } 271 | 272 | 273 | void Config::init_fans() const 274 | { 275 | for (const unique_ptr &fan_cfg : fan_configs()) 276 | try_init_driver(*fan_cfg->fan()); 277 | } 278 | 279 | 280 | TemperatureState Config::init_sensors() const 281 | { 282 | for (const unique_ptr &sensor : sensors()) 283 | try_init_driver(*sensor); 284 | return TemperatureState(num_temps()); 285 | } 286 | 287 | 288 | void Config::init_temperature_refs(TemperatureState &tstate) const 289 | { 290 | tstate.reset_refd_count(); 291 | for (auto &sensor : sensors()) 292 | sensor->init_temp_state_ref(tstate.ref(sensor->num_temps())); 293 | } 294 | 295 | 296 | void Config::init(TemperatureState &ts) const 297 | { 298 | ts = init_sensors(); 299 | init_fans(); 300 | ensure_consistency(); 301 | init_temperature_refs(ts); 302 | } 303 | 304 | 305 | void Config::try_init_driver(Driver &drv) const 306 | { 307 | while (true) { 308 | drv.try_init(); 309 | if (drv.initialized() || drv.optional()) 310 | return; 311 | else 312 | sleep(sleeptime); 313 | } 314 | } 315 | 316 | 317 | 318 | Level::Level(int level, int lower_limit, int upper_limit) 319 | : Level(level, vector(1, lower_limit), vector(1, upper_limit)) 320 | {} 321 | 322 | Level::Level(string level, int lower_limit, int upper_limit) 323 | : Level(level, vector(1, lower_limit), vector(1, upper_limit)) 324 | {} 325 | 326 | Level::Level(int level, const vector &lower_limit, const vector &upper_limit) 327 | : level_s_("level " + std::to_string(level)), 328 | level_n_(level), 329 | lower_limit_(lower_limit), 330 | upper_limit_(upper_limit) 331 | {} 332 | 333 | Level::Level(string level, const vector &lower_limit, const vector &upper_limit) 334 | : level_s_(level), 335 | level_n_(string_to_int(level_s_)), 336 | lower_limit_(lower_limit), 337 | upper_limit_(upper_limit) 338 | { 339 | if (lower_limit.size() != upper_limit.size()) 340 | error(MSG_CONF_LIMITLEN); 341 | 342 | for (vector::const_iterator l_it = lower_limit.begin(), u_it = upper_limit.begin(); 343 | l_it != lower_limit.end() && u_it != upper_limit.end(); 344 | ++u_it, ++l_it) { 345 | if (*l_it != numeric_limits::max() && *l_it >= *u_it) 346 | error(MSG_CONF_LOWHIGH); 347 | } 348 | } 349 | 350 | int Level::string_to_int(string &level) { 351 | int rv; 352 | if (level == "level auto" || level == "level disengaged" || level == "level full-speed") 353 | return std::numeric_limits::min(); 354 | else if (sscanf(level.c_str(), "level %d", &rv) == 1) 355 | return rv; 356 | else try { 357 | rv = std::stoi(level); 358 | level = "level " + level; 359 | } catch (std::out_of_range &) { 360 | error(MSG_CONF_LVLFORMAT(level)); 361 | } catch (std::invalid_argument &) { 362 | error(MSG_CONF_LVLFORMAT(level)); 363 | } 364 | return rv; 365 | } 366 | 367 | const vector &Level::lower_limit() const 368 | { return lower_limit_; } 369 | 370 | const vector &Level::upper_limit() const 371 | { return upper_limit_; } 372 | 373 | const string &Level::str() const 374 | { return this->level_s_; } 375 | 376 | int Level::num() const 377 | { return this->level_n_; } 378 | 379 | 380 | 381 | SimpleLevel::SimpleLevel(int level, int lower_limit, int upper_limit) 382 | : Level(level, lower_limit, upper_limit) 383 | {} 384 | 385 | SimpleLevel::SimpleLevel(string level, int lower_limit, int upper_limit) 386 | : Level(level, lower_limit, upper_limit) 387 | {} 388 | 389 | bool SimpleLevel::up(const TemperatureState &temp_state) const 390 | { return *temp_state.tmax >= upper_limit().front(); } 391 | 392 | bool SimpleLevel::down(const TemperatureState &temp_state) const 393 | { return *temp_state.tmax < lower_limit().front(); } 394 | 395 | void SimpleLevel::ensure_consistency(const Config &) const 396 | {} 397 | 398 | 399 | 400 | ComplexLevel::ComplexLevel(int level, const vector &lower_limit, const vector &upper_limit) 401 | : Level(level, lower_limit, upper_limit) 402 | {} 403 | 404 | 405 | ComplexLevel::ComplexLevel(string level, const vector &lower_limit, const vector &upper_limit) 406 | : Level(level, lower_limit, upper_limit) 407 | {} 408 | 409 | 410 | bool ComplexLevel::up(const TemperatureState &temp_state) const 411 | { 412 | vector::const_iterator temp_it = temp_state.biased_temps().begin(); 413 | auto upper_it = upper_limit().begin(); 414 | 415 | while (temp_it != temp_state.biased_temps().end()) 416 | if (*temp_it++ >= *upper_it++) return true; 417 | 418 | return false; 419 | } 420 | 421 | 422 | bool ComplexLevel::down(const TemperatureState &temp_state) const 423 | { 424 | auto temp_it = temp_state.biased_temps().begin(); 425 | auto lower_it = lower_limit().begin(); 426 | 427 | while (temp_it != temp_state.biased_temps().end() && *temp_it < *lower_it) { 428 | temp_it++; 429 | lower_it++; 430 | } 431 | 432 | return temp_it == temp_state.biased_temps().end(); 433 | } 434 | 435 | 436 | void ComplexLevel::ensure_consistency(const Config &cfg) const 437 | { 438 | string limitstr; 439 | 440 | const string restmsg = " must have the length " 441 | + std::to_string(cfg.num_temps()) 442 | + " (one entry for each configured sensor)" 443 | ; 444 | 445 | if (lower_limit().size() != cfg.num_temps()) 446 | throw ConfigError( 447 | "Lower limit " 448 | + format_limit(lower_limit()) 449 | + restmsg 450 | ); 451 | 452 | if (upper_limit().size() != cfg.num_temps()) 453 | throw ConfigError( 454 | "Upper limit " 455 | + format_limit(upper_limit()) 456 | + restmsg 457 | ); 458 | } 459 | 460 | string ComplexLevel::format_limit(const vector &limit) 461 | { 462 | return "[" + std::accumulate( 463 | std::next(limit.begin()), 464 | limit.end(), 465 | std::to_string(limit.front()), 466 | [] (string l, int r) -> string { 467 | return std::move(l) + ", " + std::to_string(r); 468 | } 469 | ) + "]"; 470 | } 471 | 472 | 473 | } /* namespace thinkfan */ 474 | -------------------------------------------------------------------------------- /src/sensors.cpp: -------------------------------------------------------------------------------- 1 | /******************************************************************** 2 | * drivers.cpp: Interface to the kernel drivers. 3 | * (C) 2015, Victor Mataré 4 | * 5 | * this file is part of thinkfan. See thinkfan.c for further information. 6 | * 7 | * thinkfan is free software: you can redistribute it and/or modify 8 | * it under the terms of the GNU General Public License as published by 9 | * the Free Software Foundation, either version 3 of the License, or 10 | * (at your option) any later version. 11 | * 12 | * thinkfan is distributed in the hope that it will be useful, 13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | * GNU General Public License for more details. 16 | * 17 | * You should have received a copy of the GNU General Public License 18 | * along with thinkfan. If not, see . 19 | * 20 | * ******************************************************************/ 21 | 22 | #include "sensors.h" 23 | #include "error.h" 24 | #include "message.h" 25 | 26 | #include 27 | #include 28 | #include 29 | #include 30 | 31 | #ifdef USE_NVML 32 | #include 33 | #endif 34 | 35 | namespace thinkfan { 36 | 37 | 38 | /*---------------------------------------------------------------------------- 39 | | SensorDriver: The superclass of all hardware-specific sensor drivers | 40 | ----------------------------------------------------------------------------*/ 41 | 42 | SensorDriver::SensorDriver(bool optional, opt> correction, opt max_errors) 43 | : Driver(optional, max_errors.value_or(0)) 44 | , correction_(correction.value_or(vector())) 45 | , num_temps_(0) 46 | {} 47 | 48 | void SensorDriver::init() 49 | { readstream(path()); } 50 | 51 | SensorDriver::~SensorDriver() noexcept(false) 52 | {} 53 | 54 | 55 | inline int SensorDriver::readstream(const string &path) { 56 | std::ifstream f(path); 57 | if (!(f.is_open() && f.good())) 58 | throw IOerror(MSG_T_GET(path), errno); 59 | 60 | int tmp; 61 | if (!(f >> tmp)) 62 | throw IOerror(MSG_T_GET(path), errno); 63 | 64 | return tmp; 65 | } 66 | 67 | 68 | void SensorDriver::set_correction(const vector &correction) 69 | { 70 | correction_ = correction; 71 | check_correction_length(); 72 | } 73 | 74 | 75 | void SensorDriver::set_num_temps(unsigned int n) 76 | { 77 | num_temps_ = n; 78 | if (correction_.empty()) 79 | correction_.resize(num_temps(), 0); 80 | else 81 | check_correction_length(); 82 | } 83 | 84 | 85 | bool SensorDriver::operator == (const SensorDriver &other) const 86 | { 87 | return typeid(*this) == typeid(other) 88 | && this->correction_ == other.correction_ 89 | && this->path() == other.path(); 90 | } 91 | 92 | 93 | void SensorDriver::read_temps() 94 | { 95 | temp_state_.restart(); 96 | robust_io(&SensorDriver::read_temps_); 97 | } 98 | 99 | void SensorDriver::init_temp_state_ref(TemperatureState::Ref &&ref) 100 | { temp_state_ = std::move(ref); } 101 | 102 | 103 | void SensorDriver::check_correction_length() 104 | { 105 | if (correction_.size() > num_temps()) 106 | throw ConfigError(MSG_CONF_CORRECTION_LEN(path(), correction_.size(), num_temps())); 107 | else if (correction_.size() < num_temps_) 108 | log(TF_WRN) << MSG_CONF_CORRECTION_LEN(path(), correction_.size(), num_temps()) << flush; 109 | } 110 | 111 | 112 | void SensorDriver::skip_io_error(const ExpectedError &e) 113 | { 114 | if (this->optional()) { 115 | log(TF_INF) << DriverLost(e).what(); 116 | // Completely ignore sensor. optional says we're good without it 117 | temp_state_.add_temp(-128); 118 | } 119 | else if (tolerate_errors) { 120 | log(TF_NFY) << DriverLost(e).what(); 121 | // Read error on wakeup: keep last temp 122 | temp_state_.skip_temp(); 123 | } 124 | else { 125 | log(TF_NFY) << "Ignoring Error " << errors() << "/" << max_errors() 126 | << " on " << path() << ": " << e.what(); 127 | 128 | // Other error the user said is acceptable: keep last temp 129 | temp_state_.skip_temp(); 130 | } 131 | } 132 | 133 | 134 | 135 | /*---------------------------------------------------------------------------- 136 | | HwmonSensorDriver: A driver for sensors provided by other kernel drivers, | 137 | | typically somewhere in sysfs. | 138 | ----------------------------------------------------------------------------*/ 139 | 140 | HwmonSensorDriver::HwmonSensorDriver(const string &path, bool optional) 141 | : HwmonSensorDriver( 142 | std::make_shared>(path, nullopt, nullopt, nullopt), 143 | optional, 144 | nullopt, 145 | 0 146 | ) 147 | {} 148 | 149 | HwmonSensorDriver::HwmonSensorDriver( 150 | shared_ptr> hwmon_interface, 151 | bool optional, 152 | opt correction, 153 | opt max_errors 154 | ) 155 | : SensorDriver(optional, correction ? vector{*correction} : vector{}, max_errors) 156 | , hwmon_interface_(hwmon_interface) 157 | {} 158 | 159 | void HwmonSensorDriver::init() 160 | { 161 | SensorDriver::init(); 162 | set_num_temps(1); 163 | } 164 | 165 | void HwmonSensorDriver::read_temps_() 166 | { 167 | temp_state_.add_temp( 168 | readstream(path()) / 1000 + correction_[0] 169 | ); 170 | } 171 | 172 | 173 | 174 | string HwmonSensorDriver::lookup() 175 | { return hwmon_interface_->lookup(); } 176 | 177 | string HwmonSensorDriver::type_name() const 178 | { return "hwmon sensor driver"; } 179 | 180 | 181 | /*---------------------------------------------------------------------------- 182 | | TpSensorDriver: A driver for sensors provided by thinkpad_acpi, typically | 183 | | in /proc/acpi/ibm/thermal. | 184 | ----------------------------------------------------------------------------*/ 185 | 186 | const string TpSensorDriver::skip_prefix_("temperatures:"); 187 | 188 | TpSensorDriver::TpSensorDriver( 189 | string conf_path, 190 | bool optional, 191 | opt> temp_indices, 192 | opt> correction, 193 | opt max_errors 194 | ) 195 | : SensorDriver(optional, correction, max_errors) 196 | , temp_indices_(temp_indices) 197 | , conf_path_(conf_path) 198 | { 199 | if (temp_indices_) 200 | set_num_temps(static_cast(temp_indices_->size())); 201 | } 202 | 203 | 204 | void TpSensorDriver::init() 205 | { 206 | int tmp; 207 | unsigned int count = 0; 208 | 209 | string skip; 210 | skip.resize(skip_prefix_.size()); 211 | 212 | std::ifstream f(path()); 213 | if (!(f.is_open() && f.good())) 214 | throw IOerror(MSG_SENSOR_INIT(path()), errno); 215 | 216 | if (!f.get(&*skip.begin(), static_cast(skip_prefix_.size() + 1))) 217 | throw IOerror(MSG_SENSOR_INIT(path()), errno); 218 | 219 | if (skip == skip_prefix_) 220 | skip_bytes_ = f.tellg(); 221 | else 222 | throw SystemError(path() + ": Unknown file format."); 223 | 224 | while (!(f.eof() || f.fail())) { 225 | f >> tmp; 226 | if (f.bad()) 227 | throw IOerror(MSG_T_GET(path()), errno); 228 | if (!f.fail()) 229 | ++count; 230 | } 231 | 232 | if (temp_indices_) { 233 | if (temp_indices_->size() > count) 234 | throw ConfigError( 235 | "Config specifies " + std::to_string(temp_indices_->size()) 236 | + " temperature inputs in " + path() 237 | + ", but there are only " + std::to_string(count) + "." 238 | ); 239 | 240 | 241 | in_use_ = vector(count, false); 242 | for (unsigned int i : *temp_indices_) 243 | in_use_[i] = true; 244 | } 245 | else { 246 | in_use_ = vector(count, true); 247 | set_num_temps(count); 248 | } 249 | } 250 | 251 | 252 | void TpSensorDriver::read_temps_() 253 | { 254 | std::ifstream f(path()); 255 | if (!(f.is_open() && f.good())) 256 | throw IOerror(MSG_T_GET(path()), errno); 257 | 258 | f.seekg(skip_bytes_); 259 | if (f.fail()) 260 | throw IOerror(MSG_T_GET(path()), errno); 261 | 262 | unsigned int tidx = 0; 263 | unsigned int cidx = 0; 264 | int tmp; 265 | while (!(f.eof() || f.fail())) { 266 | f >> tmp; 267 | if (f.bad()) 268 | throw IOerror(MSG_T_GET(path()), errno); 269 | if (!f.fail() && in_use_[tidx++]) 270 | temp_state_.add_temp(tmp + correction_[cidx++]); 271 | } 272 | } 273 | 274 | 275 | string TpSensorDriver::lookup() 276 | { 277 | std::ifstream f(conf_path_); 278 | if (f.is_open() && f.good()) 279 | return conf_path_; 280 | else 281 | throw IOerror(MSG_SENSOR_INIT(path()), errno); 282 | } 283 | 284 | string TpSensorDriver::type_name() const 285 | { return "tpacpi sensor driver"; } 286 | 287 | 288 | #ifdef USE_ATASMART 289 | /*---------------------------------------------------------------------------- 290 | | AtssmartSensorDriver: Reads temperatures from hard disks using S.M.A.R.T. | 291 | | via device files like /dev/sda. | 292 | ----------------------------------------------------------------------------*/ 293 | 294 | AtasmartSensorDriver::AtasmartSensorDriver( 295 | string device_path, 296 | bool optional, 297 | opt> correction, 298 | opt max_errors 299 | ) 300 | : SensorDriver(optional, correction, max_errors) 301 | , device_path_(device_path) 302 | { 303 | set_num_temps(1); 304 | } 305 | 306 | void AtasmartSensorDriver::init() 307 | { 308 | if (sk_disk_open(path().c_str(), &disk_) < 0) { 309 | string msg = std::strerror(errno); 310 | throw SystemError("sk_disk_open(" + path() + "): " + msg); 311 | } 312 | } 313 | 314 | 315 | AtasmartSensorDriver::~AtasmartSensorDriver() 316 | { sk_disk_free(disk_); } 317 | 318 | 319 | void AtasmartSensorDriver::read_temps_() 320 | { 321 | SkBool disk_sleeping = false; 322 | 323 | if (unlikely(dnd_disk && (sk_disk_check_sleep_mode(disk_, &disk_sleeping) < 0))) { 324 | string msg = strerror(errno); 325 | throw SystemError("sk_disk_check_sleep_mode(" + path() + "): " + msg); 326 | } 327 | 328 | if (unlikely(disk_sleeping)) { 329 | temp_state_.add_temp(0); 330 | } 331 | else { 332 | uint64_t mKelvin; 333 | float tmp; 334 | 335 | if (unlikely(sk_disk_smart_read_data(disk_) < 0)) { 336 | string msg = strerror(errno); 337 | throw SystemError("sk_disk_smart_read_data(" + path() + "): " + msg); 338 | } 339 | if (unlikely(sk_disk_smart_get_temperature(disk_, &mKelvin)) < 0) { 340 | string msg = strerror(errno); 341 | throw SystemError("sk_disk_smart_get_temperature(" + path() + "): " + msg); 342 | } 343 | 344 | tmp = mKelvin / 1000.0f; 345 | tmp -= 273.15f; 346 | 347 | if (unlikely(tmp > std::floor(numeric_limits::max()) || tmp < std::numeric_limits::min())) { 348 | throw SystemError(MSG_T_GET(path()) + std::to_string(tmp) + " isn't a valid temperature."); 349 | } 350 | 351 | temp_state_.add_temp(int(tmp) + correction_[0]); 352 | } 353 | } 354 | 355 | string AtasmartSensorDriver::lookup() 356 | { return device_path_; } 357 | 358 | string AtasmartSensorDriver::type_name() const 359 | { return "atasmart sensor driver"; } 360 | 361 | #endif /* USE_ATASMART */ 362 | 363 | 364 | #ifdef USE_NVML 365 | /*---------------------------------------------------------------------------- 366 | | NvmlSensorDriver: Gets temperatures directly from GPUs supported by the | 367 | | nVidia Management Library that is included with the proprietary driver. | 368 | ----------------------------------------------------------------------------*/ 369 | 370 | NvmlSensorDriver::NvmlSensorDriver(string bus_id, bool optional, opt> correction, opt max_errors) 371 | : SensorDriver(optional, correction, max_errors), 372 | bus_id_(bus_id), 373 | dl_nvmlInit_v2(nullptr), 374 | dl_nvmlDeviceGetHandleByPciBusId_v2(nullptr), 375 | dl_nvmlDeviceGetName(nullptr), 376 | dl_nvmlDeviceGetTemperature(nullptr), 377 | dl_nvmlShutdown(nullptr) 378 | { 379 | if (!(nvml_so_handle_ = dlopen("libnvidia-ml.so.1", RTLD_LAZY))) { 380 | string msg = strerror(errno); 381 | throw SystemError("Failed to load libnvidia-ml.so.1: " + msg); 382 | } 383 | 384 | /* Apparently GCC doesn't want to cast to function pointers, so we have to do 385 | * this kind of weird stuff. 386 | * See http://stackoverflow.com/questions/1096341/function-pointers-casting-in-c 387 | */ 388 | *reinterpret_cast(&dl_nvmlInit_v2) = dlsym(nvml_so_handle_, "nvmlInit_v2"); 389 | *reinterpret_cast(&dl_nvmlDeviceGetHandleByPciBusId_v2) = dlsym( 390 | nvml_so_handle_, "nvmlDeviceGetHandleByPciBusId_v2"); 391 | *reinterpret_cast(&dl_nvmlDeviceGetName) = dlsym(nvml_so_handle_, "nvmlDeviceGetName"); 392 | *reinterpret_cast(&dl_nvmlDeviceGetTemperature) = dlsym(nvml_so_handle_, "nvmlDeviceGetTemperature"); 393 | *reinterpret_cast(&dl_nvmlShutdown) = dlsym(nvml_so_handle_, "nvmlShutdown"); 394 | 395 | if (!(dl_nvmlDeviceGetHandleByPciBusId_v2 && dl_nvmlDeviceGetName && 396 | dl_nvmlDeviceGetTemperature && dl_nvmlInit_v2 && dl_nvmlShutdown)) 397 | throw SystemError("Incompatible NVML driver."); 398 | set_num_temps(1); 399 | } 400 | 401 | 402 | void NvmlSensorDriver::init() 403 | { 404 | nvmlReturn_t ret; 405 | string name, brand; 406 | name.resize(256); 407 | brand.resize(256); 408 | 409 | if ((ret = dl_nvmlInit_v2())) 410 | throw SystemError("Failed to initialize NVML driver. Error code (cf. nvml.h): " + std::to_string(ret)); 411 | if ((ret = dl_nvmlDeviceGetHandleByPciBusId_v2(path().c_str(), &device_))) 412 | throw SystemError("Failed to open PCI device " + path() + ". Error code (cf. nvml.h): " + std::to_string(ret)); 413 | dl_nvmlDeviceGetName(device_, &*name.begin(), 255); 414 | log(TF_DBG) << "Initialized NVML sensor on " << name << " at PCI " << path() << "." << flush; 415 | } 416 | 417 | 418 | NvmlSensorDriver::~NvmlSensorDriver() noexcept(false) 419 | { 420 | nvmlReturn_t ret; 421 | if ((ret = dl_nvmlShutdown())) 422 | log(TF_ERR) << "Failed to shutdown NVML driver. Error code (cf. nvml.h): " << std::to_string(ret); 423 | dlclose(nvml_so_handle_); 424 | } 425 | 426 | 427 | void NvmlSensorDriver::read_temps_() 428 | { 429 | nvmlReturn_t ret; 430 | unsigned int tmp; 431 | if ((ret = dl_nvmlDeviceGetTemperature(device_, NVML_TEMPERATURE_GPU, &tmp))) 432 | throw SystemError(MSG_T_GET(path()) + "Error code (cf. nvml.h): " + std::to_string(ret)); 433 | temp_state_.add_temp(int(tmp)); 434 | } 435 | 436 | string NvmlSensorDriver::lookup() 437 | { return bus_id_; } 438 | 439 | string NvmlSensorDriver::type_name() const 440 | { return "NVML sensor driver"; } 441 | 442 | #endif /* USE_NVML */ 443 | 444 | 445 | #ifdef USE_LM_SENSORS 446 | 447 | /*---------------------------------------------------------------------------- 448 | | LMSensorsDriver: Driver for sensors provided by LM sensors's `libsensors`. | 449 | ----------------------------------------------------------------------------*/ 450 | 451 | 452 | 453 | LMSensorsDriver::LMSensorsDriver( 454 | string chip_name, 455 | vector feature_names, 456 | bool optional, 457 | opt> correction, 458 | opt max_errors 459 | ) 460 | : SensorDriver(optional, correction, max_errors), 461 | chip_name_(chip_name), 462 | feature_names_(feature_names) 463 | { 464 | set_num_temps(feature_names_.size()); 465 | } 466 | 467 | 468 | LMSensorsDriver::~LMSensorsDriver() 469 | {} 470 | 471 | const string &LMSensorsDriver::chip_name() const 472 | { return chip_name_; } 473 | 474 | const vector &LMSensorsDriver::feature_names() const 475 | { return feature_names_; } 476 | 477 | void LMSensorsDriver::init() 478 | { 479 | if (correction_.empty()) 480 | correction_.resize(feature_names_.size(), 0); 481 | } 482 | 483 | // This could be solved more elegantly by making available() virtual, but that would 484 | // cost us an additional vtable lookup on every read_temps(), so we choose to manipulate 485 | // the state here. 486 | void LMSensorsDriver::set_unavailable() 487 | { path_.reset(); } 488 | 489 | 490 | string LMSensorsDriver::lookup() 491 | { 492 | if (!libsensors_iface_) 493 | libsensors_iface_ = LibsensorsInterface::instance(); 494 | 495 | // If a sensor is not found, uninit() is called on ALL OTHER LMSensorsDrivers 496 | // instances and an exception is thrown. 497 | return libsensors_iface_->lookup_client_features(this); 498 | } 499 | 500 | 501 | string LMSensorsDriver::type_name() const 502 | { return "libsensors sensor driver"; } 503 | 504 | 505 | void LMSensorsDriver::read_temps_() 506 | { 507 | size_t index = 0; 508 | for (double real_value : libsensors_iface_->get_temps(this)) 509 | temp_state_.add_temp( 510 | int(real_value) + correction_[index++] 511 | ); 512 | } 513 | 514 | 515 | #endif /* USE_LM_SENSORS */ 516 | 517 | 518 | } 519 | -------------------------------------------------------------------------------- /src/thinkfan.conf.5.cmake: -------------------------------------------------------------------------------- 1 | .TH THINKFAN.CONF 5 "November 2022" "thinkfan @THINKFAN_VERSION@" 2 | .SH NAME 3 | thinkfan.conf \- YAML-formatted config for 4 | .BR thinkfan (1) 5 | 6 | 7 | 8 | .SH DESCRIPTION 9 | 10 | YAML is a very powerful, yet concise notation for structured data. 11 | Its full specification is available at https://yaml.org/spec/1.2/spec.html. 12 | Thinkfan uses only a small subset of the full YAML syntax, so it may be helpful, 13 | but not strictly necessary for users to take a look at the spec. 14 | 15 | The most important thing to note is that indentation is syntactically relevant. 16 | In particular, tabs should not be mixed with spaces. 17 | We recommend using two spaces for indentation, like it is shown below. 18 | 19 | The thinkfan config has three main sections: 20 | 21 | .TP 22 | .B sensors: 23 | Where temperatures should be read from. All 24 | .BR hwmon -style 25 | drivers are supported, as well as 26 | .BR /proc/acpi/ibm/thermal , 27 | and, depending on the compile-time options, 28 | .B libatasmart 29 | (to read temperatures directly from hard disks) and 30 | .B NVML 31 | (via the proprietary nvidia driver). 32 | 33 | .TP 34 | .B fans: 35 | Which fans should be used (currently only one allowed). 36 | Support for multiple fans is currently in development and planned for a future 37 | release. 38 | Both 39 | .BR hwmon -style 40 | PWM controls and 41 | .B /proc/acpi/ibm/fan 42 | can be used. 43 | 44 | .TP 45 | .B levels: 46 | Maps temperatures to fan speeds. 47 | A \*(lqsimple mapping\*(rq just specifies one temperature as the lower and 48 | upper bound (respectively) for a given fan speed. 49 | In a \*(lqdetailed mapping\*(rq, the upper and lower bounds are specified for 50 | each driver/sensor configured under 51 | .BR sensors: . 52 | This mode should be used when thinkfan is monitoring multiple devices that can 53 | tolerate different amounts of heat. 54 | 55 | .PP 56 | Under each of these sections, there must be a list of key-value maps, each of 57 | which configures a sensor driver, fan driver or fan speed mapping. 58 | 59 | 60 | .SH SENSOR & FAN DRIVERS 61 | 62 | For thinkfan to work, it first needs to know which temperature sensor drivers 63 | and which fan drivers it should use. 64 | The mapping between temperature readings and fan speeds is specified in a 65 | separate config section (see the 66 | .B FAN SPEEDS 67 | section below). 68 | 69 | .SS Sensor Syntax 70 | 71 | The entries under the 72 | .B sensors: 73 | section can specify sysfs/hwmon, lm_sensors, thinkpad_acpi, NVML or atasmart drivers. 74 | Support for lm_sensors, NVML and atasmart requires the appropriate libraries 75 | and must have been enabled at compile time. 76 | There can be any number (greater than zero) and combination of 77 | .BR hwmon , 78 | .BR tpacpi , 79 | .BR nvml 80 | and 81 | .BR atasmart 82 | entries. 83 | However there may be at most one instance of the 84 | .BR tpacpi 85 | entry. 86 | 87 | The syntax for identifying each type of sensors looks as follows: 88 | 89 | .nf 90 | \f[C] 91 | ######## 92 | \f[CB]sensors: 93 | \f[CB] \- hwmon: \f[CI]hwmon-path\f[CR] # A path to a sysfs/hwmon sensor 94 | \f[CB] name: \f[CI]hwmon-name\f[CR] # Optional entry 95 | \f[CB] model: \f[CI]hwmon-model\f[CR] # Optional entry for nvme 96 | \f[CB] indices: \f[CI]index-list\f[CR] # Optional entry 97 | 98 | \f[CB] \- chip: \f[CI]chip-name\f[CR] # An lm_sensors/libsensors chip... 99 | \f[CB] ids: \f[CI]id-list\f[CR] # ... with some feature IDs 100 | 101 | \f[CB] \- tpacpi: /proc/acpi/ibm/thermal\f[CR] # Provided by the thinkpad_acpi kernel module 102 | \f[CB] indices: \f[CI]index-list\f[CR] # Optional entry 103 | 104 | \f[CB] \- nvml: \f[CI]nvml-bus-id\f[CR] # Uses the proprietary nVidia driver 105 | 106 | \f[CB] \- atasmart: \f[CI]disk-device-file\f[CR] # Requires libatasmart support 107 | 108 | \f[CB] \- \f[CR]... 109 | \fR 110 | .fi 111 | 112 | Additionally, each sensor entry can have a number of settings that modify its 113 | behavior: 114 | 115 | .nf 116 | \fC 117 | ######## 118 | \f[CB]sensors: 119 | \f[CB] \- \f[CR]...\f[CB]: \f[CR]... # A sensor specification as shown above 120 | \f[CB] correction: \f[CI]correction-list\f[CR] # Optional entry 121 | \f[CB] optional: \f[CI]bool-ignore-errors\f[CR] # Optional entry 122 | \f[CB] max_errors: \f[CI]num-max-errors\f[CR] # Optional entry 123 | \fR 124 | .fi 125 | 126 | 127 | .SS Fan Syntax 128 | 129 | Since version 2.0, thinkfan can control multiple fans. 130 | So any number of 131 | .B hwmon 132 | fan sections can be specified. 133 | Note however that the thinkpad_acpi kernel module only supports one fan, so 134 | there can be at most one 135 | .B tpacpi 136 | section: 137 | 138 | .nf 139 | \fC 140 | # ... 141 | \f[CB]fans: 142 | \f[CB] \- tpacpi: /proc/acpi/ibm/fan 143 | 144 | \f[CB] \- hwmon: \f[CI]hwmon-path 145 | \f[CB] name: \f[CI]hwmon-name 146 | \f[CB] indices: \f[CI]index-list 147 | 148 | \f[CB] \- \f[CR]... 149 | \fR 150 | .fi 151 | 152 | The behavior of any fan can optionally be controlled with the \fBoptional\fR 153 | and \fBmax_errors\fR keywords, and by specifying a local fan speed config 154 | under the \fBlevels:\fR keyword: 155 | 156 | .nf 157 | \fC 158 | # ... 159 | \f[CB]fans: 160 | \f[CB] \- \f[CR]...\f[CB]: \f[CR] ... # A fan specification as shown above 161 | \f[CB] optional: \f[CI]bool-ignore-errors\f[CR] # Optional entry 162 | \f[CB] max_errors: \f[CI]num-max-errors\f[CR] # Optional entry 163 | \f[CB] levels: \f[CI]levels-section\f[CR] # Optional entry 164 | 165 | 166 | .SS Values 167 | 168 | .TP 169 | .I hwmon-path 170 | There are three ways of specifying hwmon fans or sensors: 171 | 172 | .RS 173 | .IP 1) 3 174 | A full path of a \*(lqtemp*_input\*(rq or \*(lqpwm*\*(rq file, like 175 | \*(lq/sys/class/hwmon/hwmon0/pwm1\*(rq or 176 | \*(lq/sys/class/hwmon/hwmon0/temp1_input\*(rq. 177 | In this case, the \*(lq\c 178 | .BI indices: " index-list"\c 179 | \*(rq and \*(lq\c 180 | .BI name: " hwmon-name"\c 181 | \*(rq entries are unnecessary since the path uniquely identifies a specific fan or 182 | sensor. 183 | 184 | .RS 185 | Note that this method may lead to problems when the load order of the drivers 186 | changes across bootups, because in the \*(lqhwmon\fIX\fR\*(rq folder name, the 187 | .I X 188 | actually corresponds to the load order. 189 | Use method 2) or 3) to avoid this problem. 190 | .RE 191 | 192 | .IP 2) 3 193 | A directory that contains a specific hwmon driver, for example 194 | \*(lq/sys/devices/platform/nct6775.2592\*(rq. 195 | Note that this path does not contain the load-order dependent 196 | \*(lqhwmon\fIX\fR\*(rq folder. 197 | As long as it contains only a single hwmon driver/interface it is sufficient to 198 | specify the 199 | \*(lq\c 200 | .BI indices: " index-list"\c 201 | \*(rq 202 | entry to tell thinkfan which specific sensors to use from that interface. 203 | The 204 | \*(lq\c 205 | .BI name: " hwmon-name"\c 206 | \*(rq 207 | entry is unnecessary. 208 | 209 | .P 210 | .IP 3) 3 211 | A directory that contains multiple or all of the hwmon drivers, for example 212 | \*(lq/sys/class/hwmon\*(rq. 213 | Here, both the \*(lq\c 214 | .BI name: " hwmon-name"\c 215 | \*(rq and \*(lq\c 216 | .BI indices: " index-list"\c 217 | \*(rq entries are required to tell thinkfan which interface to select below that 218 | path, and which sensors or which fan to use from that interface. 219 | 220 | .RE 221 | .TP 222 | .I hwmon-name 223 | The name of a hwmon interface, typically found in a file called \*(lqname\*(rq. 224 | This has to be specified if 225 | .I hwmon-path 226 | is a base path that contains multiple hwmons. 227 | This method of specifying sensors is particularly useful if the full path to a 228 | particular hwmon keeps changing between bootups, e.g. due to changing load order 229 | of the driver modules. 230 | 231 | .TP 232 | .I hwmon-model 233 | The model of a device in a hwmon interface usually found for NVME devices in a 234 | file under \*(lqdevice\*(rq called \*(lqmodel\*(rq. 235 | For example, you might have an NVME \*(lq/sys/class/hwmon/hwmon3/device/model\*(rq 236 | and you might have an external NVME over USB or Thunderbolt that you don't want 237 | to monitor or you might have two NVME's. 238 | 239 | .TP 240 | .I index-list 241 | A YAML list 242 | .BI "[ " X1 ", " X2 ", " "\fR...\fB ]" 243 | that specifies which sensors, resp. which fan to use from a given 244 | interface. 245 | Both 246 | .B /proc/acpi/ibm/thermal 247 | and also many hwmon interfaces contain multiple sensors, and not 248 | all of them may be relevant for fan control. 249 | 250 | .RS 251 | .IP \(bu 2 252 | For 253 | .B hwmon 254 | entries, this is required if 255 | .I hwmon-path 256 | does not refer directly to a single \*(lqtemp\fIXi\fR_input\*(rq file, but to a folder 257 | that contains one or more of them. 258 | In this case, 259 | .I index-list 260 | specifies the 261 | .I Xi 262 | for the \*(lqtemp\fIXi\fR_input\*(rq files that should be used. 263 | A hwmon interface may also contain multiple PWM controls for fans, so in that case, 264 | .I index-list 265 | must contain exactly one entry. 266 | 267 | .IP \(bu 268 | For 269 | .B tpacpi 270 | sensors, this entry is optional. 271 | If it is omitted, all temperatures found in 272 | .B /proc/acpi/ibm/thermal 273 | will be used. 274 | .RE 275 | 276 | .TP 277 | .I chip-name 278 | The unique name of an lm-sensors hwmon chip. lm-sensors or libsensors is the 279 | official client library to access the sysfs hwmon subsystem, and as such it is 280 | the preferred way of specifying sensors. 281 | Available chips can be listed by running the \fBsensors\fR command as root. 282 | The header above each block will be the chip name. 283 | 284 | .TP 285 | .I ids 286 | A list of lm-sensors feature IDs, for example \*(lq\fB[ SYSTIN, CPUTIN, "Sensor 2" 287 | ]\fR\*(rq. 288 | In the \fBsensors\fR command output, the name before the colon on each line 289 | will be the feature ID. 290 | 291 | .TP 292 | .I nvml-bus-id 293 | NOTE: only available if thinkfan was compiled with USE_NVML enabled. 294 | 295 | The PCI bus ID of an nVidia graphics card that is run with the proprietary 296 | nVidia driver. Can be obtained with e.g. \*(lqlspci | grep \-i vga\*(rq. 297 | Usually, nVidia cards will use the open source 298 | .B nouveau 299 | driver, which should support hwmon sensors instead. 300 | 301 | .TP 302 | .I disk-device-file 303 | NOTE: only available if thinkfan was compiled with USE_ATASMART enabled. 304 | 305 | Full path to a device file for a hard disk that supports S.M.A.R.T. 306 | See also the 307 | .B \-d 308 | option in 309 | .BR thinkfan (1) 310 | that prevents thinkfan from waking up sleeping (mechanical) disks to read their 311 | temperature. 312 | 313 | .TP 314 | .IR correction-list " (optional, zeroes by default)" 315 | A YAML list that specifies temperature offsets for each sensor in use by the 316 | given driver. Use this if you want to use the \*(lqsimple\*(rq level syntax, 317 | but need to compensate for devices with a lower heat tolerance. 318 | Note however that the detailed level syntax is usually the better (i.e. more 319 | fine-grained) choice. 320 | 321 | .TP 322 | .IR bool-ignore-errors " (optional, \fBfalse\fR by default)" 323 | A truth value 324 | .RB ( yes / no / true / false ) 325 | that specifies whether thinkfan should ignore all errors when using a given 326 | sensor or fan. 327 | Normally, thinkfan will exit with an error message if it fails reading 328 | temperatures from a sensor or writing to a fan. 329 | 330 | An optional device will not delay startup. 331 | Instead, thinkfan will commence normal operation with the remaining devices and 332 | re-try initializing unavailable optional devices in every loop until all are 333 | found. 334 | 335 | Marking a sensor/fan as optional may be useful for removable hardware or devices 336 | that may get switched off entirely to save power. 337 | 338 | .TP 339 | .IR num-max-errors " (optional, \fB0\fR by default)" 340 | A positive integer that specifies how often thinkfan is allowed to re-try if 341 | if fails to initialize, read from or write to a given sensor or fan. 342 | On startup, thinkfan will attempt to initialize the device \fInum-max-errors\fR 343 | times before commencing normal operation. 344 | If the device cannot be initialized after the given number of attempts, 345 | thinkfan will fail. 346 | 347 | When a device with a positive \fInum-max-errors\fR fails during runtime, 348 | thinkfan will likewise attempt to re-initialize it the given number of times 349 | before failing. 350 | 351 | .TP 352 | .IR levels-section " (optional, use global levels section by default)" 353 | As of thinkfan 2.0, multiple fans can be configured. 354 | To this end, each fan can now have its own \fBlevels:\fR section (cf. FAN 355 | SPEEDS below). 356 | The syntax of the global vs. the fan-specific \fBlevels:\fR section are 357 | identical, the only difference being that in a fan-specific one, the speed 358 | value(s) refer only the fans configured in that particular \fBfans:\fR entry. 359 | 360 | NOTE: Global and fan-specific \fBlevels:\fR are mutually exclusive, i.e. 361 | there cannot be both a global one and fan-specific sections. 362 | 363 | 364 | .SH FAN SPEEDS 365 | 366 | The 367 | .B levels: 368 | section specifies a list of fan speeds with associated lower and upper 369 | temperature bounds. 370 | If temperature(s) drop below the lower bound, thinkfan switches to the previous 371 | level, and if the upper bound is reached, thinkfan switches to the next level. 372 | 373 | Since thinkfan 2.0, this section can appear either under an individual fan 374 | (cf. \fBlevels:\fR keyword under \fBFan Syntax\fR), or globally. 375 | 376 | .SS Simple Syntax 377 | In the simplified form, only one temperature is specified as an upper/lower 378 | limit for a given fan speed. 379 | In that case, the 380 | .I lower-bound 381 | and 382 | .I upper-bound 383 | are compared only to the highest temperature found among all configured sensors. 384 | All other temperatures are ignored. 385 | This mode is suitable for small systems (like laptops) where there is only one 386 | device (e.g. the CPU) whose temperature needs to be controlled, or where the 387 | required fan behaviour is similar enough for all heat-generating devices. 388 | 389 | .nf 390 | \fC 391 | # ... 392 | \f[CB]levels: 393 | \f[CB] \- [ \f[CI]fan-speed\f[CB], \f[CI]lower-bound\f[CB], \f[CI]upper-bound\f[CB] ] 394 | \f[CB] \- \f[CR]... 395 | \fR 396 | .fi 397 | 398 | 399 | .SS Detailed Syntax 400 | This mode is suitable for more complex systems, with devices that have 401 | different temperature ratings. 402 | For example, many modern CPUs and GPUs can deal with temperatures above 403 | 80\[char176]C on a daily basis, whereas a hard disk will die quickly if it 404 | reaches such temperatures. 405 | In detailed mode, upper and lower temperature limits are specified for each 406 | sensor individually: 407 | 408 | .nf 409 | \fC 410 | # ... 411 | \f[CB]levels: 412 | \f[CB] \- speed: [ \f[CI]fan1-speed\f[CB], \f[CI]fan2-speed\f[CB], \f[CR]...\f[CB] ] 413 | \f[CB] lower_limit: [ \f[CI]l1\f[CB], \f[CI]l2\f[CB], \f[CR]...\f[CB] ] 414 | \f[CB] upper_limit: [ \f[CI]u1\f[CB], \f[CI]u2\f[CB], \f[CR]...\f[CB] ] 415 | \f[CB] \- \f[CR]...\f[CB] 416 | \fR 417 | .fi 418 | 419 | 420 | .SS Values 421 | 422 | .TP 423 | .IB fan1-speed ", " fan2-speed ", " \fR... 424 | .TP 425 | .I fan-speed 426 | When multiple fans are specified under the 427 | .B fans: 428 | section, value of the 429 | .B speed: 430 | keyword must be a list of as many 431 | .I fanX-speed 432 | values. 433 | They are applied to the fans by their order of appearance, i.e. the first 434 | speed value applies to the fan that has been specified first, the second value 435 | to the second fan, and so on. 436 | If there is just one fan, instead of a list with just one element, the speed 437 | value can be given as a scalar. 438 | 439 | The possible speed values for 440 | .B fanX-speed 441 | are different depending on which fan driver is used: 442 | 443 | .RS 444 | .IP \(bu 3 445 | For a 446 | .B hwmon 447 | fan, 448 | .I fanX-speed 449 | is a numeric value ranging from 450 | .B 0 451 | to 452 | .BR 255 , 453 | corresponding to the PWM values accepted by the various kernel drivers. 454 | 455 | .IP \(bu 456 | For a 457 | .B tpacpi 458 | fan on Lenovo/IBM ThinkPads and some other Lenovo laptops (see \fBSENSORS & FAN 459 | DRIVERS\fR above), numeric values and strings can be used. 460 | The numeric values range from 0 to 7. 461 | The string values take the form \fB"level \fIlvl-id\fB"\fR, where 462 | .I lvl-id 463 | may be a value from 464 | .BR 0 " to " 7 , 465 | .BR auto , 466 | .B full-speed 467 | or 468 | .BR disengaged . 469 | The numeric values 470 | .BR 0 " to " 7 471 | correspond to the regular fan speeds used by the firmware, although many 472 | firmwares don't even use level \fB7\fR. 473 | The value \fB"level auto"\fR gives control back to the firmware, which may be 474 | useful if the fan behavior only needs to be changed for certain specific 475 | temperature ranges (usually at the high and low end of the range). 476 | The values \fB"level full-speed"\fR and \fB"level disengaged"\fR take the fan 477 | speed control away from the firmware, causing the fan to slowly ramp up to an 478 | absolute maximum that can be achieved within electrical limits. 479 | Note that this will run the fan out of specification and cause increased wear, 480 | though it may be helpful to combat thermal throttling. 481 | .RE 482 | 483 | .TP 484 | .IB l1 ", " l2 ", " \fR... 485 | .TP 486 | .IB u1 ", " u2 ", " \fR... 487 | The lower and upper temperature limits refer to the sensors in the same order 488 | in which they were found when processing the 489 | .B sensors: 490 | section (see 491 | .B SENSOR & FAN DRIVERS 492 | above). 493 | For the first level entry, the 494 | .B lower_limit 495 | may be omitted, and for the last one, the 496 | .B upper_limit 497 | may be omitted. 498 | For all levels in between, the lower limits must overlap with the upper limits 499 | of the previous level, to make sure the entire temperature range is covered and 500 | that there is some hysteresis between speed levels. 501 | 502 | Instead of a temperature, an underscore (\fB_\fR) can be given. 503 | An underscore means that the temperature of that sensor should be ignored at 504 | the given speed level. 505 | 506 | 507 | .SH SEE ALSO 508 | The thinkfan manpage: 509 | .BR thinkfan (1) 510 | 511 | .nf 512 | Example configs shipped with the source distribution, also available at: 513 | .UR https://github.com/vmatare/thinkfan/tree/master/examples 514 | .UE 515 | .fi 516 | 517 | .nf 518 | The Linux hwmon user interface documentation: 519 | .UR https://www.kernel.org/doc/html/latest/hwmon/sysfs\-interface.html 520 | .UE 521 | .fi 522 | 523 | .nf 524 | The thinkpad_acpi interface documentation: 525 | .UR https://www.kernel.org/doc/html/latest/admin\-guide/laptops/thinkpad\-acpi.html 526 | .UE 527 | .fi 528 | 529 | .SH BUGS 530 | 531 | .nf 532 | Report bugs on the github issue tracker: 533 | .UR https://github.com/vmatare/thinkfan/issues 534 | .UE 535 | .fi 536 | 537 | --------------------------------------------------------------------------------