From f4f9cd5bc1b390221c39f12b5eb45f9f01016d61 Mon Sep 17 00:00:00 2001 From: Zen Dodd Date: Fri, 23 Oct 2026 23:30:34 +1000 Subject: [PATCH 1/2] options: add --nice process-priority controls --- Makefile.in | 2 +- NEWS.md | 4 + clientserver.c | 2 + configure.ac | 4 +- main.c | 2 + niceness.c | 472 +++++++++++++++++++++++++++++++++++++ options.c | 41 +++- packaging/cull-options | 5 +- pipe.c | 1 + rsync.1.md | 41 ++++ support/rrsync | 9 +- support/rrsync.1.md | 4 + testsuite/COVERAGE.md | 1 + testsuite/niceness_test.py | 331 ++++++++++++++++++++++++++ 14 files changed, 913 insertions(+), 6 deletions(-) create mode 100644 niceness.c create mode 100644 testsuite/niceness_test.py diff --git a/Makefile.in b/Makefile.in index 716171db1..798a975cb 100644 --- a/Makefile.in +++ b/Makefile.in @@ -49,7 +49,7 @@ OBJS1_NO_MAIN=flist.o rsync.o generator.o receiver.o cleanup.o sender.o exclude. OBJS1=$(OBJS1_NO_MAIN) main.o OBJS2=options.o io.o compat.o hlink.o token.o uidlist.o socket.o hashtable.o \ usage.o fileio.o batch.o clientname.o chmod.o acls.o xattrs.o -OBJS3=progress.o pipe.o @MD5_ASM@ @ROLL_SIMD@ @ROLL_ASM@ +OBJS3=progress.o pipe.o niceness.o @MD5_ASM@ @ROLL_SIMD@ @ROLL_ASM@ DAEMON_OBJ = params.o loadparm.o clientserver.o access.o connection.o authenticate.o popt_OBJS= popt/popt.o popt/poptconfig.o \ popt/popthelp.o popt/poptparse.o popt/poptint.o diff --git a/NEWS.md b/NEWS.md index 0ef793898..c77588c78 100644 --- a/NEWS.md +++ b/NEWS.md @@ -12,6 +12,9 @@ excludes. ### ENHANCEMENTS: +- Added `--nice` to adjust CPU and I/O scheduling priorities for local and + remote rsync processes. Daemon clients and rrsync cannot raise server-side + priorities. - Human-readable output now uses dynamic precision to show three significant digits and appends `i` to binary unit suffixes. `--progress` uses the same formatter for transfer rates and labels byte counts and rates with `B` and @@ -73,6 +76,7 @@ test improvements. - Updated platform tests and fleet-test coverage for the 3.5.0 fixes. ------------------------------------------------------------------------------ + # NEWS for rsync 3.5.0 (13 Aug 2026) ## Changes in this version: diff --git a/clientserver.c b/clientserver.c index 42a31ee29..2034ea405 100644 --- a/clientserver.c +++ b/clientserver.c @@ -1442,6 +1442,7 @@ int start_daemon(int f_in, int f_out) * (when rsync is run by init and run by a remote shell). */ if (!load_config(0)) exit_cleanup(RERR_SYNTAX); + niceness_apply(); /* Bound the handshake before ANY peer input is read -- the PROXY-protocol * header below is peer-supplied too, and was previously unbounded. An @@ -1802,6 +1803,7 @@ int daemon_main(void) exit_cleanup(RERR_SYNTAX); } set_dparams(0); + niceness_apply(); /* "proxy protocol = true" with no trusted-proxy list rejects every * connection as an untrusted proxy peer (fail-closed). That is intended, diff --git a/configure.ac b/configure.ac index 58a5cac59..2434aeaf6 100644 --- a/configure.ac +++ b/configure.ac @@ -13,7 +13,7 @@ AC_CHECK_HEADERS(poll.h sys/fcntl.h sys/select.h fcntl.h sys/time.h sys/unistd.h sys/acl.h acl/libacl.h attr/xattr.h sys/xattr.h sys/extattr.h dl.h \ popt.h popt/popt.h linux/falloc.h netinet/in_systm.h netgroup.h \ zlib.h xxhash.h openssl/md4.h openssl/md5.h zstd.h lz4.h sys/file.h \ - sys/resource.h bsd/string.h idn2.h wchar.h) + sys/resource.h bsd/string.h idn2.h wchar.h linux/ioprio.h sys/syscall.h) AC_CHECK_HEADERS([netinet/ip.h], [], [], [[#include ]]) AC_HEADER_MAJOR_FIXED @@ -978,7 +978,7 @@ AC_CHECK_FUNCS(waitpid wait4 getcwd chown chmod lchmod mknod mkfifo fdopendir \ seteuid strerror putenv iconv_open locale_charset nl_langinfo getxattr \ extattr_get_link sigaction sigprocmask setattrlist getgrouplist \ initgroups utimensat futimens posix_fallocate attropen setvbuf nanosleep usleep \ - setenv unsetenv mbrtowc) + setenv unsetenv mbrtowc getpriority setpriority) dnl dirfd() is a macro or static inline on several systems (the BSDs), so the dnl default AC_CHECK_FUNCS link probe -- which declares `char dirfd(void);` and diff --git a/main.c b/main.c index 594af0090..61c2ade65 100644 --- a/main.c +++ b/main.c @@ -1309,6 +1309,7 @@ void start_server(int f_in, int f_out, int argc, char *argv[]) if (protocol_version >= 23) io_start_multiplex_out(f_out); + niceness_apply(); if (am_daemon && io_timeout && protocol_version >= 31) send_msg_int(MSG_IO_TIMEOUT, io_timeout); @@ -1344,6 +1345,7 @@ int client_run(int f_in, int f_out, pid_t pid, int argc, char *argv[]) int exit_code = 0, exit_code2 = 0; char *local_name = NULL; + niceness_apply(); cleanup_child_pid = pid; if (!read_batch) { set_nonblocking(f_in); diff --git a/niceness.c b/niceness.c new file mode 100644 index 000000000..99ef27002 --- /dev/null +++ b/niceness.c @@ -0,0 +1,472 @@ +#include "rsync.h" + +#ifdef HAVE_SYS_RESOURCE_H +#include +#endif +#ifdef HAVE_LINUX_IOPRIO_H +#include +#endif +#ifdef HAVE_SYS_SYSCALL_H +#include +#endif + +extern int am_server; +extern int am_daemon; + +#define PRIORITY_UNSET (-100) +#define DEFAULT_NICE 19 + +#if defined HAVE_GETPRIORITY && defined HAVE_SYS_RESOURCE_H +#define SUPPORT_GET_PRIORITY 1 +#endif + +#if defined SUPPORT_GET_PRIORITY && defined HAVE_SETPRIORITY +#define SUPPORT_NICE_PRIORITY 1 +#endif + +#if defined HAVE_LINUX_IOPRIO_H && defined HAVE_SYS_SYSCALL_H \ + && defined SUPPORT_GET_PRIORITY \ + && defined SYS_ioprio_get && defined SYS_ioprio_set \ + && defined IOPRIO_PRIO_CLASS \ + && defined IOPRIO_PRIO_DATA && defined IOPRIO_PRIO_VALUE +#define SUPPORT_IO_PRIORITY 1 +#endif + +#ifdef SUPPORT_IO_PRIORITY +#ifdef IOPRIO_PRIO_LEVEL +#define RSYNC_IOPRIO_LEVEL(value) IOPRIO_PRIO_LEVEL(value) +#else +/* Older headers expose the whole data field; the level is its low 3 bits. */ +#define RSYNC_IOPRIO_LEVEL(value) (IOPRIO_PRIO_DATA(value) & 7) +#endif +#endif + +enum io_priority_class { + IO_PRIORITY_NONE, + IO_PRIORITY_REALTIME, + IO_PRIORITY_BEST_EFFORT, + IO_PRIORITY_IDLE +}; + +struct priority_setting { + int cpu; + int io_class; + int io_data; +}; + +struct priority_options { + struct priority_setting local; + struct priority_setting remote; +}; + +static struct priority_options priorities = { + { PRIORITY_UNSET, PRIORITY_UNSET, 0 }, + { PRIORITY_UNSET, PRIORITY_UNSET, 0 } +}; + +static void clear_setting(struct priority_setting *setting) +{ + setting->cpu = PRIORITY_UNSET; + setting->io_class = PRIORITY_UNSET; + setting->io_data = 0; +} + +void niceness_reset(void) +{ + clear_setting(&priorities.local); + clear_setting(&priorities.remote); +} + +void niceness_use_remote(void) +{ + priorities.local = priorities.remote; + clear_setting(&priorities.remote); +} + +static void default_setting(struct priority_setting *setting) +{ + setting->cpu = DEFAULT_NICE; + setting->io_class = IO_PRIORITY_IDLE; + setting->io_data = 0; +} + +static uchar ascii_lower(uchar ch) +{ + return ch >= 'A' && ch <= 'Z' ? ch + ('a' - 'A') : ch; +} + +static int token_equal(const char *str, size_t len, const char *word) +{ + size_t i; + + if (strlen(word) != len) + return 0; + for (i = 0; i < len; i++) { + if (ascii_lower((uchar)str[i]) != ascii_lower((uchar)word[i])) + return 0; + } + return 1; +} + +static int parse_io_priority(const char *str, size_t len, int *class_p, int *data_p) +{ + if (token_equal(str, len, "none")) + *class_p = IO_PRIORITY_NONE; + else if (token_equal(str, len, "idle")) + *class_p = IO_PRIORITY_IDLE; + else if (len == 4 && str[2] == '_' && str[3] >= '0' && str[3] <= '7' + && (token_equal(str, 2, "rt") || token_equal(str, 2, "be"))) { + *class_p = ascii_lower((uchar)str[0]) == 'r' + ? IO_PRIORITY_REALTIME : IO_PRIORITY_BEST_EFFORT; + *data_p = str[3] - '0'; + return 1; + } else + return 0; + + *data_p = 0; + return 1; +} + +static int parse_priority(const char *str, const char *end, struct priority_setting *setting) +{ + char *num_end; + long value; + + clear_setting(setting); + if (parse_io_priority(str, end - str, &setting->io_class, &setting->io_data)) + return 1; + if (!isdigit((uchar)*str) + && !((*str == '+' || *str == '-') && str + 1 < end + && isdigit((uchar)str[1]))) + return 0; + + errno = 0; + value = strtol(str, &num_end, 10); + if (errno == ERANGE || num_end == str || num_end > end || value < -20 || value > 19) + return 0; + setting->cpu = (int)value; + if (num_end == end) + return 1; + if (*num_end++ != '/' || num_end == end) + return 0; + return parse_io_priority(num_end, end - num_end, + &setting->io_class, &setting->io_data); +} + +static int parse_target(const char *str, size_t len, int *local_p, int *remote_p) +{ + if (token_equal(str, len, "local")) { + *local_p = 1; + *remote_p = 0; + } else if (token_equal(str, len, "remote")) { + *local_p = 0; + *remote_p = 1; + } else if (token_equal(str, len, "all")) + *local_p = *remote_p = 1; + else + return 0; + return 1; +} + +static void merge_setting(struct priority_setting *dest, const struct priority_setting *src) +{ + if (src->cpu != PRIORITY_UNSET) + dest->cpu = src->cpu; + if (src->io_class != PRIORITY_UNSET) { + dest->io_class = src->io_class; + dest->io_data = src->io_data; + } +} + +int niceness_parse(const char *arg) +{ + struct priority_options parsed = priorities; + const char *str = arg; + + if (!str || !*str) + return 0; + + while (*str) { + struct priority_setting setting; + const char *end = strchr(str, ','); + const char *colon; + int local = 1, remote = 1; + + if (!end) + end = str + strlen(str); + if (end == str) + return 0; + + colon = memchr(str, ':', end - str); + if (colon) { + if (!parse_target(str, colon - str, &local, &remote) + || !parse_priority(colon + 1, end, &setting)) + return 0; + } else if (parse_target(str, end - str, &local, &remote)) + default_setting(&setting); + else if (!parse_priority(str, end, &setting)) + return 0; + + if (local) + merge_setting(&parsed.local, &setting); + if (remote) + merge_setting(&parsed.remote, &setting); + if (*end) { + str = end + 1; + if (!*str) + return 0; + } else + str = end; + } + + priorities = parsed; + return 1; +} + +static const char *io_priority_name(int class, int data, char *buf, size_t len) +{ + switch (class) { + case IO_PRIORITY_NONE: + return "none"; + case IO_PRIORITY_REALTIME: + snprintf(buf, len, "rt_%d", data); + return buf; + case IO_PRIORITY_BEST_EFFORT: + snprintf(buf, len, "be_%d", data); + return buf; + case IO_PRIORITY_IDLE: + return "idle"; + } + return "unknown"; +} + +char *niceness_remote_option(void) +{ + struct priority_setting *setting = &priorities.remote; + char *arg; + char io_buf[5]; + const char *io_name; + int ret; + + if (setting->cpu == PRIORITY_UNSET && setting->io_class == PRIORITY_UNSET) + return NULL; + if (setting->io_class != PRIORITY_UNSET) + io_name = io_priority_name(setting->io_class, setting->io_data, + io_buf, sizeof io_buf); + else + io_name = NULL; + if (setting->cpu != PRIORITY_UNSET && setting->io_class != PRIORITY_UNSET) + ret = asprintf(&arg, "--nice=local:%d/%s", setting->cpu, io_name); + else if (setting->cpu != PRIORITY_UNSET) + ret = asprintf(&arg, "--nice=local:%d", setting->cpu); + else + ret = asprintf(&arg, "--nice=local:%s", io_name); + if (ret < 0) + out_of_memory("niceness_remote_option"); + return arg; +} + +#ifdef SUPPORT_IO_PRIORITY +static int effective_io_priority(int class, int data, int cpu, int *rank_p, int *data_p) +{ + if (class == IOPRIO_CLASS_NONE) { + class = IOPRIO_CLASS_BE; + data = (cpu + 20) / 5; + if (data > 7) + data = 7; + } + + switch (class) { + case IOPRIO_CLASS_RT: + *rank_p = 0; + break; + case IOPRIO_CLASS_BE: + *rank_p = 1; + break; + case IOPRIO_CLASS_IDLE: + *rank_p = 2; + data = 0; + break; + default: + return 0; + } + *data_p = data; + return 1; +} + +static int requested_io_priority(const struct priority_setting *setting, int cpu, + int *rank_p, int *data_p) +{ + int class; + + switch (setting->io_class) { + case IO_PRIORITY_NONE: + class = IOPRIO_CLASS_NONE; + break; + case IO_PRIORITY_REALTIME: + class = IOPRIO_CLASS_RT; + break; + case IO_PRIORITY_BEST_EFFORT: + class = IOPRIO_CLASS_BE; + break; + case IO_PRIORITY_IDLE: + class = IOPRIO_CLASS_IDLE; + break; + default: + return 0; + } + return effective_io_priority(class, setting->io_data, cpu, rank_p, data_p); +} +#endif + +int niceness_check_daemon_request(char *err_buf, size_t err_len) +{ + struct priority_setting *setting = &priorities.local; + + if (setting->cpu == PRIORITY_UNSET && setting->io_class == PRIORITY_UNSET) + return 1; + +#if defined SUPPORT_NICE_PRIORITY || defined SUPPORT_IO_PRIORITY + int current_cpu; + + errno = 0; + current_cpu = getpriority(PRIO_PROCESS, 0); + if (errno) { + snprintf(err_buf, err_len, "cannot inspect daemon process priority: %s\n", + strerror(errno)); + return 0; + } +#endif + +#ifdef SUPPORT_NICE_PRIORITY + if (setting->cpu != PRIORITY_UNSET && setting->cpu < current_cpu) { + snprintf(err_buf, err_len, "--nice cannot raise daemon CPU priority\n"); + return 0; + } +#endif + +#ifdef SUPPORT_IO_PRIORITY + if (setting->io_class != PRIORITY_UNSET) { + int raw = syscall(SYS_ioprio_get, IOPRIO_WHO_PROCESS, 0); + int requested_cpu = current_cpu; + int current_rank, current_data, requested_rank, requested_data; + + if (raw < 0) { + snprintf(err_buf, err_len, "cannot inspect daemon I/O priority: %s\n", + strerror(errno)); + return 0; + } +#ifdef SUPPORT_NICE_PRIORITY + if (setting->cpu != PRIORITY_UNSET) + requested_cpu = setting->cpu; +#endif + /* IOPRIO_CLASS_NONE derives its level from CPU nice. */ + if (!effective_io_priority(IOPRIO_PRIO_CLASS(raw), RSYNC_IOPRIO_LEVEL(raw), + current_cpu, ¤t_rank, ¤t_data) + || !requested_io_priority(setting, requested_cpu, + &requested_rank, &requested_data)) { + snprintf(err_buf, err_len, "unsupported daemon I/O priority class\n"); + return 0; + } + if (requested_rank < current_rank + || (requested_rank == current_rank && requested_data < current_data)) { + snprintf(err_buf, err_len, "--nice cannot raise daemon I/O priority\n"); + return 0; + } + } +#endif + +#if !defined SUPPORT_NICE_PRIORITY && !defined SUPPORT_IO_PRIORITY + (void)err_buf; + (void)err_len; +#endif + + return 1; +} + +static const char *process_role(void) +{ + return am_daemon ? "daemon" : am_server ? "server" : "client"; +} + +static int apply_cpu_priority(int priority) +{ +#ifdef SUPPORT_NICE_PRIORITY + if (setpriority(PRIO_PROCESS, 0, priority) < 0) { + rsyserr(FWARNING, errno, "failed to set %s CPU priority to %d", + process_role(), priority); + return 0; + } + if (DEBUG_GTE(CMD, 1)) + rprintf(FINFO, "set %s CPU priority to %d\n", process_role(), priority); + return 1; +#else + rprintf(FWARNING, + "cannot set %s CPU priority to %d: " + "operation is not supported on this system\n", + process_role(), priority); + return 0; +#endif +} + +static void apply_io_priority(int class, int data) +{ + char name_buf[5]; + const char *name = io_priority_name(class, data, name_buf, sizeof name_buf); + +#ifdef SUPPORT_IO_PRIORITY + int kernel_class; + + switch (class) { + case IO_PRIORITY_NONE: + kernel_class = IOPRIO_CLASS_NONE; + break; + case IO_PRIORITY_REALTIME: + kernel_class = IOPRIO_CLASS_RT; + break; + case IO_PRIORITY_BEST_EFFORT: + kernel_class = IOPRIO_CLASS_BE; + break; + case IO_PRIORITY_IDLE: + kernel_class = IOPRIO_CLASS_IDLE; + break; + default: + return; + } + if (syscall(SYS_ioprio_set, IOPRIO_WHO_PROCESS, 0, + IOPRIO_PRIO_VALUE(kernel_class, data)) < 0) + rsyserr(FWARNING, errno, "failed to set %s I/O priority to %s", + process_role(), name); + else if (DEBUG_GTE(CMD, 1)) + rprintf(FINFO, "set %s I/O priority to %s\n", process_role(), name); +#else + rprintf(FWARNING, + "cannot set %s I/O priority to %s: " + "operation is not supported on this system\n", + process_role(), name); +#endif +} + +void niceness_apply(void) +{ + struct priority_setting setting = priorities.local; + int cpu_applied = 1; + + clear_setting(&priorities.local); + if (setting.cpu != PRIORITY_UNSET) + cpu_applied = apply_cpu_priority(setting.cpu); +#ifndef SUPPORT_NICE_PRIORITY + (void)cpu_applied; +#endif + if (setting.io_class != PRIORITY_UNSET) { +#ifdef SUPPORT_NICE_PRIORITY + if (am_daemon && setting.cpu != PRIORITY_UNSET + && setting.io_class == IO_PRIORITY_NONE && !cpu_applied) { + rprintf(FWARNING, + "not resetting daemon I/O priority because the CPU priority change failed\n"); + return; + } +#endif + apply_io_priority(setting.io_class, setting.io_data); + } +} diff --git a/options.c b/options.c index 90e94bb7c..d95341b2b 100644 --- a/options.c +++ b/options.c @@ -604,7 +604,7 @@ enum {OPT_SERVER = 1000, OPT_DAEMON, OPT_SENDER, OPT_EXCLUDE, OPT_EXCLUDE_FROM, OPT_NO_D, OPT_APPEND, OPT_NO_ICONV, OPT_INFO, OPT_DEBUG, OPT_BLOCK_SIZE, OPT_USERMAP, OPT_GROUPMAP, OPT_CHOWN, OPT_BWLIMIT, OPT_STDERR, OPT_OLD_COMPRESS, OPT_NEW_COMPRESS, OPT_NO_COMPRESS, OPT_OLD_ARGS, - OPT_STOP_AFTER, OPT_STOP_AT, + OPT_STOP_AFTER, OPT_STOP_AT, OPT_NICE, OPT_NO_NICE, OPT_REFUSED_BASE = 9000}; static struct poptOption long_options[] = { @@ -862,6 +862,8 @@ static struct poptOption long_options[] = { {"remote-option", 'M', POPT_ARG_STRING, 0, 'M', 0, 0 }, {"protocol", 0, POPT_ARG_INT, &protocol_version, 0, 0, 0 }, {"checksum-seed", 0, POPT_ARG_INT, &checksum_seed, 0, 0, 0 }, + {"nice", 0, POPT_ARG_STRING, 0, OPT_NICE, 0, 0 }, + {"no-nice", 0, POPT_ARG_NONE, 0, OPT_NO_NICE, 0, 0 }, {"server", 0, POPT_ARG_NONE, 0, OPT_SERVER, 0, 0 }, {"sender", 0, POPT_ARG_NONE, 0, OPT_SENDER, 0, 0 }, /* All the following options switch us into daemon-mode option-parsing. */ @@ -889,6 +891,8 @@ static const struct poptOption long_daemon_options[] = { {"port", 0, POPT_ARG_INT, &rsync_port, 0, 0, 0 }, {"sockopts", 0, POPT_ARG_STRING, &sockopts, 0, 0, 0 }, {"protocol", 0, POPT_ARG_INT, &protocol_version, 0, 0, 0 }, + {"nice", 0, POPT_ARG_STRING, 0, OPT_NICE, 0, 0 }, + {"no-nice", 0, POPT_ARG_NONE, 0, OPT_NO_NICE, 0, 0 }, {"server", 0, POPT_ARG_NONE, &am_server, 0, 0, 0 }, {"temp-dir", 'T', POPT_ARG_STRING, &tmpdir, 0, 0, 0 }, {"verbose", 'v', POPT_ARG_NONE, 0, 'v', 0, 0 }, @@ -901,6 +905,17 @@ static const struct poptOption long_daemon_options[] = { static char err_buf[200]; +static int parse_niceness_option(poptContext pc) +{ + const char *arg = poptGetOptArg(pc); + + if (niceness_parse(arg)) + return 1; + snprintf(err_buf, sizeof err_buf, + "Invalid argument passed to --nice (%s)\n", arg); + return 0; +} + /** * Store the option error message, if any, so that we can log the @@ -1574,6 +1589,15 @@ int parse_arguments(int *argc_p, const char ***argv_p) verbose++; break; + case OPT_NICE: + if (!parse_niceness_option(pc)) + goto cleanup; + break; + + case OPT_NO_NICE: + niceness_reset(); + break; + default: rprintf(FERROR, "rsync: %s: %s (in daemon mode)\n", @@ -1714,6 +1738,15 @@ int parse_arguments(int *argc_p, const char ***argv_p) quiet++; break; + case OPT_NICE: + if (!parse_niceness_option(pc)) + goto cleanup; + break; + + case OPT_NO_NICE: + niceness_reset(); + break; + case 'x': one_file_system++; break; @@ -2219,6 +2252,9 @@ int parse_arguments(int *argc_p, const char ***argv_p) poptFreeContext(pc); return 1; } + if (am_daemon && module_id >= 0 + && !niceness_check_daemon_request(err_buf, sizeof err_buf)) + goto cleanup; /* Because popt 1.19 has started to free the returned args data, we now * make a copy of the array and then do an immediate cleanup. */ @@ -3199,6 +3235,9 @@ void server_options(char **args, int *argc_p) if (mkpath_dest_arg && am_sender) args[ac++] = "--mkpath"; + if ((arg = niceness_remote_option()) != NULL) + args[ac++] = arg; + if (ac > MAX_SERVER_ARGS) { /* Not possible... */ rprintf(FERROR, "argc overflow in server_options().\n"); exit_cleanup(RERR_MALLOC); diff --git a/packaging/cull-options b/packaging/cull-options index e71818cdb..1d0e6c9c6 100755 --- a/packaging/cull-options +++ b/packaging/cull-options @@ -20,6 +20,8 @@ long_opts = { # These include some extra long-args that BackupPC uses: 'links': 0, 'log-file': 3, 'munge-links': 0, + # niceness_remote_option() constructs this outside options.c. + 'nice': 4, 'no-munge-links': -1, 'one-file-system': 0, 'owner': 0, @@ -101,7 +103,8 @@ def main(): txt += """ # To disable a long-named option, change its value to a -1. The values mean: # 0 = the option has no arg; 1 = the arg doesn't need any checking; 2 = only -# check the arg when receiving; and 3 = always check the arg. +# check the path arg when receiving; 3 = always check the path arg; and 4 = +# allow only a non-elevating --nice setting. """ print(txt, end='') diff --git a/pipe.c b/pipe.c index 3e5f038b5..e0a847ce1 100644 --- a/pipe.c +++ b/pipe.c @@ -130,6 +130,7 @@ pid_t local_child(int argc, char **argv, int *f_in, int *f_out, if (pid == 0) { am_sender = 0; am_server = 1; + niceness_use_remote(); filesfrom_fd = -1; munge_symlinks = 0; /* Each side needs its own option. */ chmod_modes = NULL; /* Let the sending side handle this. */ diff --git a/rsync.1.md b/rsync.1.md index 415480c8a..5a91fb8b4 100644 --- a/rsync.1.md +++ b/rsync.1.md @@ -686,6 +686,8 @@ has its own detailed description later in this manpage. --checksum-seed=NUM set block/file checksum seed (advanced) --ipv4, -4 prefer IPv4 --ipv6, -6 prefer IPv6 +--nice=SETTINGS adjust rsync CPU and I/O priorities +--no-nice clear earlier --nice settings --version, -V print the version + other info and exit --help, -h (*) show this help (* -h is help only on its own) ``` @@ -706,6 +708,8 @@ accepted: --log-file=FILE override the "log file" setting --log-file-format=FMT override the "log format" setting --sockopts=OPTIONS specify custom TCP options +--nice=SETTINGS adjust daemon process CPU and I/O priorities +--no-nice clear earlier --nice settings --verbose, -v increase verbosity --ipv4, -4 prefer IPv4 --ipv6, -6 prefer IPv6 @@ -4131,6 +4135,43 @@ sign) if you want the local shell to expand it. user wants a more random checksum seed. Setting NUM to 0 causes rsync to use the default of **time**() for checksum seed. +0. `--nice=SETTINGS` + + Adjust the CPU and I/O scheduling priority of the local process, the remote + process or both. SETTINGS is a comma-separated list. Each setting is + `TARGET`, `PRIORITY` or `TARGET:PRIORITY`. `TARGET` is `local`, `remote` or + `all`. Omitting `TARGET` applies the priority to both processes. A + target-only setting uses `19/idle`. Target names and I/O values are + case-insensitive. + + `PRIORITY` is a CPU nice value from -20 to 19, an I/O value or both + separated by `/`. I/O values are `none`, `idle`, `rt_0` through `rt_7` and + `be_0` through `be_7`. `none` restores the kernel-derived I/O priority. + Higher nice values and higher `rt` or `be` suffixes mean lower priority. + Raising priority may require additional privilege. + + A setting that specifies only CPU or I/O priority leaves the other component + unchanged. For example, + `--nice=remote:5 --nice=remote:idle` requests remote CPU priority 5 and + remote idle I/O priority. Later settings replace earlier values for the + same component. `--no-nice` clears all earlier settings. + + Examples: + + --nice=19/idle + --nice=local:10,remote:idle + --nice=local:19/be_7,remote:5 + + The remote rsync must support this option before a remote setting can be + used. Daemon clients may keep or lower the daemon worker's inherited + priorities but cannot raise them. A daemon administrator can also refuse + the option with `refuse options = nice`. When starting a daemon, the local + setting is applied before it accepts client settings. A standalone listener + passes that priority to its workers. The remote setting has no meaning. + + If a priority change is unsupported or fails, rsync warns and continues the + transfer. + ## DAEMON OPTIONS The options allowed when starting an rsync daemon are as follows: diff --git a/support/rrsync b/support/rrsync index 0310a5da1..a29889a1b 100755 --- a/support/rrsync +++ b/support/rrsync @@ -35,7 +35,8 @@ short_with_num = '@B' # DO NOT REMOVE ANY # To disable a long-named option, change its value to a -1. The values mean: # 0 = the option has no arg; 1 = the arg doesn't need any checking; 2 = only -# check the arg when receiving; and 3 = always check the arg. +# check the path arg when receiving; 3 = always check the path arg; and 4 = +# allow only a non-elevating --nice setting. long_opts = { 'append': 0, 'backup-dir': 2, @@ -93,6 +94,7 @@ long_opts = { 'msgs2stderr': 0, 'munge-links': 0, 'new-compress': 0, + 'nice': 4, 'no-W': 0, 'no-implied-dirs': 0, 'no-msgs2stderr': 0, @@ -703,6 +705,11 @@ def validated_arg(opt, arg, typ=3, wild=False): if opt != 'arg': # arg values already have their backslashes removed. arg = DE_BACKSLASH_RE.sub(r'\1', arg) + if typ == 4: + if not re.fullmatch(r'(?i:local(?::(?:19(?:/idle)?|idle))?)', arg): + die('unsafe --nice setting:', arg) + return arg + # "-" is rsync's read-the-list-from-stdin sentinel, not a pathname: a pull # with a local --files-from sends exactly "--files-from=-" to the server. if opt == '--files-from': diff --git a/support/rrsync.1.md b/support/rrsync.1.md index 2ab351de2..2ab0fd17d 100644 --- a/support/rrsync.1.md +++ b/support/rrsync.1.md @@ -129,6 +129,10 @@ uses when running the server command. A few extra convenience options are also included to help it to interact with BackupPC and accept some convenient user overrides. +The [`--nice`](rsync.1#opt--nice) option is limited to local CPU nice 19, idle +I/O priority or both. Other values could raise the server process priority and +are rejected. + The script (or a copy of it) can be manually edited if you want it to customize the option handling. diff --git a/testsuite/COVERAGE.md b/testsuite/COVERAGE.md index 470d87a09..6f09d4527 100644 --- a/testsuite/COVERAGE.md +++ b/testsuite/COVERAGE.md @@ -157,6 +157,7 @@ Status legend: ✓ property asserted · `~` shallow / by an existing ported test | --bwlimit | partial*new* (used, not asserted) | `~` | | --copy-as | — | ✗ root-gated | | --iconv | — | ✗ | +| --nice / --no-nice | niceness | ✓ local, remote shell, local server, daemon and rrsync | | -s/--secluded-args, --old-args, --trust-sender | (default arg-protection exercised) | `~` | --- diff --git a/testsuite/niceness_test.py b/testsuite/niceness_test.py new file mode 100644 index 000000000..b2e1fda77 --- /dev/null +++ b/testsuite/niceness_test.py @@ -0,0 +1,331 @@ +#!/usr/bin/env python3 + +import locale +import os +import shlex +import shutil +import subprocess + +from rsyncfns import ( + FROMDIR, RSYNC, SCRATCHDIR, TODIR, build_rsyncd_conf, hands_setup, + patched_rrsync, rsync_argv, rsync_path_arg, rsh_cmd, run_rsync, + start_test_daemon, test_fail, write_daemon_conf, +) + + +def output(proc): + return (proc.stdout or '') + (proc.stderr or '') + + +def forwarded_option(*args): + proc = run_rsync( + '-nvv', '--rsh=echo', *args, f'localhost:{FROMDIR}/', f'{TODIR}/', + check=False, capture_output=True, + ) + for line in output(proc).splitlines(): + if 'opening connection using:' in line: + return line + test_fail(f'remote command was not reported:\n{output(proc)}') + + +valid = ( + (('--nice=remote:5/be_6',), '--nice=local:5/be_6'), + (('--nice=remote:idle',), '--nice=local:idle'), + (('--nice=remote:IDLE',), '--nice=local:idle'), + (('--nice=remote:5', '--nice=remote:idle'), '--nice=local:5/idle'), + (('--nice=remote:5,remote:idle',), '--nice=local:5/idle'), + (('--nice=remote:5/be_6,remote:19/idle',), '--nice=local:19/idle'), + (('--nice=remote:none',), '--nice=local:none'), + (('--nice=remote:0',), '--nice=local:0'), + (('--nice=REMOTE:-20/RT_0',), '--nice=local:-20/rt_0'), + (('--nice=remote:19/BE_7',), '--nice=local:19/be_7'), + (('--nice=ALL',), '--nice=local:19/idle'), + (('--nice=local:19',), None), + (('--nice=19/idle', '--no-nice'), None), + (('--nice=local:6/idle,remote:5/be_6',), '--nice=local:5/be_6'), +) + +for args, expected in valid: + command = forwarded_option(*args) + if expected and expected not in command: + test_fail(f'{args!r} did not forward {expected!r}:\n{command}') + if not expected and '--nice=' in command: + test_fail(f'{args!r} unexpectedly forwarded --nice:\n{command}') + +invalid = ( + '', 'local:', '2/', '20', '+20', '-21', '999999999999999999999999', + 'be_8', 'rt_9', 'locality', ' 1', '\t1', + ' local:1', 'local: 1', 'local::1', 'idle/none', '1//idle', 'none/idle', + 'local:1,', 'local:1,,remote:2', 'somewhere:1', '19/idle/x', +) + +for value in invalid: + proc = run_rsync(f'--nice={value}', '--version', check=False, capture_output=True) + if proc.returncode == 0 or 'Invalid argument passed to --nice' not in output(proc): + test_fail(f'invalid --nice value was accepted: {value!r}') + +saved_locale = locale.setlocale(locale.LC_CTYPE) +turkish_locale = None +try: + for candidate in ('tr_TR.UTF-8', 'tr_TR.utf8', 'Turkish_Turkey.1254'): + try: + turkish_locale = locale.setlocale(locale.LC_CTYPE, candidate) + break + except locale.Error: + pass +finally: + locale.setlocale(locale.LC_CTYPE, saved_locale) + +if turkish_locale: + env = {**os.environ, 'LC_ALL': turkish_locale} + proc = subprocess.run( + rsync_argv('--nice=IDLE', '--version'), env=env, + capture_output=True, text=True, timeout=20, + ) + if proc.returncode != 0: + test_fail(f'uppercase I/O priority depended on the locale:\n{output(proc)}') + +true = shutil.which('true') +if true: + rrsync_base = SCRATCHDIR / 'rrsync-nice' + restricted = rrsync_base / 'restricted' + restricted.mkdir(parents=True, exist_ok=True) + rrsync = patched_rrsync(rrsync_base, rsync_path=true) + for setting in ('local', 'local:19', 'local:idle', 'local:19/idle', + 'LOCAL:19/IDLE'): + env = {**os.environ, + 'SSH_ORIGINAL_COMMAND': f'rsync --server --nice={setting} . .'} + proc = subprocess.run( + [str(rrsync), '-no-lock', str(restricted)], env=env, + capture_output=True, text=True, timeout=20, + ) + if proc.returncode != 0: + test_fail(f'rrsync rejected safe --nice setting {setting!r}:\n{output(proc)}') + + for setting in ('local:18', 'local:none', 'local:be_7', 'local:19/be_7', + 'all:19/idle', 'local:-1/rt_0'): + env = {**os.environ, + 'SSH_ORIGINAL_COMMAND': f'rsync --server --nice={setting} . .'} + proc = subprocess.run( + [str(rrsync), '-no-lock', str(restricted)], env=env, + capture_output=True, text=True, timeout=20, + ) + if proc.returncode == 0 or 'unsafe --nice setting' not in output(proc): + test_fail(f'rrsync accepted unsafe --nice setting {setting!r}:\n{output(proc)}') + +proc = run_rsync('--daemon', '--nice=local:19/idle', '--no-nice', '--help', + check=False, capture_output=True) +if proc.returncode != 0: + test_fail(f'daemon niceness options were rejected:\n{output(proc)}') + +hands_setup() +(FROMDIR / 'file').write_text('data\n') +rsh = rsh_cmd(None, '--no-cd') +proc = run_rsync( + '-a', '--debug=CMD1', '-M--debug=CMD1', + '--nice=local,remote:18', f'{FROMDIR}/', f'{TODIR}/', + capture_output=True, +) +text = output(proc) +cpu_supported = 'cannot set client CPU priority to 19: operation is not supported' not in text +io_supported = 'cannot set client I/O priority to idle: operation is not supported' not in text +if cpu_supported: + if 'set client CPU priority to 19' not in text: + test_fail(f'local-transfer client priority was not applied:\n{text}') + if 'set server CPU priority to 18' not in text: + test_fail(f'local-transfer server priority was not applied independently:\n{text}') +if io_supported and 'set client I/O priority to idle' not in text: + test_fail(f'target-only local I/O priority was not applied:\n{text}') + +shutil.rmtree(TODIR) +proc = run_rsync( + '-a', '--debug=CMD1', '-M--debug=CMD1', '-M--nice=local:17', + '--nice=local:19,remote:18', f'{FROMDIR}/', f'{TODIR}/', + capture_output=True, +) +text = output(proc) +if cpu_supported and 'set server CPU priority to 17' not in text: + test_fail(f'explicit remote option did not override the remote priority:\n{text}') + +shutil.rmtree(TODIR) +proc = run_rsync( + '-nr', '--debug=CMD1', '--nice=local:19/idle', '--no-nice', + f'{FROMDIR}/', f'{TODIR}/', capture_output=True, +) +if 'CPU priority' in output(proc) or 'I/O priority' in output(proc): + test_fail(f'--no-nice did not clear local priority settings:\n{output(proc)}') + +proc = run_rsync( + '-a', '--debug=CMD1', '-M--debug=CMD1', + '--nice=local:19/idle', '--nice=remote:19/idle', + f'--rsh={rsh}', f'--rsync-path={rsync_path_arg(RSYNC)}', + f'{FROMDIR}/', f'localhost:{TODIR}/', capture_output=True, +) +text = output(proc) +cpu_supported = not any( + 'CPU priority' in line and 'not supported' in line for line in text.splitlines() +) +io_supported = not any( + 'I/O priority' in line and 'not supported' in line for line in text.splitlines() +) +if cpu_supported: + for role in ('client', 'server'): + if f'set {role} CPU priority to 19' not in text: + test_fail(f'{role} CPU priority was not applied:\n{text}') +if io_supported: + for role in ('client', 'server'): + if f'set {role} I/O priority to idle' not in text: + test_fail(f'{role} I/O priority was not applied:\n{text}') + +shutil.rmtree(TODIR) +proc = run_rsync( + '-as', '--debug=CMD1', '-M--debug=CMD1', '--nice=remote:19', f'--rsh={rsh}', + f'--rsync-path={rsync_path_arg(RSYNC)}', f'{FROMDIR}/', + f'localhost:{TODIR}/', capture_output=True, +) +text = output(proc) +if cpu_supported and 'set server CPU priority to 19' not in text: + test_fail(f'remote priority with secluded arguments was not applied:\n{text}') + +shutil.rmtree(TODIR) +proc = run_rsync( + '-a', '-M--debug=CMD1', '--nice=remote:none', f'--rsh={rsh}', + f'--rsync-path={rsync_path_arg(RSYNC)}', f'{FROMDIR}/', + f'localhost:{TODIR}/', capture_output=True, +) +if io_supported and 'set server I/O priority to none' not in output(proc): + test_fail(f'remote I/O priority was not reset:\n{output(proc)}') + +cpu_raise_denied = False +if cpu_supported: + proc = run_rsync( + '-nr', '--debug=CMD1', '--nice=local:-1', f'{FROMDIR}/', f'{TODIR}/', + check=False, capture_output=True, + ) + if proc.returncode != 0: + test_fail(f'local priority probe failed:\n{output(proc)}') + cpu_raise_denied = 'failed to set client CPU priority to -1' in output(proc) + +conf = build_rsyncd_conf() +with conf.open('a') as fh: + fh.write(f'''\ + +[test-nice-refused] + path = {FROMDIR} + read only = yes + refuse options = nice + +[test-nice-rsh] + path = {FROMDIR} + read only = yes + hosts allow = * +''') +daemon_start_cpu = -1 if cpu_raise_denied else 17 +daemon_raise_cpu = -1 if cpu_raise_denied else 16 +daemon_rsh = ( + f'{rsync_path_arg(RSYNC)} --config={shlex.quote(str(conf))} ' + f'--daemon --nice=local:{daemon_start_cpu}/idle' +) +proc = run_rsync( + '-nr', '--debug=CMD1', '-M--debug=CMD1', '--nice=remote:19/idle', + f'--rsh={rsh}', f'--rsync-path={daemon_rsh}', 'localhost::test-nice-rsh/', + f'{SCRATCHDIR}/daemon-rsh-copy/', capture_output=True, +) +text = output(proc) +if cpu_supported and 'set daemon CPU priority to 19' not in text: + test_fail(f'remote-shell daemon CPU priority was not applied:\n{text}') +if io_supported and 'set daemon I/O priority to idle' not in text: + test_fail(f'remote-shell daemon I/O priority was not applied:\n{text}') + +daemon_log = SCRATCHDIR / 'rsyncd.log' +if not io_supported: + startup_message = 'cannot set daemon I/O priority to idle: operation is not supported' +elif not cpu_supported: + startup_message = (f'cannot set daemon CPU priority to {daemon_start_cpu}: ' + 'operation is not supported') +elif cpu_raise_denied: + startup_message = f'failed to set daemon CPU priority to {daemon_start_cpu}' +else: + startup_message = None +if startup_message and startup_message not in daemon_log.read_text(): + test_fail(f'remote-shell daemon startup priority missed its configured log:\n' + f'{daemon_log.read_text()}') + +proc = run_rsync( + '-nr', f'--nice=remote:{daemon_raise_cpu}/be_7', f'--rsh={rsh}', + f'--rsync-path={daemon_rsh}', 'localhost::test-nice-rsh/', + f'{SCRATCHDIR}/daemon-rsh-copy/', check=False, capture_output=True, +) +text = output(proc) +if cpu_supported or io_supported: + rejected = ('--nice cannot raise daemon CPU priority' in text + or '--nice cannot raise daemon I/O priority' in text) + if proc.returncode == 0 or not rejected: + test_fail(f'remote-shell daemon accepted a priority increase:\n{text}') + +daemon_log.unlink() +daemon_url = start_test_daemon( + conf, 12898, rsync_cmd=f'{RSYNC} --nice=local:{daemon_start_cpu}/idle', +) +proc = run_rsync( + '-nr', '--debug=CMD1', '-M--debug=CMD1', '--nice=remote:19/idle', + f'{daemon_url}test-from/', f'{SCRATCHDIR}/daemon-copy/', capture_output=True, +) +text = output(proc) +if cpu_supported and 'set daemon CPU priority to 19' not in text: + test_fail(f'daemon CPU priority was not lowered:\n{text}') +if io_supported and 'set daemon I/O priority to idle' not in text: + test_fail(f'daemon I/O priority was not lowered:\n{text}') +if startup_message and startup_message not in daemon_log.read_text(): + test_fail(f'daemon startup priority missed its configured log:\n' + f'{daemon_log.read_text()}') + +proc = run_rsync( + '-nr', f'--nice=remote:{daemon_raise_cpu}', f'{daemon_url}test-from/', + f'{SCRATCHDIR}/daemon-copy/', check=False, capture_output=True, +) +if cpu_supported: + if proc.returncode == 0 or '--nice cannot raise daemon CPU priority' not in output(proc): + test_fail(f'daemon accepted a CPU priority increase:\n{output(proc)}') + +proc = run_rsync( + '-nr', '--nice=remote:be_7', f'{daemon_url}test-from/', + f'{SCRATCHDIR}/daemon-copy/', check=False, capture_output=True, +) +if io_supported: + if proc.returncode == 0 or '--nice cannot raise daemon I/O priority' not in output(proc): + test_fail(f'daemon accepted an I/O priority increase:\n{output(proc)}') + +proc = run_rsync( + '-nr', '--nice=remote:19', f'{daemon_url}test-nice-refused/', + f'{SCRATCHDIR}/daemon-copy/', check=False, capture_output=True, +) +if proc.returncode == 0 or 'server is configured to refuse --nice' not in output(proc): + test_fail(f'daemon did not refuse --nice:\n{output(proc)}') + +compound_conf = write_daemon_conf( + [('test-from', {'path': FROMDIR, 'read only': 'yes'})], + globals={ + 'pid file': SCRATCHDIR / 'compound-rsyncd.pid', + 'log file': SCRATCHDIR / 'compound-rsyncd.log', + }, + name='compound-rsyncd.conf', +) +compound_daemon_url = start_test_daemon( + compound_conf, 12998, rsync_cmd=f'{RSYNC} --nice=local:0/be_7', +) +compound_rejected = io_supported and not cpu_supported +proc = run_rsync( + '-nr', '--debug=CMD1', '-M--debug=CMD1', '--nice=remote:19/none', + f'{compound_daemon_url}test-from/', f'{SCRATCHDIR}/compound-daemon-copy/', + check=not compound_rejected, capture_output=True, +) +text = output(proc) +if compound_rejected: + if proc.returncode == 0 or '--nice cannot raise daemon I/O priority' not in text: + test_fail(f'compound daemon priority was not rejected safely:\n{text}') +else: + if cpu_supported and 'set daemon CPU priority to 19' not in text: + test_fail(f'compound daemon CPU priority was not lowered:\n{text}') + if io_supported and 'set daemon I/O priority to none' not in text: + test_fail(f'compound daemon I/O priority was not retained safely:\n{text}') From 5b1749b55d663cc41d6def27676b1bcc1fab401e Mon Sep 17 00:00:00 2001 From: Zen Dodd Date: Sat, 24 Oct 2026 23:43:43 +1000 Subject: [PATCH 2/2] options: restrict daemon client priorities --- configure.ac | 2 +- main.c | 2 + niceness.c | 134 +++----------------------------- rsync.1.md | 10 +-- testsuite/niceness_test.py | 151 ++++++++++++++----------------------- 5 files changed, 74 insertions(+), 225 deletions(-) diff --git a/configure.ac b/configure.ac index 2434aeaf6..5c6ce8910 100644 --- a/configure.ac +++ b/configure.ac @@ -978,7 +978,7 @@ AC_CHECK_FUNCS(waitpid wait4 getcwd chown chmod lchmod mknod mkfifo fdopendir \ seteuid strerror putenv iconv_open locale_charset nl_langinfo getxattr \ extattr_get_link sigaction sigprocmask setattrlist getgrouplist \ initgroups utimensat futimens posix_fallocate attropen setvbuf nanosleep usleep \ - setenv unsetenv mbrtowc getpriority setpriority) + setenv unsetenv mbrtowc setpriority) dnl dirfd() is a macro or static inline on several systems (the BSDs), so the dnl default AC_CHECK_FUNCS link probe -- which declares `char dirfd(void);` and diff --git a/main.c b/main.c index 61c2ade65..63c35a15c 100644 --- a/main.c +++ b/main.c @@ -1309,6 +1309,8 @@ void start_server(int f_in, int f_out, int argc, char *argv[]) if (protocol_version >= 23) io_start_multiplex_out(f_out); + /* Priority diagnostics need the negotiated message channel. + * File-list work starts below. */ niceness_apply(); if (am_daemon && io_timeout && protocol_version >= 31) send_msg_int(MSG_IO_TIMEOUT, io_timeout); diff --git a/niceness.c b/niceness.c index 99ef27002..15b31a5ec 100644 --- a/niceness.c +++ b/niceness.c @@ -9,38 +9,21 @@ #ifdef HAVE_SYS_SYSCALL_H #include #endif - extern int am_server; extern int am_daemon; #define PRIORITY_UNSET (-100) #define DEFAULT_NICE 19 -#if defined HAVE_GETPRIORITY && defined HAVE_SYS_RESOURCE_H -#define SUPPORT_GET_PRIORITY 1 -#endif - -#if defined SUPPORT_GET_PRIORITY && defined HAVE_SETPRIORITY +#if defined HAVE_SETPRIORITY && defined HAVE_SYS_RESOURCE_H #define SUPPORT_NICE_PRIORITY 1 #endif #if defined HAVE_LINUX_IOPRIO_H && defined HAVE_SYS_SYSCALL_H \ - && defined SUPPORT_GET_PRIORITY \ - && defined SYS_ioprio_get && defined SYS_ioprio_set \ - && defined IOPRIO_PRIO_CLASS \ - && defined IOPRIO_PRIO_DATA && defined IOPRIO_PRIO_VALUE + && defined SYS_ioprio_set && defined IOPRIO_PRIO_VALUE #define SUPPORT_IO_PRIORITY 1 #endif -#ifdef SUPPORT_IO_PRIORITY -#ifdef IOPRIO_PRIO_LEVEL -#define RSYNC_IOPRIO_LEVEL(value) IOPRIO_PRIO_LEVEL(value) -#else -/* Older headers expose the whole data field; the level is its low 3 bits. */ -#define RSYNC_IOPRIO_LEVEL(value) (IOPRIO_PRIO_DATA(value) & 7) -#endif -#endif - enum io_priority_class { IO_PRIORITY_NONE, IO_PRIORITY_REALTIME, @@ -266,120 +249,21 @@ char *niceness_remote_option(void) return arg; } -#ifdef SUPPORT_IO_PRIORITY -static int effective_io_priority(int class, int data, int cpu, int *rank_p, int *data_p) -{ - if (class == IOPRIO_CLASS_NONE) { - class = IOPRIO_CLASS_BE; - data = (cpu + 20) / 5; - if (data > 7) - data = 7; - } - - switch (class) { - case IOPRIO_CLASS_RT: - *rank_p = 0; - break; - case IOPRIO_CLASS_BE: - *rank_p = 1; - break; - case IOPRIO_CLASS_IDLE: - *rank_p = 2; - data = 0; - break; - default: - return 0; - } - *data_p = data; - return 1; -} - -static int requested_io_priority(const struct priority_setting *setting, int cpu, - int *rank_p, int *data_p) -{ - int class; - - switch (setting->io_class) { - case IO_PRIORITY_NONE: - class = IOPRIO_CLASS_NONE; - break; - case IO_PRIORITY_REALTIME: - class = IOPRIO_CLASS_RT; - break; - case IO_PRIORITY_BEST_EFFORT: - class = IOPRIO_CLASS_BE; - break; - case IO_PRIORITY_IDLE: - class = IOPRIO_CLASS_IDLE; - break; - default: - return 0; - } - return effective_io_priority(class, setting->io_data, cpu, rank_p, data_p); -} -#endif - int niceness_check_daemon_request(char *err_buf, size_t err_len) { struct priority_setting *setting = &priorities.local; - if (setting->cpu == PRIORITY_UNSET && setting->io_class == PRIORITY_UNSET) - return 1; - -#if defined SUPPORT_NICE_PRIORITY || defined SUPPORT_IO_PRIORITY - int current_cpu; - - errno = 0; - current_cpu = getpriority(PRIO_PROCESS, 0); - if (errno) { - snprintf(err_buf, err_len, "cannot inspect daemon process priority: %s\n", - strerror(errno)); + if (setting->cpu != PRIORITY_UNSET && setting->cpu != DEFAULT_NICE) { + snprintf(err_buf, err_len, + "daemon clients may only request CPU nice 19\n"); return 0; } -#endif - -#ifdef SUPPORT_NICE_PRIORITY - if (setting->cpu != PRIORITY_UNSET && setting->cpu < current_cpu) { - snprintf(err_buf, err_len, "--nice cannot raise daemon CPU priority\n"); + if (setting->io_class != PRIORITY_UNSET + && setting->io_class != IO_PRIORITY_IDLE) { + snprintf(err_buf, err_len, + "daemon clients may only request idle I/O priority\n"); return 0; } -#endif - -#ifdef SUPPORT_IO_PRIORITY - if (setting->io_class != PRIORITY_UNSET) { - int raw = syscall(SYS_ioprio_get, IOPRIO_WHO_PROCESS, 0); - int requested_cpu = current_cpu; - int current_rank, current_data, requested_rank, requested_data; - - if (raw < 0) { - snprintf(err_buf, err_len, "cannot inspect daemon I/O priority: %s\n", - strerror(errno)); - return 0; - } -#ifdef SUPPORT_NICE_PRIORITY - if (setting->cpu != PRIORITY_UNSET) - requested_cpu = setting->cpu; -#endif - /* IOPRIO_CLASS_NONE derives its level from CPU nice. */ - if (!effective_io_priority(IOPRIO_PRIO_CLASS(raw), RSYNC_IOPRIO_LEVEL(raw), - current_cpu, ¤t_rank, ¤t_data) - || !requested_io_priority(setting, requested_cpu, - &requested_rank, &requested_data)) { - snprintf(err_buf, err_len, "unsupported daemon I/O priority class\n"); - return 0; - } - if (requested_rank < current_rank - || (requested_rank == current_rank && requested_data < current_data)) { - snprintf(err_buf, err_len, "--nice cannot raise daemon I/O priority\n"); - return 0; - } - } -#endif - -#if !defined SUPPORT_NICE_PRIORITY && !defined SUPPORT_IO_PRIORITY - (void)err_buf; - (void)err_len; -#endif return 1; } diff --git a/rsync.1.md b/rsync.1.md index 5a91fb8b4..e447ed3eb 100644 --- a/rsync.1.md +++ b/rsync.1.md @@ -4163,11 +4163,11 @@ sign) if you want the local shell to expand it. --nice=local:19/be_7,remote:5 The remote rsync must support this option before a remote setting can be - used. Daemon clients may keep or lower the daemon worker's inherited - priorities but cannot raise them. A daemon administrator can also refuse - the option with `refuse options = nice`. When starting a daemon, the local - setting is applied before it accepts client settings. A standalone listener - passes that priority to its workers. The remote setting has no meaning. + used. Daemon clients may request only CPU nice 19, idle I/O priority or + both. A daemon administrator can also refuse the option with + `refuse options = nice`. When starting a daemon, the local setting is + applied before it accepts client settings. A standalone listener passes + that priority to its workers. A daemon's remote setting has no meaning. If a priority change is unsupported or fails, rsync warns and continues the transfer. diff --git a/testsuite/niceness_test.py b/testsuite/niceness_test.py index b2e1fda77..409d7a42e 100644 --- a/testsuite/niceness_test.py +++ b/testsuite/niceness_test.py @@ -5,11 +5,12 @@ import shlex import shutil import subprocess +import sys from rsyncfns import ( FROMDIR, RSYNC, SCRATCHDIR, TODIR, build_rsyncd_conf, hands_setup, patched_rrsync, rsync_argv, rsync_path_arg, rsh_cmd, run_rsync, - start_test_daemon, test_fail, write_daemon_conf, + start_test_daemon, test_fail, ) @@ -64,6 +65,8 @@ def forwarded_option(*args): if proc.returncode == 0 or 'Invalid argument passed to --nice' not in output(proc): test_fail(f'invalid --nice value was accepted: {value!r}') +# In Turkish, ASCII I lowercases to a dotless i rather than i. Option names use +# ASCII syntax, so their case matching must remain locale-independent. saved_locale = locale.setlocale(locale.LC_CTYPE) turkish_locale = None try: @@ -196,16 +199,6 @@ def forwarded_option(*args): if io_supported and 'set server I/O priority to none' not in output(proc): test_fail(f'remote I/O priority was not reset:\n{output(proc)}') -cpu_raise_denied = False -if cpu_supported: - proc = run_rsync( - '-nr', '--debug=CMD1', '--nice=local:-1', f'{FROMDIR}/', f'{TODIR}/', - check=False, capture_output=True, - ) - if proc.returncode != 0: - test_fail(f'local priority probe failed:\n{output(proc)}') - cpu_raise_denied = 'failed to set client CPU priority to -1' in output(proc) - conf = build_rsyncd_conf() with conf.open('a') as fh: fh.write(f'''\ @@ -220,11 +213,9 @@ def forwarded_option(*args): read only = yes hosts allow = * ''') -daemon_start_cpu = -1 if cpu_raise_denied else 17 -daemon_raise_cpu = -1 if cpu_raise_denied else 16 daemon_rsh = ( f'{rsync_path_arg(RSYNC)} --config={shlex.quote(str(conf))} ' - f'--daemon --nice=local:{daemon_start_cpu}/idle' + f'--daemon --nice=local:19/be_7' ) proc = run_rsync( '-nr', '--debug=CMD1', '-M--debug=CMD1', '--nice=remote:19/idle', @@ -237,64 +228,63 @@ def forwarded_option(*args): if io_supported and 'set daemon I/O priority to idle' not in text: test_fail(f'remote-shell daemon I/O priority was not applied:\n{text}') -daemon_log = SCRATCHDIR / 'rsyncd.log' -if not io_supported: - startup_message = 'cannot set daemon I/O priority to idle: operation is not supported' -elif not cpu_supported: - startup_message = (f'cannot set daemon CPU priority to {daemon_start_cpu}: ' - 'operation is not supported') -elif cpu_raise_denied: - startup_message = f'failed to set daemon CPU priority to {daemon_start_cpu}' -else: - startup_message = None -if startup_message and startup_message not in daemon_log.read_text(): - test_fail(f'remote-shell daemon startup priority missed its configured log:\n' - f'{daemon_log.read_text()}') - -proc = run_rsync( - '-nr', f'--nice=remote:{daemon_raise_cpu}/be_7', f'--rsh={rsh}', - f'--rsync-path={daemon_rsh}', 'localhost::test-nice-rsh/', - f'{SCRATCHDIR}/daemon-rsh-copy/', check=False, capture_output=True, +idle_code = ( + 'import os\n' + 'os.sched_setscheduler(0, os.SCHED_IDLE, os.sched_param(0))\n' ) -text = output(proc) -if cpu_supported or io_supported: - rejected = ('--nice cannot raise daemon CPU priority' in text - or '--nice cannot raise daemon I/O priority' in text) - if proc.returncode == 0 or not rejected: - test_fail(f'remote-shell daemon accepted a priority increase:\n{text}') +sched_idle = False +if hasattr(os, 'sched_setscheduler') and hasattr(os, 'SCHED_IDLE'): + probe = subprocess.run( + [sys.executable, '-c', idle_code], stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, timeout=20, + ) + sched_idle = probe.returncode == 0 +if sched_idle: + idle_wrapper = SCRATCHDIR / 'sched-idle-rsync.py' + idle_wrapper.write_text( + idle_code + 'import sys\nos.execvp(sys.argv[1], sys.argv[1:])\n' + ) + idle_command = shlex.join([sys.executable, str(idle_wrapper), *shlex.split(RSYNC)]) + idle_daemon_rsh = ( + f'{idle_command} --config={shlex.quote(str(conf))} --daemon' + ) + proc = run_rsync( + '-nr', '--nice=remote:be_7', f'--rsh={rsh}', + f'--rsync-path={idle_daemon_rsh}', 'localhost::test-nice-rsh/', + f'{SCRATCHDIR}/idle-daemon-copy/', check=False, capture_output=True, + ) + if proc.returncode == 0 or 'daemon clients may only request idle I/O priority' not in output(proc): + test_fail(f'SCHED_IDLE daemon accepted an I/O priority increase:\n{output(proc)}') + +daemon_log = SCRATCHDIR / 'rsyncd.log' daemon_log.unlink() daemon_url = start_test_daemon( - conf, 12898, rsync_cmd=f'{RSYNC} --nice=local:{daemon_start_cpu}/idle', + conf, 12898, rsync_cmd=f'{RSYNC} --nice=local:19/be_7', ) -proc = run_rsync( - '-nr', '--debug=CMD1', '-M--debug=CMD1', '--nice=remote:19/idle', - f'{daemon_url}test-from/', f'{SCRATCHDIR}/daemon-copy/', capture_output=True, -) -text = output(proc) -if cpu_supported and 'set daemon CPU priority to 19' not in text: - test_fail(f'daemon CPU priority was not lowered:\n{text}') -if io_supported and 'set daemon I/O priority to idle' not in text: - test_fail(f'daemon I/O priority was not lowered:\n{text}') -if startup_message and startup_message not in daemon_log.read_text(): - test_fail(f'daemon startup priority missed its configured log:\n' - f'{daemon_log.read_text()}') - -proc = run_rsync( - '-nr', f'--nice=remote:{daemon_raise_cpu}', f'{daemon_url}test-from/', - f'{SCRATCHDIR}/daemon-copy/', check=False, capture_output=True, -) -if cpu_supported: - if proc.returncode == 0 or '--nice cannot raise daemon CPU priority' not in output(proc): - test_fail(f'daemon accepted a CPU priority increase:\n{output(proc)}') - -proc = run_rsync( - '-nr', '--nice=remote:be_7', f'{daemon_url}test-from/', - f'{SCRATCHDIR}/daemon-copy/', check=False, capture_output=True, -) -if io_supported: - if proc.returncode == 0 or '--nice cannot raise daemon I/O priority' not in output(proc): - test_fail(f'daemon accepted an I/O priority increase:\n{output(proc)}') +for setting in ('19', 'idle', '19/idle'): + proc = run_rsync( + '-nr', '--debug=CMD1', '-M--debug=CMD1', f'--nice=remote:{setting}', + f'{daemon_url}test-from/', f'{SCRATCHDIR}/daemon-copy/', + capture_output=True, + ) + text = output(proc) + if setting == '19/idle': + if cpu_supported and 'set daemon CPU priority to 19' not in text: + test_fail(f'daemon CPU priority was not lowered:\n{text}') + if io_supported and 'set daemon I/O priority to idle' not in text: + test_fail(f'daemon I/O priority was not lowered:\n{text}') + +for setting, message in ( + ('18', 'daemon clients may only request CPU nice 19'), + ('be_7', 'daemon clients may only request idle I/O priority'), +): + proc = run_rsync( + '-nr', f'--nice=remote:{setting}', f'{daemon_url}test-from/', + f'{SCRATCHDIR}/daemon-copy/', check=False, capture_output=True, + ) + if proc.returncode == 0 or message not in output(proc): + test_fail(f'daemon accepted unsafe --nice setting {setting!r}:\n{output(proc)}') proc = run_rsync( '-nr', '--nice=remote:19', f'{daemon_url}test-nice-refused/', @@ -302,30 +292,3 @@ def forwarded_option(*args): ) if proc.returncode == 0 or 'server is configured to refuse --nice' not in output(proc): test_fail(f'daemon did not refuse --nice:\n{output(proc)}') - -compound_conf = write_daemon_conf( - [('test-from', {'path': FROMDIR, 'read only': 'yes'})], - globals={ - 'pid file': SCRATCHDIR / 'compound-rsyncd.pid', - 'log file': SCRATCHDIR / 'compound-rsyncd.log', - }, - name='compound-rsyncd.conf', -) -compound_daemon_url = start_test_daemon( - compound_conf, 12998, rsync_cmd=f'{RSYNC} --nice=local:0/be_7', -) -compound_rejected = io_supported and not cpu_supported -proc = run_rsync( - '-nr', '--debug=CMD1', '-M--debug=CMD1', '--nice=remote:19/none', - f'{compound_daemon_url}test-from/', f'{SCRATCHDIR}/compound-daemon-copy/', - check=not compound_rejected, capture_output=True, -) -text = output(proc) -if compound_rejected: - if proc.returncode == 0 or '--nice cannot raise daemon I/O priority' not in text: - test_fail(f'compound daemon priority was not rejected safely:\n{text}') -else: - if cpu_supported and 'set daemon CPU priority to 19' not in text: - test_fail(f'compound daemon CPU priority was not lowered:\n{text}') - if io_supported and 'set daemon I/O priority to none' not in text: - test_fail(f'compound daemon I/O priority was not retained safely:\n{text}')