pax_global_header00006660000000000000000000000064145672474250014532gustar00rootroot0000000000000052 comment=1e150b6e3e06869271f9fa567da85e0c5a40e93b zcfan-1.3.0/000077500000000000000000000000001456724742500126345ustar00rootroot00000000000000zcfan-1.3.0/.clang-format000066400000000000000000000003571456724742500152140ustar00rootroot00000000000000BreakStringLiterals: false ColumnLimit: 80 IndentWidth: 4 PenaltyReturnTypeOnItsOwnLine: 999 PenaltyBreakComment: 0 SortIncludes: true UseTab: Never IndentPPDirectives: BeforeHash IndentCaseLabels: true AllowShortBlocksOnASingleLine: true zcfan-1.3.0/.github/000077500000000000000000000000001456724742500141745ustar00rootroot00000000000000zcfan-1.3.0/.github/workflows/000077500000000000000000000000001456724742500162315ustar00rootroot00000000000000zcfan-1.3.0/.github/workflows/ci.yml000066400000000000000000000007631456724742500173550ustar00rootroot00000000000000jobs: build_and_test: name: CI runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - run: sudo add-apt-repository -y ppa:ubuntu-toolchain-r/test - run: sudo apt-get update - uses: awalsh128/cache-apt-pkgs-action@v1 with: packages: clang clang-tidy clang-format version: 1.0 - run: make lint - run: make clean clang-tidy - run: make clean clang-everything on: push: pull_request: workflow_dispatch: zcfan-1.3.0/.gitignore000066400000000000000000000000061456724742500146200ustar00rootroot00000000000000zcfan zcfan-1.3.0/LICENSE000066400000000000000000000020751456724742500136450ustar00rootroot00000000000000The MIT License Copyright (c) 2022-present Christopher Down Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. zcfan-1.3.0/Makefile000066400000000000000000000031761456724742500143030ustar00rootroot00000000000000CFLAGS:=-std=gnu99 -O2 -pedantic -Wall -Wextra -Wwrite-strings -Warray-bounds -Wconversion -Wstrict-prototypes -Werror $(CFLAGS) CPPFLAGS:=$(CPPFLAGS) SOURCES=$(wildcard *.c) EXECUTABLES=$(patsubst %.c,%,$(SOURCES)) INSTALL:=install prefix:=/usr/local bindir:=$(prefix)/bin datarootdir:=$(prefix)/share mandir:=$(datarootdir)/man all: $(EXECUTABLES) %: %.c $(CC) $(CPPFLAGS) $(CFLAGS) $< -o $@ $(LIBS) $(LDFLAGS) %.o: %.c $(CC) $(CPPFLAGS) $(CFLAGS) $< -c -o $@ %: %.o $(CC) $< -o $@ $(LIBS) $(LDFLAGS) # Noisy clang build that's expected to fail, but can be useful to find corner # cases. clang-everything: CC=clang clang-everything: CFLAGS+=-Weverything -Wno-disabled-macro-expansion -Wno-padded -Wno-covered-switch-default -Wno-gnu-zero-variadic-macro-arguments clang-everything: all sanitisers: CFLAGS+=-fsanitize=address -fsanitize=undefined -fanalyzer sanitisers: debug debug: CFLAGS+=-Og -ggdb -fno-omit-frame-pointer debug: all clang-tidy: # DeprecatedOrUnsafeBufferHandling: See https://stackoverflow.com/a/50724865/945780 # clang-diagnostic-gnu-zero-variadic-macro-arguments: We require this for ##__VA_ARGS__ clang-tidy zcfan.c -checks=-clang-analyzer-security.insecureAPI.DeprecatedOrUnsafeBufferHandling,-clang-diagnostic-gnu-zero-variadic-macro-arguments -- $(CPPFLAGS) $(CFLAGS) $(LDFLAGS) install: all mkdir -p $(DESTDIR)$(bindir)/ $(INSTALL) -pt $(DESTDIR)$(bindir)/ $(EXECUTABLES) $(INSTALL) -Dp -m 644 zcfan.service $(DESTDIR)$(prefix)/lib/systemd/system/zcfan.service $(INSTALL) -Dp -m 644 zcfan.1 $(DESTDIR)$(mandir)/man1/zcfan.1 lint: clang-format -style=file --dry-run --Werror zcfan.c clean: rm -f $(EXECUTABLES) zcfan-1.3.0/README.md000066400000000000000000000043651456724742500141230ustar00rootroot00000000000000# zcfan | [![Tests](https://img.shields.io/github/actions/workflow/status/cdown/zcfan/ci.yml?branch=master)](https://github.com/cdown/zcfan/actions?query=branch%3Amaster) Zero-configuration fan control daemon for ThinkPads. ## Features - Extremely small (~250 lines), simple, and easy to understand code - Sensible out of the box, configuration is optional (see "usage" below) - Strong focus on stopping the fan as soon as safe to do so, without inducing throttling - Automatic temperature- and time-based hysteresis: no bouncing between fan levels - Watchdog support - Minimal resource usage - No dependencies ## Usage zcfan has the following default fan states: | Config name | thinkpad_acpi fan level | Default trip temperature (C) | |-------------|-----------------------------------|------------------------------| | max_temp | full-speed (or 7 if unsupported) | 90 | | med_temp | 4 | 80 | | low_temp | 1 | 70 | If no trip temperature is reached, the fan will be turned off. To override these defaults, you can place a file at `/etc/zcfan.conf` with updated trip temperatures in degrees celsius. As an example: max_temp 85 med_temp 70 low_temp 55 ### Hysteresis We will only reduce the fan level again once: 1. The temperature is now at least 10C below the trip point, and 2. At least 3 seconds have elapsed since the initial trip. This avoids unnecessary fluctuations in fan speed. ## Comparison with thinkfan I wrote zcfan because I found thinkfan's configuration and code complexity too much for my tastes. Use whichever suits your needs. ## Compilation Run `make`. ## Installation 1. Compile zcfan or install from the [AUR package](https://aur.archlinux.org/packages/zcfan) 2. Load your thinkpad_acpi module with `fan_control=1` - At runtime: `rmmod thinkpad_acpi && modprobe thinkpad_acpi fan_control=1` - By default: `echo options thinkpad_acpi fan_control=1 > /etc/modprobe.d/99-fancontrol.conf` 3. Run `zcfan` as root (or use the `zcfan` systemd service provided) ## Disclaimer While the author uses this on their own machine, obviously there is no warranty whatsoever. zcfan-1.3.0/zcfan.1000066400000000000000000000027031456724742500140210ustar00rootroot00000000000000.TH zcfan 1 .SH NAME zcfan - zero-configuration fan control daemon for ThinkPads .SH DESCRIPTION .B zcfan is a minimal, zero-configuration fan control daemon for ThinkPads. .SH OPTIONS .B zcfan does not take any options. If any are provided, a help message will be printed and .B zcfan will exit. .SH CONFIGURATION .SS USAGE .B zcfan has the following default fan states: .RS .TS tab(;); l l l. Config name;thinkpad_acpi fan level;Default trip temperature (C) _ max_temp;7;90 med_temp;4;80 low_temp;1;70 .TE .RE You can optionally override this configuration at .I /etc/zcfan.conf with your desired threshold values in the following format: .RS .EX max_temp 85 med_temp 70 low_temp 55 .EE .RE If no trip temperature is reached, the fan will be turned off. .SS HYSTERESIS We will only reduce the fan level again once: .IP "1." 3 The temperature is now at least 10C below the trip point, and .IP "2." 3 At least 3 seconds have elapsed since the initial trip. .PP This avoids unnecessary fluctuations in fan speed. .SS WATCHDOG The kernel watchdog is reset every 120 seconds by default, but a smaller value can be selected in the .I /etc/zcfan.conf config file with the syntax .RS .EX watchdog_secs 10 .EE .RE .SH DEPENDENCIES .B thinkpad-acpi must be loaded with .IR fan_control=1. .SH SEE ALSO .BR thinkfan (1) .SH AUTHOR Chris Down .MT chris@chrisdown.name .ME .SH REPORTING BUGS Please send bug reports to .UR https://github.com/cdown/zcfan/issues .UE . zcfan-1.3.0/zcfan.c000066400000000000000000000226341456724742500141100ustar00rootroot00000000000000#include #include #include #include #include #include #include #include #include #include #define MILLIC_TO_C(n) (n / 1000) #define TEMP_FILES_GLOB "/sys/class/hwmon/hwmon*/temp*_input" #define FAN_CONTROL_FILE "/proc/acpi/ibm/fan" #define TEMP_INVALID INT_MIN #define TEMP_MIN INT_MIN + 1 #define STR_HELPER(x) #x #define STR(x) STR_HELPER(x) #define DEFAULT_WATCHDOG_SECS 120 #define S_DEFAULT_WATCHDOG_SECS STR(DEFAULT_WATCHDOG_SECS) #define err(fmt, ...) fprintf(stderr, "[ERR] " fmt, ##__VA_ARGS__) #define max(x, y) ((x) > (y) ? (x) : (y)) #define expect(x) \ do { \ if (!(x)) { \ fprintf(stderr, "FATAL: !(%s) at %s:%s:%d\n", #x, __FILE__, \ __func__, __LINE__); \ abort(); \ } \ } while (0) /* Must be highest to lowest temp */ enum FanLevel { FAN_MAX, FAN_MED, FAN_LOW, FAN_OFF, FAN_INVALID }; struct Rule { const char *tpacpi_level; int threshold; const char *name; }; static struct Rule rules[] = { [FAN_MAX] = {"full-speed", 90, "maximum"}, [FAN_MED] = {"4", 80, "medium"}, [FAN_LOW] = {"1", 70, "low"}, [FAN_OFF] = {"0", TEMP_MIN, "off"}, }; static struct timespec last_watchdog_ping = {0, 0}; static time_t watchdog_secs = DEFAULT_WATCHDOG_SECS; static const unsigned int fan_hysteresis = 10; static const unsigned int tick_hysteresis = 3; static char output_buf[512]; static const struct Rule *current_rule = NULL; static volatile sig_atomic_t run = 1; static int first_tick = 1; /* Stop running if errors are immediate */ static glob_t temp_files; static void exit_if_first_tick(void) { if (first_tick) { err("Quitting due to failure during first run\n"); exit(1); } } static int glob_err_handler(const char *epath, int eerrno) { err("glob: %s: %s\n", epath, strerror(eerrno)); return 0; } static void populate_temp_files(void) { expect(glob(TEMP_FILES_GLOB, 0, glob_err_handler, &temp_files) == 0); } static int full_speed_supported(void) { FILE *f = fopen(FAN_CONTROL_FILE, "re"); char line[256]; // If exceeded, we'll just read again int found = 0; expect(f); while (fgets(line, sizeof(line), f) != NULL) { if (strstr(line, "full-speed") != NULL) { found = 1; break; } } fclose(f); return found; } static int read_temp_file(const char *filename) { FILE *f = fopen(filename, "re"); int val; if (!f) { return TEMP_INVALID; } if (fscanf(f, "%d", &val) != 1) { val = TEMP_INVALID; } fclose(f); return val; } static int get_max_temp(void) { int max_temp = TEMP_INVALID; for (size_t i = 0; i < temp_files.gl_pathc; i++) { int temp = read_temp_file(temp_files.gl_pathv[i]); max_temp = max(max_temp, temp); } if (max_temp == TEMP_INVALID) { err("Couldn't find any valid temperature\n"); exit_if_first_tick(); return TEMP_INVALID; } return MILLIC_TO_C(max_temp); } #define write_fan_level(level) write_fan("level", level) static int write_fan(const char *command, const char *value) { FILE *f = fopen(FAN_CONTROL_FILE, "we"); int ret; if (!f) { err("%s: fopen: %s%s\n", FAN_CONTROL_FILE, strerror(errno), errno == ENOENT ? " (is thinkpad_acpi loaded?)" : ""); exit_if_first_tick(); return -errno; } expect(setvbuf(f, NULL, _IONBF, 0) == 0); /* Make fprintf see errors */ ret = fprintf(f, "%s %s", command, value); if (ret < 0) { err("%s: write: %s%s\n", FAN_CONTROL_FILE, strerror(errno), errno == EINVAL ? " (did you enable fan_control=1?)" : ""); exit_if_first_tick(); fclose(f); return -errno; } expect(clock_gettime(CLOCK_MONOTONIC, &last_watchdog_ping) == 0); fclose(f); return 0; } static void write_watchdog_timeout(const time_t timeout) { char timeout_s[sizeof(S_DEFAULT_WATCHDOG_SECS)]; /* max timeout value */ int ret = snprintf(timeout_s, sizeof(timeout_s), "%" PRIuMAX, (uintmax_t)timeout); expect(ret >= 0 && (size_t)ret < sizeof(timeout_s)); write_fan("watchdog", timeout_s); } /* 1: set fan level, 0: didn't set fan level */ static int set_fan_level(void) { int max_temp = get_max_temp(), temp_penalty = 0; static unsigned int tick_penalty = tick_hysteresis; if (tick_penalty > 0) { tick_penalty--; } if (max_temp == TEMP_INVALID) { write_fan_level("full-speed"); return 1; } for (size_t i = 0; i < FAN_INVALID; i++) { const struct Rule *rule = rules + i; if (rule == current_rule) { if (tick_penalty) { return 0; /* Must wait longer until able to move down levels */ } temp_penalty = fan_hysteresis; } if (rule->threshold < temp_penalty || (rule->threshold - temp_penalty) < max_temp) { if (rule != current_rule) { current_rule = rule; tick_penalty = tick_hysteresis; printf("[FAN] Temperature now %dC, fan set to %s\n", max_temp, rule->name); write_fan_level(rule->tpacpi_level); return 1; } return 0; } } err("No threshold matched?\n"); return 0; } #define WATCHDOG_GRACE_PERIOD_SECS 2 static void maybe_ping_watchdog(void) { struct timespec now; expect(clock_gettime(CLOCK_MONOTONIC, &now) == 0); if (now.tv_sec - last_watchdog_ping.tv_sec < (watchdog_secs - WATCHDOG_GRACE_PERIOD_SECS)) { return; } expect(current_rule); /* Already set up on first run by set_fan_level */ write_fan_level(current_rule->tpacpi_level); } #define CONFIG_PATH "/etc/zcfan.conf" #define fscanf_int_for_key(f, pos, name, dest) \ do { \ int val; \ if (fscanf(f, name " %d ", &val) == 1) { \ dest = val; \ } else { \ expect(fseek(f, pos, SEEK_SET) == 0); \ } \ } while (0) static void get_config(void) { FILE *f; f = fopen(CONFIG_PATH, "re"); if (!f) { if (errno != ENOENT) { err("%s: fopen: %s\n", CONFIG_PATH, strerror(errno)); exit_if_first_tick(); } return; } while (!feof(f)) { long pos = ftell(f); int ch; expect(pos >= 0); fscanf_int_for_key(f, pos, "max_temp", rules[FAN_MAX].threshold); fscanf_int_for_key(f, pos, "med_temp", rules[FAN_MED].threshold); fscanf_int_for_key(f, pos, "low_temp", rules[FAN_LOW].threshold); fscanf_int_for_key(f, pos, "watchdog_secs", watchdog_secs); if (ftell(f) == pos) { while ((ch = fgetc(f)) != EOF && ch != '\n') {} } } /* Maximum value handled by the kernel is 120, and * (watchdog_secs - WATCHDOG_GRACE_PERIOD_SECS) must stay positive. */ if (watchdog_secs < WATCHDOG_GRACE_PERIOD_SECS || watchdog_secs > DEFAULT_WATCHDOG_SECS) { err("%s: value for the watchdog_secs directive has to be between %d and %d\n", CONFIG_PATH, WATCHDOG_GRACE_PERIOD_SECS, DEFAULT_WATCHDOG_SECS); exit(1); } fclose(f); } static void print_thresholds(void) { for (size_t i = 0; i < FAN_OFF; i++) { const struct Rule *rule = rules + i; printf("[CFG] At %dC fan is set to %s\n", rule->threshold, rule->name); } } static void stop(int sig) { (void)sig; run = 0; } int main(int argc, char *argv[]) { const struct sigaction sa_exit = { .sa_handler = stop, }; (void)argv; if (argc != 1) { printf("zcfan: Zero-configuration ThinkPad fan daemon.\n\n"); printf(" [any argument] Show this help\n\n"); printf("See the zcfan(1) man page for details.\n"); return 0; } get_config(); print_thresholds(); expect(sigaction(SIGTERM, &sa_exit, NULL) == 0); expect(sigaction(SIGINT, &sa_exit, NULL) == 0); expect(setvbuf(stdout, output_buf, _IOLBF, sizeof(output_buf)) == 0); if (!full_speed_supported()) { err("level \"full-speed\" not supported, using level 7\n"); rules[FAN_MAX].tpacpi_level = "7"; } write_watchdog_timeout(watchdog_secs); populate_temp_files(); while (run) { int set = set_fan_level(); if (!set) { maybe_ping_watchdog(); } if (run) { sleep(1); first_tick = 0; } } globfree(&temp_files); printf("[FAN] Quit requested, reenabling thinkpad_acpi fan control\n"); if (write_fan_level("auto") == 0) { write_watchdog_timeout(0); } } zcfan-1.3.0/zcfan.service000066400000000000000000000006771456724742500153310ustar00rootroot00000000000000[Unit] Description=Zero-configuration fan control for ThinkPad [Service] ExecStart=/usr/bin/zcfan Restart=always RestartSec=500ms MemoryDenyWriteExecute=yes NoNewPrivileges=yes ProtectControlGroups=yes RestrictAddressFamilies= RestrictRealtime=yes # We don't need to do any substantial clean up, so if something hangs it's # going to stay that way. Just forcefully kill and get it over with. TimeoutStopSec=2 [Install] WantedBy=default.target