From 906d215bf56f0164312ea71cf18f5dc5ee17aed9 Mon Sep 17 00:00:00 2001 From: Andrea Mazzoleni Date: Sun, 21 Sep 2025 19:54:10 +0200 Subject: [PATCH] Add thermal protection options: temp_limit and temp_sleep Introduce two new configuration directives to protect disks from overheating: - temp_limit Sets the maximum allowed disk temperature. When any disk exceeds this limit, SnapRAID stops all operations and spins down the disks. - temp_sleep Specifies how long disks remain in standby after a temperature event. Defaults to 15 minutes. These options allow SnapRAID to automatically pause activity and cool down disks when a temperature threshold is reached, improving data safety and hardware longevity. --- HISTORY | 10 + Makefile.am | 2 + cmdline/check.c | 15 ++ cmdline/dry.c | 15 ++ cmdline/mingw.c | 16 +- cmdline/mingw.h | 2 +- cmdline/mkstream.c | 2 + cmdline/mktest.c | 2 + cmdline/portable.h | 12 ++ cmdline/scrub.c | 15 ++ cmdline/state.c | 169 +++++++++++++++- cmdline/state.h | 35 +++- cmdline/support.c | 51 +++++ cmdline/support.h | 20 ++ cmdline/sync.c | 25 +++ cmdline/thermal.c | 367 ++++++++++++++++++++++++++++++++++ cmdline/thermal.h | 75 +++++++ cmdline/unix.c | 213 +++++++++++++++++--- snapraid.1 | 15 +- snapraid.conf.example | 12 ++ snapraid.conf.example.windows | 12 ++ snapraid.d | 17 +- snapraid.txt | 123 ++++++++---- 23 files changed, 1144 insertions(+), 81 deletions(-) create mode 100644 cmdline/thermal.c create mode 100644 cmdline/thermal.h diff --git a/HISTORY b/HISTORY index a0593ff0..42a8e561 100644 --- a/HISTORY +++ b/HISTORY @@ -3,6 +3,16 @@ SnapRAID HISTORY 13.0 2025/XX ============ +* Add new thermal protection configuration options: + - temp_limit TEMPERATURE_CELSIUS + Sets the maximum allowed disk temperature. When any disk exceeds this + limit, SnapRAID stops all operations and spins down the disks to prevent + overheating. + - temp_sleep TIME_IN_MINUTES + Defines how long the disks remain in standby after a temperature limit + event. Defaults to 15 minutes. + These options allow SnapRAID to automatically pause activity and cool down + disks when a temperature threshold is reached. * Add a new "probe" command that shows the spinning status of all disks. * Add a new -s, --spin-down-on-error option that spins down all disks when a command ends with an error. diff --git a/Makefile.am b/Makefile.am index a3fb237b..bfaa9273 100644 --- a/Makefile.am +++ b/Makefile.am @@ -51,6 +51,7 @@ snapraid_SOURCES = \ cmdline/speed.c \ cmdline/import.c \ cmdline/search.c \ + cmdline/thermal.c \ cmdline/mingw.c \ cmdline/unix.c @@ -96,6 +97,7 @@ noinst_HEADERS = \ cmdline/fnmatch.h \ cmdline/import.h \ cmdline/search.h \ + cmdline/thermal.h \ cmdline/mingw.h \ cmdline/unix.h diff --git a/cmdline/check.c b/cmdline/check.c index c1fe274b..23d2abcb 100644 --- a/cmdline/check.c +++ b/cmdline/check.c @@ -1458,6 +1458,21 @@ static int state_check_process(struct snapraid_state* state, int fix, struct sna break; /* LCOV_EXCL_STOP */ } + + /* thermal control */ + if (state_thermal_alarm(state)) { + /* until now is misc */ + state_usage_misc(state); + + state_progress_stop(state); + + state_thermal_cooldown(state); + + state_progress_restart(state); + + /* drop until now */ + state_usage_waste(state); + } } /* for each disk, recover empty files, symlinks and empty dirs */ diff --git a/cmdline/dry.c b/cmdline/dry.c index 202f4e76..20cfc513 100644 --- a/cmdline/dry.c +++ b/cmdline/dry.c @@ -345,6 +345,21 @@ static int state_dry_process(struct snapraid_state* state, struct snapraid_parit break; /* LCOV_EXCL_STOP */ } + + /* thermal control */ + if (state_thermal_alarm(state)) { + /* until now is misc */ + state_usage_misc(state); + + state_progress_stop(state); + + state_thermal_cooldown(state); + + state_progress_restart(state); + + /* drop until now */ + state_usage_waste(state); + } } end: diff --git a/cmdline/mingw.c b/cmdline/mingw.c index 3c655322..cb87c309 100644 --- a/cmdline/mingw.c +++ b/cmdline/mingw.c @@ -1549,9 +1549,16 @@ const char* windows_stat_desc(struct stat* st) return st->st_desc; } -void windows_sleep(unsigned seconds) +unsigned windows_sleep(unsigned seconds) { - Sleep(seconds * 1000); + while (seconds) { + if (global_interrupt) /* SIGINT should stop the sleep */ + break; + Sleep(1000); + --seconds; + } + + return seconds; } void windows_usleep(uint64_t useconds) @@ -3029,5 +3036,10 @@ int windows_join(thread_id_t thread, void** retval) return 0; } +int ambient_temperature(void) +{ + return 0; +} + #endif diff --git a/cmdline/mingw.h b/cmdline/mingw.h index 0a9ced24..b540935b 100644 --- a/cmdline/mingw.h +++ b/cmdline/mingw.h @@ -331,7 +331,7 @@ const char* windows_stat_desc(struct stat* st); /** * Like sleep(). */ -void windows_sleep(unsigned seconds); +unsigned windows_sleep(unsigned seconds); /** * List usleep(). diff --git a/cmdline/mkstream.c b/cmdline/mkstream.c index 5a2f804c..422d30d8 100644 --- a/cmdline/mkstream.c +++ b/cmdline/mkstream.c @@ -20,6 +20,8 @@ #include "stream.h" #include "support.h" +volatile int global_interrupt = 0; + #define STREAM_MAX 8 #define BUFFER_MAX 64 #define STR_MAX 128 diff --git a/cmdline/mktest.c b/cmdline/mktest.c index 1df95cc9..c83807bf 100644 --- a/cmdline/mktest.c +++ b/cmdline/mktest.c @@ -19,6 +19,8 @@ #include "support.h" +volatile int global_interrupt = 0; + /****************************************************************************/ /* random */ diff --git a/cmdline/portable.h b/cmdline/portable.h index d3d91a16..793f6ab7 100644 --- a/cmdline/portable.h +++ b/cmdline/portable.h @@ -533,5 +533,17 @@ typedef struct devinfo_struct devinfo_t; */ int devquery(tommy_list* high, tommy_list* low, int operation, int others); +/** + * Get the ambient temperature in degree + * + * Return 0 if not available. + */ +int ambient_temperature(void); + +/** + * Global variable to identify if Ctrl+C is pressed. + */ +extern volatile int global_interrupt; + #endif diff --git a/cmdline/scrub.c b/cmdline/scrub.c index 3fc0f5c8..8d32e40a 100644 --- a/cmdline/scrub.c +++ b/cmdline/scrub.c @@ -627,6 +627,21 @@ static int state_scrub_process(struct snapraid_state* state, struct snapraid_par /* LCOV_EXCL_STOP */ } + /* thermal control */ + if (state_thermal_alarm(state)) { + /* until now is misc */ + state_usage_misc(state); + + state_progress_stop(state); + + state_thermal_cooldown(state); + + state_progress_restart(state); + + /* drop until now */ + state_usage_waste(state); + } + /* autosave */ if (state->autosave != 0 && autosavedone >= autosavelimit /* if we have reached the limit */ diff --git a/cmdline/state.c b/cmdline/state.c index aa1c2c85..705a4094 100644 --- a/cmdline/state.c +++ b/cmdline/state.c @@ -26,6 +26,7 @@ #include "stream.h" #include "handle.h" #include "io.h" +#include "thermal.h" #include "raid/raid.h" #include "raid/cpu.h" @@ -177,12 +178,20 @@ void state_init(struct snapraid_state* state) state->no_conf = 0; for (i = 0; i < SMART_IGNORE_MAX; ++i) state->smartignore[i] = 0; + state->thermal_stop_gathering = 0; + state->thermal_ambient_temperature = 0; + state->thermal_highest_temperature = 0; + state->thermal_first = 0; + state->thermal_latest = 0; + state->thermal_temperature_limit = 0; + state->thermal_cooldown_time = 0; tommy_list_init(&state->disklist); tommy_list_init(&state->maplist); tommy_list_init(&state->contentlist); tommy_list_init(&state->filterlist); tommy_list_init(&state->importlist); + tommy_list_init(&state->thermallist); tommy_hashdyn_init(&state->importset); tommy_hashdyn_init(&state->previmportset); tommy_hashdyn_init(&state->searchset); @@ -196,6 +205,7 @@ void state_done(struct snapraid_state* state) tommy_list_foreach(&state->contentlist, (tommy_foreach_func*)content_free); tommy_list_foreach(&state->filterlist, (tommy_foreach_func*)filter_free); tommy_list_foreach(&state->importlist, (tommy_foreach_func*)import_file_free); + tommy_list_foreach(&state->thermallist, (tommy_foreach_func*)thermal_free); tommy_hashdyn_foreach(&state->searchset, (tommy_foreach_func*)search_file_free); tommy_hashdyn_done(&state->importset); tommy_hashdyn_done(&state->previmportset); @@ -1154,6 +1164,42 @@ void state_config(struct snapraid_state* state, const char* path, const char* co exit(EXIT_FAILURE); /* LCOV_EXCL_STOP */ } + } else if (strcmp(tag, "temp_limit") == 0) { + unsigned temp; + + ret = sgetu32(f, &temp); + if (ret < 0) { + /* LCOV_EXCL_START */ + log_fatal("Invalid 'temp_limit' specification in '%s' at line %u\n", path, line); + exit(EXIT_FAILURE); + /* LCOV_EXCL_STOP */ + } + if (temp < 40 || temp > 70) { + /* LCOV_EXCL_START */ + log_fatal("Invalid 'temp_limit' temperature specification in '%s' at line %u. It must be between 40 and 70\n", path, line); + exit(EXIT_FAILURE); + /* LCOV_EXCL_STOP */ + } + + state->thermal_temperature_limit = temp; + } else if (strcmp(tag, "temp_sleep") == 0) { + unsigned time; + + ret = sgetu32(f, &time); + if (ret < 0) { + /* LCOV_EXCL_START */ + log_fatal("Invalid 'temp_sleep' specification in '%s' at line %u\n", path, line); + exit(EXIT_FAILURE); + /* LCOV_EXCL_STOP */ + } + if (time < 5 || time > 120) { + /* LCOV_EXCL_START */ + log_fatal("Invalid 'temp_sleep' temperature specification in '%s' at line %u. It must be between 5 and 120\n", path, line); + exit(EXIT_FAILURE); + /* LCOV_EXCL_STOP */ + } + + state->thermal_cooldown_time = time * 60; } else if (strcmp(tag, "nohidden") == 0) { state->filter_hidden = 1; } else if (strcmp(tag, "exclude") == 0) { @@ -4300,11 +4346,14 @@ int state_progress_begin(struct snapraid_state* state, block_off_t blockstart, b now = time(0); state->progress_whole_start = now; - state->progress_tick = 0; state->progress_ptr = 0; state->progress_wasted = 0; + /* start of thermal control */ + if (!state_thermal_begin(state, now)) + return 0; + /* stop if requested */ if (global_interrupt) { /* LCOV_EXCL_START */ @@ -4403,13 +4452,37 @@ static void state_progress_latest(struct snapraid_state* state) /** * Number of columns */ -#define GRAPH_COLUMN 68 +#define GRAPH_COLUMN 78 /** * Get the reference value, 0 if index is PROGRESS_MAX */ #define ref(map, index) (index < PROGRESS_MAX ? map[index] : 0) +static struct snapraid_thermal* state_thermal_find(struct snapraid_state* state, const char* name) +{ + tommy_node* i; + struct snapraid_thermal* found = 0; + + for (i = tommy_list_head(&state->thermallist); i != 0; i = i->next) { + struct snapraid_thermal* thermal = i->data; + if (strcmp(thermal->name, name) == 0) { + if (found == 0 + // if multiple matches, return the one with highest temperature + || found->latest_temperature < thermal->latest_temperature + // or, if the temperature is equal, the one with the higher r_squared + || (found->latest_temperature == thermal->latest_temperature && found->params.r_squared < thermal->params.r_squared) + ) { + found = thermal; + } + } + } + + return found; +} + +#define THERMAL_PAD " " + static void state_progress_graph(struct snapraid_state* state, struct snapraid_io* io, unsigned current, unsigned oldest) { uint64_t v; @@ -4418,6 +4491,7 @@ static void state_progress_graph(struct snapraid_state* state, struct snapraid_i tommy_node* i; unsigned l; size_t pad; + size_t pre; tick_total = 0; @@ -4445,10 +4519,11 @@ static void state_progress_graph(struct snapraid_state* state, struct snapraid_i /* extra space */ pad += 1; - if (pad + 30 < GRAPH_COLUMN) - bar = GRAPH_COLUMN - pad; + pre = 4; + if (pad + pre + 30 < GRAPH_COLUMN) + bar = GRAPH_COLUMN - pad - pre; else - bar = 30; + bar = 30; /* at least a bar of 30 */ if (io) { const char* legend = "cached blocks (instant, more is better)"; @@ -4458,7 +4533,6 @@ static void state_progress_graph(struct snapraid_state* state, struct snapraid_i printf("\n"); - /* search for the slowest */ for (i = state->disklist; i != 0; i = i->next) { struct snapraid_disk* disk = i->data; v = disk->cached_blocks; @@ -4493,10 +4567,33 @@ static void state_progress_graph(struct snapraid_state* state, struct snapraid_i printf("\n"); + pre = 4; + if (state->thermal_temperature_limit != 0) + pre += strlen(THERMAL_PAD); + if (pad + pre + 30 < GRAPH_COLUMN) + bar = GRAPH_COLUMN - pad - pre; + else + bar = 30; /* at least a bar of 30 */ + for (i = state->disklist; i != 0; i = i->next) { struct snapraid_disk* disk = i->data; + + v = disk->progress_tick[current] - ref(disk->progress_tick, oldest); printr(disk->name, pad); + + if (state->thermal_temperature_limit != 0) { + struct snapraid_thermal* thermal = state_thermal_find(state, disk->name); + if (thermal) { + if (thermal->params.r_squared >= THERMAL_R_SQUARED_LIMIT) + printf(" %2u (%2u)", thermal->latest_temperature, (int)thermal->params.t_steady); + else + printf(" %2u ", thermal->latest_temperature); + } else { + printf(THERMAL_PAD); + } + } + printf("%3u%% | ", muldiv(v, 100, tick_total)); printc('*', v * bar / tick_total); printf("\n"); @@ -4508,6 +4605,19 @@ static void state_progress_graph(struct snapraid_state* state, struct snapraid_i for (l = 0; l < state->level; ++l) { v = state->parity[l].progress_tick[current] - ref(state->parity[l].progress_tick, oldest); printr(lev_config_name(l), pad); + + if (state->thermal_temperature_limit != 0) { + struct snapraid_thermal* thermal = state_thermal_find(state, lev_config_name(l)); + if (thermal) { + if (thermal->params.r_squared >= THERMAL_R_SQUARED_LIMIT) + printf(" %2u (%2u)", thermal->latest_temperature, (int)thermal->params.t_steady); + else + printf(" %2u ", thermal->latest_temperature); + } else { + printf(THERMAL_PAD); + } + } + printf("%3u%% | ", muldiv(v, 100, tick_total)); printc('*', v * bar / tick_total); printf("\n"); @@ -4515,40 +4625,54 @@ static void state_progress_graph(struct snapraid_state* state, struct snapraid_i v = state->progress_tick_raid[current] - ref(state->progress_tick_raid, oldest); printr("raid", pad); + if (state->thermal_temperature_limit != 0) + printf(THERMAL_PAD); printf("%3u%% | ", muldiv(v, 100, tick_total)); printc('*', v * bar / tick_total); printf("\n"); v = state->progress_tick_hash[current] - ref(state->progress_tick_hash, oldest); printr("hash", pad); + if (state->thermal_temperature_limit != 0) + printf(THERMAL_PAD); printf("%3u%% | ", muldiv(v, 100, tick_total)); printc('*', v * bar / tick_total); printf("\n"); v = state->progress_tick_sched[current] - ref(state->progress_tick_sched, oldest); printr("sched", pad); + if (state->thermal_temperature_limit != 0) + printf(THERMAL_PAD); printf("%3u%% | ", muldiv(v, 100, tick_total)); printc('*', v * bar / tick_total); printf("\n"); v = state->progress_tick_misc[current] - ref(state->progress_tick_misc, oldest); printr("misc", pad); + if (state->thermal_temperature_limit != 0) + printf(THERMAL_PAD); printf("%3u%% | ", muldiv(v, 100, tick_total)); printc('*', v * bar / tick_total); printf("\n"); printc(' ', pad); + if (state->thermal_temperature_limit != 0) + printf(THERMAL_PAD); printf(" |_"); printc('_', bar); printf("\n"); if (oldest == PROGRESS_MAX) { const char* legend = "wait time (total, less is better)"; + if (state->thermal_temperature_limit != 0) + printf(THERMAL_PAD); printc(' ', 5 + pad + 1 + bar / 2 - strlen(legend) / 2); printf("%s", legend); printf("\n"); } else { const char* legend_d = "wait time (last %d secs, less is better)"; + if (state->thermal_temperature_limit != 0) + printf(THERMAL_PAD); printc(' ', 5 + pad + 1 + bar / 2 - strlen(legend_d) / 2); printf(legend_d, PROGRESS_MAX); printf("\n"); @@ -4564,6 +4688,12 @@ int state_progress(struct snapraid_state* state, struct snapraid_io* io, block_o now = time(0); + /* thermal measure */ + if (now > state->thermal_latest + THERMAL_PERIOD_SECONDS) { + state->thermal_latest = now; + state_thermal(state, now); + } + /* previous position */ pred = state->progress_ptr + PROGRESS_MAX - 1; if (pred >= PROGRESS_MAX) @@ -4579,6 +4709,8 @@ int state_progress(struct snapraid_state* state, struct snapraid_io* io, block_o unsigned out_block_speed = 0; unsigned out_cpu = 0; unsigned out_eta = 0; + int out_temperature = 0; + int out_steady = 0; int out_computed = 0; /* store the new measure */ @@ -4609,6 +4741,7 @@ int state_progress(struct snapraid_state* state, struct snapraid_io* io, block_o uint64_t delta_tick_total; uint64_t oldest_tick_cpu; uint64_t oldest_tick_total; + tommy_node* i; /* number of past measures */ past = state->progress_tick; @@ -4668,6 +4801,24 @@ int state_progress(struct snapraid_state* state, struct snapraid_io* io, block_o state_progress_graph(state, io, state->progress_ptr, oldest); } + if (state->thermal_temperature_limit != 0) { + /* get the max temperature */ + out_temperature = 0; + for (i = tommy_list_head(&state->thermallist); i != 0; i = i->next) { + struct snapraid_thermal* thermal = i->data; + if (out_temperature < thermal->latest_temperature) { + out_temperature = thermal->latest_temperature; + if (thermal->params.r_squared >= THERMAL_R_SQUARED_LIMIT) + out_steady = thermal->params.t_steady; + else + out_steady = 0; + } + } + } else { + out_temperature = 0; + out_steady = 0; + } + /* we have the output value */ out_computed = 1; } @@ -4681,6 +4832,12 @@ int state_progress(struct snapraid_state* state, struct snapraid_io* io, block_o msg_bar(", %u MB/s", out_size_speed); msg_bar(", %u stripe/s", out_block_speed); msg_bar(", CPU %u%%", out_cpu); + if (out_temperature != 0) { + if (out_steady != 0) + msg_bar(", Tmax %u (%u)", out_temperature, out_steady); + else + msg_bar(", Tmax %u", out_temperature); + } msg_bar(", %u:%02u ETA", out_eta / 60, out_eta % 60); } msg_bar("%s\r", PROGRESS_CLEAR); diff --git a/cmdline/state.h b/cmdline/state.h index f95881d6..f97680fa 100644 --- a/cmdline/state.h +++ b/cmdline/state.h @@ -56,11 +56,6 @@ const char* lev_config_name(unsigned level); #define GIBI (1024 * 1024 * 1024) #define TEBI (1024 * 1024 * 1024 * 1024LL) -/** - * Global variable to identify if Ctrl+C is pressed. - */ -extern volatile int global_interrupt; - #define SORT_PHYSICAL 1 /**< Sort by physical order. */ #define SORT_INODE 2 /**< Sort by inode. */ #define SORT_ALPHA 3 /**< Sort by alphabetic order. */ @@ -155,6 +150,16 @@ struct snapraid_state { tommy_hashdyn searchset; /**< Hashtable by timestamp of all the search files. */ tommy_arrayblkof infoarr; /**< Block information array. */ + /* Thermal */ + tommy_list thermallist; /**< List of all thermal state */ + int thermal_stop_gathering; /**< If thermal data gathering is stopped */ + int thermal_ambient_temperature; /**< Ambient themperature. 0 if not available */ + int thermal_highest_temperature; /**< Current highest temperature */ + time_t thermal_first; /**< Time of the first measure */ + time_t thermal_latest; /**< Time of latest measure */ + int thermal_cooldown_time; /**< Cooldown wait time in seconds */ + int thermal_temperature_limit; /**< Upper limit temperature of the operating range */ + /** * Cumulative time used for computations. */ @@ -377,6 +382,26 @@ void state_usage_print(struct snapraid_state* state); */ void state_fscheck(struct snapraid_state* state, const char* ope); +/** + * Measure the temperature of all disks + */ +void state_thermal(struct snapraid_state* state, time_t now); + +/** + * Check if the temperature is outside the operating range + */ +int state_thermal_alarm(struct snapraid_state* state); + +/** + * Cool down the system + */ +void state_thermal_cooldown(struct snapraid_state* state); + +/** + * Start of thermal processing + */ +int state_thermal_begin(struct snapraid_state* state, time_t now); + /****************************************************************************/ /* misc */ diff --git a/cmdline/support.c b/cmdline/support.c index ca792811..f97e9ad7 100644 --- a/cmdline/support.c +++ b/cmdline/support.c @@ -631,6 +631,57 @@ unsigned strsplit(char** split_map, unsigned split_max, char* str, const char* d return mac; } +void strtrim(char* str) +{ + char* begin; + char* end; + + begin = str; + while (begin[0] && isspace((unsigned char)begin[0])) + ++begin; + + end = begin + strlen(begin); + while (end > begin && isspace((unsigned char)end[-1])) + --end; + + end[0] = 0; + + if (begin != end) + memmove(str, begin, end - begin + 1); +} + +#ifndef __MINGW32__ +void strlwr(char* s) +{ + while (*s) { + *s = tolower((unsigned char)*s); + ++s; + } +} +#endif + +char* worddigitstr(const char* haystack, const char* needle) +{ + size_t len = strlen(needle); + const char* s; + + if (len == 0) + return NULL; + + for (s = haystack; (s = strstr(s, needle)) != NULL; ++s) { + + /* left boundary */ + if (s == haystack || isspace((unsigned char)s[-1]) || isdigit((unsigned char)s[-1])) { + /* right boundary */ + if (s[len] == '\0' || isspace((unsigned char)s[len]) || isdigit((unsigned char)s[len])) { + return (char *)s; + } + } + } + + return NULL; +} + /****************************************************************************/ /* path */ diff --git a/cmdline/support.h b/cmdline/support.h index f958c7e0..b95bad2f 100644 --- a/cmdline/support.h +++ b/cmdline/support.h @@ -250,6 +250,26 @@ char* strpolish(char* s); */ unsigned strsplit(char** split_map, unsigned split_max, char* line, const char* delimiters); +/** + * Trim spaces from the start and the end + */ +void strtrim(char* s); + +/** + * Lower case + */ +#ifndef __MINGW32__ +void strlwr(char* s); +#endif + +/* + * Find the first occurrence of 'needle' in 'haystack' only if it + * appears as a separate word (space, number or string boundaries). + * + * Returns a pointer to the beginning of the match, or NULL if not found. + */ +char* worddigitstr(const char* haystack, const char* needle); + /****************************************************************************/ /* path */ diff --git a/cmdline/sync.c b/cmdline/sync.c index f45e3da1..d8d6606b 100644 --- a/cmdline/sync.c +++ b/cmdline/sync.c @@ -1298,6 +1298,31 @@ static int state_sync_process(struct snapraid_state* state, struct snapraid_pari /* LCOV_EXCL_STOP */ } + /* thermal control */ + if (state_thermal_alarm(state)) { + /* until now is misc */ + state_usage_misc(state); + + state_progress_stop(state); + + /* before spinning down flush all the caches */ + ret = state_flush(state, &io, parity_handle, blockcur); + if (ret == -1) { + /* LCOV_EXCL_START */ + log_fatal("Stopping at block %u\n", blockcur); + ++error; + goto bail; + /* LCOV_EXCL_STOP */ + } + + state_thermal_cooldown(state); + + state_progress_restart(state); + + /* drop until now */ + state_usage_waste(state); + } + /* autosave */ if ((state->autosave != 0 && autosavedone >= autosavelimit /* if we have reached the limit */ diff --git a/cmdline/thermal.c b/cmdline/thermal.c new file mode 100644 index 00000000..dcf6e131 --- /dev/null +++ b/cmdline/thermal.c @@ -0,0 +1,367 @@ +/* + * Copyright (C) 2025 Andrea Mazzoleni + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "portable.h" + +#include "thermal.h" +#include "state.h" +#include "io.h" + +#include + +struct snapraid_thermal* thermal_alloc(uint64_t dev, const char* name) +{ + struct snapraid_thermal* thermal = malloc_nofail(sizeof(struct snapraid_thermal)); + + thermal->device = dev; + thermal->latest_temperature = 0; + thermal->count = 0; + pathcpy(thermal->name, sizeof(thermal->name), name); + + return thermal; +} + +void thermal_free(struct snapraid_thermal* thermal) +{ + free(thermal); +} + +// Fit exponential heating model to data using least squares +struct snapraid_thermal_params fit_thermal_model(const struct snapraid_thermal_point* points, int n_points, double t_ambient) +{ + struct snapraid_thermal_params model; + double t_steady_try; + double k_try; + + memset(&model, 0, sizeof(model)); + + model.t_ambient = t_ambient; + + /* at least three points to have a result */ + if (n_points < 3) + return model; + + double last_temp = points[n_points - 1].temperature; + + /* iterative refinement to find best k_heat and t_steady */ + double best_error = 1e10; + double best_k = 0; + double best_t_steady = 0; + + /* grid search for parameters */ + for (t_steady_try = last_temp + 2.0; t_steady_try <= last_temp + 25.0; t_steady_try += 0.5) { + for (k_try = 0.00001; k_try <= 0.001; k_try *= 1.2) { + double error = 0.0; + int i; + + /* calculate error for this parameter set */ + for (i = 0; i < n_points; i++) { + double t = points[i].time; + double t_predicted = t_steady_try - (t_steady_try - points[0].temperature) * exp(-k_try * t); + double diff = points[i].temperature - t_predicted; + error += diff * diff; + } + + if (error < best_error) { + best_error = error; + best_k = k_try; + best_t_steady = t_steady_try; + } + } + } + + model.k_heat = best_k; + model.t_steady = best_t_steady; + + /* calculate quality metrics */ + double sum_squared_residuals = 0.0; + double sum_total = 0.0; + double mean_temp = 0.0; + model.max_error = 0.0; + int i; + + /* calculate mean temperature */ + for (i = 0; i < n_points; i++) + mean_temp += points[i].temperature; + mean_temp /= n_points; + + /* calculate R-squared and errors */ + for (i = 0; i < n_points; i++) { + double t = points[i].time; + double t_predicted = model.t_steady - (model.t_steady - points[0].temperature) * exp(-model.k_heat * t); + double residual = points[i].temperature - t_predicted; + + sum_squared_residuals += residual * residual; + sum_total += (points[i].temperature - mean_temp) * (points[i].temperature - mean_temp); + + double abs_error = fabs(residual); + if (abs_error > model.max_error) { + model.max_error = abs_error; + } + } + + model.rmse = sqrt(sum_squared_residuals / n_points); + model.r_squared = 1.0 - (sum_squared_residuals / sum_total); + + return model; +} + +static int smart_temp(devinfo_t* devinfo) +{ + uint64_t t = devinfo->smart[SMART_TEMPERATURE_CELSIUS]; + + /* validate temperature */ + if (t == SMART_UNASSIGNED) + return -1; + if (t == 0) + return -1; + if (t > 100) + return -1; + + return t; +} + +void state_thermal(struct snapraid_state* state, time_t now) +{ + tommy_node* i; + unsigned j; + tommy_list high; + tommy_list low; + int ret; + + if (state->thermal_temperature_limit == 0) + return; + + tommy_list_init(&high); + tommy_list_init(&low); + + /* for all disks */ + for (i = state->disklist; i != 0; i = i->next) { + struct snapraid_disk* disk = i->data; + devinfo_t* entry; + + entry = calloc_nofail(1, sizeof(devinfo_t)); + + entry->device = disk->device; + pathcpy(entry->name, sizeof(entry->name), disk->name); + pathcpy(entry->mount, sizeof(entry->mount), disk->dir); + pathcpy(entry->smartctl, sizeof(entry->smartctl), disk->smartctl); + memcpy(entry->smartignore, disk->smartignore, sizeof(entry->smartignore)); + + tommy_list_insert_tail(&high, &entry->node, entry); + } + + /* for all parities */ + for (j = 0; j < state->level; ++j) { + devinfo_t* entry; + unsigned s; + + for (s = 0; s < state->parity[j].split_mac; ++s) { + entry = calloc_nofail(1, sizeof(devinfo_t)); + + entry->device = state->parity[j].split_map[s].device; + pathcpy(entry->name, sizeof(entry->name), lev_config_name(j)); + pathcpy(entry->mount, sizeof(entry->mount), state->parity[j].split_map[s].path); + pathcpy(entry->smartctl, sizeof(entry->smartctl), state->parity[j].smartctl); + memcpy(entry->smartignore, state->parity[j].smartignore, sizeof(entry->smartignore)); + pathcut(entry->mount); /* remove the parity file */ + + tommy_list_insert_tail(&high, &entry->node, entry); + } + } + + ret = devquery(&high, &low, DEVICE_SMART, 0 /* only disks in the array */); + + /* on error, just disable thermal gathering */ + if (ret != 0) + return; + + /* if the list is empty, it's not supported in this platform */ + if (tommy_list_empty(&low)) + return; + + /* if ambient temperature is not set, set it now with the lowest HD temperature */ + if (state->thermal_ambient_temperature == 0) { + state->thermal_ambient_temperature = ambient_temperature(); + + for (i = tommy_list_head(&low); i != 0; i = i->next) { + devinfo_t* devinfo = i->data; + + int temp = smart_temp(devinfo); + if (temp < 0) + continue; + + log_tag("thermal:ambient:candidate:%d\n", temp); + + if (state->thermal_ambient_temperature == 0 || state->thermal_ambient_temperature > temp) + state->thermal_ambient_temperature = temp; + } + + log_tag("thermal:ambient:final:%d\n", state->thermal_ambient_temperature); + } + + int highest_temperature = 0; + for (i = tommy_list_head(&low); i != 0; i = i->next) { + tommy_node* t; + struct snapraid_thermal* found; + devinfo_t* devinfo = i->data; + unsigned k; + + int temperature = smart_temp(devinfo); + if (temperature < 0) + continue; + + /* search of the entry */ + found = 0; + for (t = tommy_list_head(&state->thermallist); t != 0; t = t->next) { + struct snapraid_thermal* thermal = t->data; + if (thermal->device == devinfo->device) { + found = thermal; + break; + } + } + + /* if not found, create it */ + if (found == 0) { + found = thermal_alloc(devinfo->device, devinfo->name); + tommy_list_insert_tail(&state->thermallist, &found->node, found); + } + + found->latest_temperature = temperature; + + if (highest_temperature < temperature) + highest_temperature = temperature; + + log_tag("thermal:current:%s:%" PRIu64 ":%d\n", devinfo->name, devinfo->device, temperature); + + if (state->thermal_stop_gathering) + continue; + + if (found->count + 1 >= THERMAL_MAX) /* keep one extra space at the end */ + continue; + + /* only monotone increasing temperature */ + if (found->count > 0 && found->data[found->count - 1].temperature >= temperature) + continue; + + /* insert the new data point */ + found->data[found->count].temperature = temperature; + found->data[found->count].time = now - state->thermal_first; + ++found->count; + + log_tag("thermal:heat:%s:%" PRIu64 ":%u:", devinfo->name, devinfo->device, found->count); + for (k = 0; k < found->count; ++k) + log_tag("%s%d/%d", k > 0 ? "," : "", (int)found->data[k].temperature, (int)found->data[k].time); + log_tag("\n"); + + /* estimate parameters */ + found->params = fit_thermal_model(found->data, found->count, state->thermal_ambient_temperature); + + log_tag("thermal:params:%s:%" PRIu64 ":%g:%g:%g:%g:%g:%g\n", devinfo->name, devinfo->device, + found->params.k_heat, found->params.t_ambient, found->params.t_steady, + found->params.rmse, found->params.r_squared, found->params.max_error); + } + + /* always update the highest temperature */ + state->thermal_highest_temperature = highest_temperature; + + log_tag("thermal:highest:%d\n", highest_temperature); + log_flush(); + + tommy_list_foreach(&high, free); + tommy_list_foreach(&low, free); +} + +int state_thermal_alarm(struct snapraid_state* state) +{ + /* if no limit, there is no thermal support */ + if (state->thermal_temperature_limit == 0) + return 0; + + if (state->thermal_highest_temperature <= state->thermal_temperature_limit) + return 0; + + return 1; +} + +void state_thermal_cooldown(struct snapraid_state* state) +{ + unsigned sleep_time = state->thermal_cooldown_time; + + if (sleep_time == 0) + sleep_time = 15 * 60; /* default sleep time */ + if (sleep_time < 5 * 60) + sleep_time = 5 * 60; /* minimum sleep time */ + + /* from now on, stop any further data gathering as the heating is interrupted */ + state->thermal_stop_gathering = 1; + + log_tag("thermal:spindown\n"); + state_device(state, DEVICE_DOWN, 0); + + msg_progress("Cooldown...\n"); + + log_tag("thermal:cooldown:%d\n", sleep_time); + printf("Waiting for %d minutes...\n", sleep_time / 60); + + log_flush(); + + sleep(sleep_time); + + if (!global_interrupt) { /* don't wake-up if we are interrupting */ + log_tag("thermal:spinup\n"); + + /* spinup */ + state_device(state, DEVICE_UP, 0); + + /* log new thermal info */ + state_thermal(state, 0); + } +} + +int state_thermal_begin(struct snapraid_state* state, time_t now) +{ + if (state->thermal_temperature_limit == 0) + return 1; + + /* initial thermal measure */ + state->thermal_first = now; + state->thermal_latest = now; + state_thermal(state, now); + + if (state->thermal_ambient_temperature != 0) { + printf("Ambient temperature %u\n", state->thermal_ambient_temperature); + + if (state->thermal_temperature_limit != 0 && state->thermal_temperature_limit <= state->thermal_ambient_temperature) { + /* LCOV_EXCL_START */ + log_fatal("DANGER! Ambient temperature %d higher than the temperature limit %d. Impossible to proceeed!\n", state->thermal_ambient_temperature, state->thermal_temperature_limit); + log_flush(); + return 0; + /* LCOV_EXCL_STOP */ + } + } + + if (state_thermal_alarm(state)) { + /* LCOV_EXCL_START */ + log_fatal("DANGER! Hard disk temperature %d is already outside the operating range. Impossible to proceeed!\n", state->thermal_highest_temperature); + log_flush(); + return 0; + /* LCOV_EXCL_STOP */ + } + + return 1; +} diff --git a/cmdline/thermal.h b/cmdline/thermal.h new file mode 100644 index 00000000..f40e246e --- /dev/null +++ b/cmdline/thermal.h @@ -0,0 +1,75 @@ +/* + * Copyright (C) 2025 Andrea Mazzoleni + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef __THERMAL_H +#define __THERMAL_H + +#include "elem.h" + +/****************************************************************************/ +/* thermal */ + +/** + * Period of temperature measures + */ +#define THERMAL_PERIOD_SECONDS 60 + +/** + * Max number of temperature measures + */ +#define THERMAL_MAX 100 + +/** + * Minimum value of the R^2 to consider valid a measure + */ +#define THERMAL_R_SQUARED_LIMIT 0.9 + +struct snapraid_thermal_point { + double temperature; /**< Temperatures in celsius */ + double time; /**< Time in seconds */ +}; + +struct snapraid_thermal_params { + double k_heat; /**< Heating coefficient */ + double t_steady; /**< Steady state temperature during heating */ + double t_ambient; /**< Ambient temperature */ + double rmse; /**< Root mean square error of the fit */ + double r_squared; /**< Coefficient of determination */ + double max_error; /**< Maximum absolute error in fit */ +}; + +struct snapraid_thermal { + uint64_t device; /**< Device ID. */ + char name[PATH_MAX]; /**< Name of the disk. Note that it's not unique as more physical disks may map to the same logical disk */ + int latest_temperature; /**< Latest temperature */ + struct snapraid_thermal_point data[THERMAL_MAX]; + unsigned count; /**< Number of measures */ + struct snapraid_thermal_params params; /**< Estimated thermal parameters */ + tommy_node node; /**< Next node in the list. */ +}; + +/** + * Allocate a thermal state. + */ +struct snapraid_thermal* thermal_alloc(uint64_t dev, const char* name); + +/** + * Deallocate a thermal state. + */ +void thermal_free(struct snapraid_thermal* thermal); + +#endif diff --git a/cmdline/unix.c b/cmdline/unix.c index d5a4657e..83eebbf3 100644 --- a/cmdline/unix.c +++ b/cmdline/unix.c @@ -68,6 +68,52 @@ const char* stat_desc(struct stat* st) return "unknown"; } +/** + * Read a file from sys + * + * Return -1 on error, otherwise the data read + */ +#if HAVE_LINUX_DEVICE +static int sysread(const char* path, char* buf, size_t buf_size) +{ + int f; + int ret; + int len; + + f = open(path, O_RDONLY); + if (f == -1) { + /* LCOV_EXCL_START */ + log_fatal("Failed to open '%s'.\n", path); + return -1; + /* LCOV_EXCL_STOP */ + } + + len = read(f, buf, buf_size); + if (len < 0) { + /* LCOV_EXCL_START */ + close(f); + log_fatal("Failed to read '%s'.\n", path); + return -1; + /* LCOV_EXCL_STOP */ + } + + ret = close(f); + if (ret != 0) { + /* LCOV_EXCL_START */ + log_fatal("Failed to close '%s'.\n", path); + return -1; + /* LCOV_EXCL_STOP */ + } + + if (len == (int)buf_size) + buf[buf_size - 1] = 0; + else + buf[len] = 0; + + return len; +} +#endif + /** * Get the device file from the device number. * @@ -248,47 +294,22 @@ static int devdereference(uint64_t device, uint64_t* new_device) #if HAVE_LINUX_DEVICE static int tagread(const char* path, const char* tag, char* value, size_t value_size) { - int f; int ret; - int len; char buf[512]; size_t tag_len; char* i; char* e; - f = open(path, O_RDONLY); - if (f == -1) { - /* LCOV_EXCL_START */ - log_fatal("Failed to open '%s'.\n", path); - return 0; - /* LCOV_EXCL_STOP */ - } - - len = read(f, buf, sizeof(buf)); - if (len < 0) { - /* LCOV_EXCL_START */ - close(f); - log_fatal("Failed to read '%s'.\n", path); + ret = sysread(path, buf, sizeof(buf)); + if (ret < 0) return -1; - /* LCOV_EXCL_STOP */ - } - if (len == sizeof(buf)) { + if (ret == sizeof(buf)) { /* LCOV_EXCL_START */ - close(f); log_fatal("Too long read '%s'.\n", path); return -1; /* LCOV_EXCL_STOP */ } - ret = close(f); - if (ret != 0) { - /* LCOV_EXCL_START */ - log_fatal("Failed to close '%s'.\n", path); - return -1; - /* LCOV_EXCL_STOP */ - } - - buf[len] = 0; tag_len = strlen(tag); for (i = buf; *i; ++i) { @@ -1706,5 +1727,141 @@ size_t direct_size(void) return size; } +#if HAVE_LINUX_DEVICE + +/* List of possible ambient temperature labels */ +const char* AMBIENT_LABEL[] = { + "systin", + "auxtin", + "mb", + "board", + "motherboard", + "system", + "chassis", + "case", + "room", + "ambient", + 0 +}; + +int ambient_temperature(void) +{ + DIR* dir; + struct dirent* entry; + int lowest_temp = 0; + + dir = opendir("/sys/class/hwmon"); + if (!dir) + return 0; + + /* iterate through hwmon devices */ + while ((entry = readdir(dir)) != NULL) { + char path[PATH_MAX]; + DIR* hwmon_dir; + struct dirent* hwmon_entry; + + if (strncmp(entry->d_name, "hwmon", 5) != 0) + continue; + + pathprint(path, sizeof(path), "/sys/class/hwmon/%s", entry->d_name); + + /* iterate through temp*_input files */ + hwmon_dir = opendir(path); + if (!hwmon_dir) + continue; + + while ((hwmon_entry = readdir(hwmon_dir)) != NULL) { + char value[128]; + char name[128]; + char label[128]; + char* dash; + int ret; + char* e; + int temp; + + if (strncmp(hwmon_entry->d_name, "temp", 4) != 0) + continue; + + dash = strrchr(hwmon_entry->d_name, '_'); + if (dash == 0) + continue; + + if (strcmp(dash, "_input") != 0) + continue; + + /* read the temperature */ + pathprint(path, sizeof(path), "/sys/class/hwmon/%s/%s", entry->d_name, hwmon_entry->d_name); + + ret = sysread(path, value, sizeof(value)); + if (ret < 0) + continue; + + temp = strtol(value, &e, 10) / 1000; + if (*e != 0 && !isspace(*e)) + continue; + + /* cut the file name at "_input" */ + *dash = 0; + + /* read the corresponding name */ + pathprint(path, sizeof(path), "/sys/class/hwmon/%s/name", entry->d_name); + ret = sysread(path, name, sizeof(name)); + if (ret < 0) { + /* fallback to using the hwmon entry */ + pathcpy(name, sizeof(name), entry->d_name); + } + + /* read the corresponding label file */ + pathprint(path, sizeof(path), "/sys/class/hwmon/%s/%s_label", entry->d_name, hwmon_entry->d_name); + ret = sysread(path, label, sizeof(label)); + if (ret < 0) { + /* fallback to using the temp* name (e.g., temp1, temp2) */ + pathcpy(label, sizeof(label), hwmon_entry->d_name); + } + + /* trim spaces */ + strtrim(label); + + log_tag("thermal:ambient:device:%s:%s:%s:%s:%d\n", entry->d_name, name, hwmon_entry->d_name, label, temp); + + /* check if temperature is in reasonable range */ + if (temp < 15 || temp > 40) + continue; + + /* lower case */ + strlwr(label); + + /* check if label matches possible ambient labels */ + for (int i = 0; AMBIENT_LABEL[i]; ++i) { + if (worddigitstr(label, AMBIENT_LABEL[i]) != 0) { + log_tag("thermal:ambient:candidate:%d\n", temp); + if (lowest_temp == 0 || lowest_temp > temp) + lowest_temp = temp; + break; + } + } + + /* accept also generic "temp1" */ + if (strcmp(label, "temp1") == 0) { + log_tag("thermal:ambient:candidate:%d\n", temp); + if (lowest_temp == 0 || lowest_temp > temp) + lowest_temp = temp; + } + } + + closedir(hwmon_dir); + } + + closedir(dir); + + return lowest_temp; +} +#else +int ambient_temperature(void) +{ + return 0; +} +#endif + #endif diff --git a/snapraid.1 b/snapraid.1 index 84201ed4..51025809 100644 --- a/snapraid.1 +++ b/snapraid.1 @@ -1448,11 +1448,22 @@ RAM = (8 * 4 * 10^12) * (1+8) / (512 * 2^10) = 0.51 GiB .PD 0 .PD .SS autosave SIZE_IN_GIGABYTES -Automatically save the state when syncing or scrubbing after the specified amount -of GB processed. +Automatically save the state when syncing or scrubbing after the +specified amount of GB processed. This option is useful to avoid to restart from scratch long \[dq]sync\[dq] commands interrupted by a machine crash, or any other event that may interrupt SnapRAID. +.SS temp_limit TEMPERATURE_CELSIUS +Sets the maximum allowed disk temperature in Celsius. When specified, +SnapRAID periodically checks the temperature of all disks using the +smartctl tool. The current disk temperatures are displayed during +processing. If any disk exceeds this limit, all operations stop and +the disks are spun down (put into standby) for the duration defined +by \[dq]temp_sleep\[dq]. +.SS temp_sleep TIME_IN_MINUTES +Sets the standby time, in minutes, when the temperature limit is +reached. During this period, the disks remain spun down. The default +is 15 minutes. .SS pool DIR Defines the pooling directory where the virtual view of the disk array is created using the \[dq]pool\[dq] command. diff --git a/snapraid.conf.example b/snapraid.conf.example index 16c8d23d..ffb01aa8 100644 --- a/snapraid.conf.example +++ b/snapraid.conf.example @@ -71,6 +71,18 @@ exclude /lost+found/ # Format: "autosave SIZE_IN_GB" #autosave 500 +# Set the maximum allowed disk temperature (in Celsius). +# If any disk reaches or exceeds this temperature, +# SnapRAID stops all operations and spins down all disks +# to prevent overheating. +#temp_limit 50 + +# Set the standby duration (in minutes) after a temperature event. +# During this period, disks remain spun down to cool down +# before SnapRAID automatically resumes operations. +# Default is 15 minutes if not specified. +#temp_sleep 10 + # Defines the pooling directory where the virtual view of the disk # array is created using the "pool" command (uncomment to enable). # The files are not really copied here, but just linked using diff --git a/snapraid.conf.example.windows b/snapraid.conf.example.windows index 6d4f563c..140cd680 100644 --- a/snapraid.conf.example.windows +++ b/snapraid.conf.example.windows @@ -75,6 +75,18 @@ exclude \Windows\ # Format: "autosave SIZE_IN_GB" #autosave 500 +# Set the maximum allowed disk temperature (in Celsius). +# If any disk reaches or exceeds this temperature, +# SnapRAID stops all operations and spins down all disks +# to prevent overheating. +#temp_limit 50 + +# Set the standby duration (in minutes) after a temperature event. +# During this period, disks remain spun down to cool down +# before SnapRAID automatically resumes operations. +# Default is 15 minutes if not specified. +#temp_sleep 10 + # Defines the pooling directory where the virtual view of the disk # array is created using the "pool" command (uncomment to enable). # The files are not really copied here, but just linked using diff --git a/snapraid.d b/snapraid.d index 11b3f290..47fb53ef 100644 --- a/snapraid.d +++ b/snapraid.d @@ -1166,12 +1166,25 @@ Configuration :RAM = (8 * 4 * 10^12) * (1+8) / (512 * 2^10) = 0.51 GiB autosave SIZE_IN_GIGABYTES - Automatically save the state when syncing or scrubbing after the specified amount - of GB processed. + Automatically save the state when syncing or scrubbing after the + specified amount of GB processed. This option is useful to avoid to restart from scratch long "sync" commands interrupted by a machine crash, or any other event that may interrupt SnapRAID. + temp_limit TEMPERATURE_CELSIUS + Sets the maximum allowed disk temperature in Celsius. When specified, + SnapRAID periodically checks the temperature of all disks using the + smartctl tool. The current disk temperatures are displayed during + processing. If any disk exceeds this limit, all operations stop and + the disks are spun down (put into standby) for the duration defined + by "temp_sleep". + + temp_sleep TIME_IN_MINUTES + Sets the standby time, in minutes, when the temperature limit is + reached. During this period, the disks remain spun down. The default + is 15 minutes. + pool DIR Defines the pooling directory where the virtual view of the disk array is created using the "pool" command. diff --git a/snapraid.txt b/snapraid.txt index b639618d..2f3b9345 100644 --- a/snapraid.txt +++ b/snapraid.txt @@ -12,7 +12,7 @@ snapraid [-c, --conf CONFIG] [-a, --audit-only] [-h, --pre-hash] [-i, --import DIR] [-p, --plan PERC|bad|new|full] [-o, --older-than DAYS] [-l, --log FILE] - [-w, --bw-limit RATE] + [-s, --spin-down-on-error] [-w, --bw-limit RATE] [-Z, --force-zero] [-E, --force-empty] [-U, --force-uuid] [-D, --force-device] [-N, --force-nocopy] [-F, --force-full] @@ -20,8 +20,8 @@ snapraid [-c, --conf CONFIG] [-S, --start BLKSTART] [-B, --count BLKCOUNT] [-L, --error-limit NUMBER] [-v, --verbose] [-q, --quiet] - status|smart|up|down|diff|sync|scrub|fix|check|list|dup - |pool|devices|touch|rehash + status|smart|probe|up|down|diff|sync|scrub|fix|check + |list|dup|pool|devices|touch|rehash snapraid [-V, --version] [-H, --help] [-C, --gen-conf CONTENT] @@ -403,7 +403,7 @@ If everything is recovered, this command is immediate. SnapRAID provides a few simple commands that allow to: * Prints the status of the array -> "status" -* Controls the disks -> "smart", "up", "down" +* Controls the disks -> "smart", "probe", "up", "down" * Makes a backup/snapshot -> "sync" * Periodically checks data -> "scrub" * Restore the last backup/snapshot -> "fix". @@ -434,7 +434,7 @@ Nothing is modified. 5.2 smart --------- -Prints a SMART report of all the disks of the array. +Prints a SMART report of all the disks in the system. It includes an estimation of the probability of failure in the next year allowing to plan maintenance replacements of the disks that show @@ -459,16 +459,33 @@ If the -v, --verbose option is specified a deeper statistical analysis is provided. This analysis can help you to decide if you need more or less parity. -This command uses the "smartctl" tool, and it's equivalent to run -"smartctl -a" on all the devices. +This command uses the "smartctl" tool and is equivalent to running +"smartctl -a" on all devices. -If your devices are not auto-detected correctly, you can configure +If your devices are not auto-detected correctly, you can specify a custom command using the "smartctl" option in the configuration file. Nothing is modified. -5.3 up +5.3 probe +--------- + +Prints the POWER state of all disks in the system. + +"Standby" means the disk is not spinning. "Active" means +the disk is spinning. + +This command uses the "smartctl" tool and is equivalent to running +"smartctl -n standby -i" on all devices. + +If your devices are not auto-detected correctly, you can specify +a custom command using the "smartctl" option in the configuration +file. + +Nothing is modified. + +5.4 up ------ Spins up all the disks of the array. @@ -480,7 +497,7 @@ Ensure that your power-supply can sustain that. Nothing is modified. -5.4 down +5.5 down -------- Spins down all the disks of the array. @@ -488,11 +505,16 @@ Spins down all the disks of the array. This command uses the "smartctl" tool, and it's equivalent to run "smartctl -s standby,now" on all the devices. -You can spin-down only some specific disks using the -d, --filter-disk option. +You can spin-down only some specific disks using the -d, --filter-disk +option. + +To automatically spin down on error, you can use the option +-s, --spin-down-on-error with any other command, which is equivalent +to running "down" manually when an error occurs. Nothing is modified. -5.5 diff +5.6 diff -------- Lists all the files modified from the last "sync" that need to have @@ -525,7 +547,7 @@ default 0. The return code 1 is instead for a generic error condition. Nothing is modified. -5.6 sync +5.7 sync -------- Updates the parity information. All the modified files @@ -552,7 +574,7 @@ but the previously computed hash information is kept. The "content" and "parity" files are modified if necessary. The files in the array are NOT modified. -5.7 scrub +5.8 scrub --------- Scrubs the array, checking for silent or input/output errors in data @@ -597,7 +619,7 @@ of each block, and to mark bad blocks. The "parity" files are NOT modified. The files in the array are NOT modified. -5.8 fix +5.9 fix ------- Fix all the files and the parity data. @@ -634,8 +656,8 @@ The "content" file is NOT modified. The "parity" files are modified if necessary. The files in the array are modified if necessary. -5.9 check ---------- +5.10 check +---------- Verify all the files and the parity data. @@ -654,7 +676,7 @@ Files are identified only by path, and not by inode. Nothing is modified. -5.10 list +5.11 list --------- Lists all the files contained in the array at the time of the @@ -662,7 +684,7 @@ last "sync". Nothing is modified. -5.11 dup +5.12 dup -------- Lists all the duplicate files. Two files are assumed equal if their @@ -671,7 +693,7 @@ pre-computed hashes are used. Nothing is modified. -5.12 pool +5.13 pool --------- Creates or updates in the "pooling" directory a virtual view of all @@ -686,7 +708,7 @@ view of the array. Any other regular file is left in place. Nothing is modified outside the pool directory. -5.13 devices +5.14 devices ------------ Prints the low level devices used by the array. @@ -704,7 +726,7 @@ low level devices used by a single disk in the array. Nothing is modified. -5.14 touch +5.15 touch ---------- Sets arbitrarily the sub-second time-stamp of all the files @@ -723,7 +745,7 @@ size and time-stamp. Note that the second precision time-stamp is not modified, and all the dates and times of your files will be maintained. -5.15 rehash +5.16 rehash ----------- Schedules a rehash of the whole array. @@ -846,6 +868,14 @@ SnapRAID provides the following options: identified regardless of their name. This option can be used only with "check" and "fix". + -s, --spin-down-on-error + On any error, spin down all managed disks before exiting with + a non-zero status code. This prevents the drives from + remaining active and spinning after an aborted operation, + helping to avoid unnecessary heat build-up and power + consumption. Use this option to ensure disks are safely + stopped even when a command fails. + -w, --bw-limit RATE Applies a global bandwidth limit for all disks. The RATE is the number of bytes per second. You can specify a multiplier @@ -1218,13 +1248,30 @@ RAM = (8 * 4 * 10^12) * (1+8) / (512 * 2^10) = 0.51 GiB 7.10 autosave SIZE_IN_GIGABYTES ------------------------------- -Automatically save the state when syncing or scrubbing after the specified amount -of GB processed. +Automatically save the state when syncing or scrubbing after the +specified amount of GB processed. This option is useful to avoid to restart from scratch long "sync" commands interrupted by a machine crash, or any other event that may interrupt SnapRAID. -7.11 pool DIR +7.11 temp_limit TEMPERATURE_CELSIUS +----------------------------------- + +Sets the maximum allowed disk temperature in Celsius. When specified, +SnapRAID periodically checks the temperature of all disks using the +smartctl tool. The current disk temperatures are displayed during +processing. If any disk exceeds this limit, all operations stop and +the disks are spun down (put into standby) for the duration defined +by "temp_sleep". + +7.12 temp_sleep TIME_IN_MINUTES +------------------------------- + +Sets the standby time, in minutes, when the temperature limit is +reached. During this period, the disks remain spun down. The default +is 15 minutes. + +7.13 pool DIR ------------- Defines the pooling directory where the virtual view of the disk @@ -1232,7 +1279,7 @@ array is created using the "pool" command. The directory must already exist. -7.12 share UNC_DIR +7.14 share UNC_DIR ------------------ Defines the Windows UNC path required to access the disks remotely. @@ -1248,26 +1295,30 @@ file dir and name. This option is only required for Windows. -7.13 smartctl DISK/PARITY OPTIONS... +7.15 smartctl DISK/PARITY OPTIONS... ------------------------------------ -Defines a custom smartctl command to obtain the SMART attributes -for each disk. This may be required for RAID controllers and for -some USB disk that cannot be auto-detected. +Defines a custom smartctl options to obtain the SMART attributes for +each disk. This may be required for RAID controllers and for some USB +disk that cannot be auto-detected. DISK is the same disk name specified in the "disk" option. PARITY is one of the parity name as "parity,(1,2,3,4,5,6,z)-parity". In the specified OPTIONS, the "%s" string is replaced by the -device name. Note that in case of RAID controllers the device is likely -fixed, and you don't have to use "%s". +device name. Note that in case of RAID controllers the device is +likely fixed, and you don't have to use "%s". Refers at the smartmontools documentation about the possible options: https://www.smartmontools.org/wiki/Supported_RAID-Controllers https://www.smartmontools.org/wiki/Supported_USB-Devices -7.14 smartignore DISK/PARITY ATTR [ATTR...] +For example: + + smartctl parity -d sat %s + +7.16 smartignore DISK/PARITY ATTR [ATTR...] ------------------------------------------- Ignores the specified SMART attribute when computing the probability @@ -1286,7 +1337,7 @@ To ignore it only on the first parity disk: smartignore parity 193 -7.15 Examples +7.17 Examples ------------- An example of a typical configuration for Unix is: @@ -1303,6 +1354,7 @@ An example of a typical configuration for Unix is: smartctl d2 -d usbjmicron %s smartctl parity -d areca,1/1 /dev/sg0 smartctl 2-parity -d areca,2/1 /dev/sg0 + smartignore * 193 An example of a typical configuration for Windows is: @@ -1319,6 +1371,7 @@ An example of a typical configuration for Windows is: smartctl d2 -d usbjmicron %s smartctl parity -d areca,1/1 /dev/arcmsr0 smartctl 2-parity -d areca,2/1 /dev/arcmsr0 + smartignore * 193 8 PATTERN