feat(p5-rsh): --rsh/-e, --rsync-path, --blocking-io, --outbuf

This commit is contained in:
2026-09-09 13:41:28 +02:00
parent 90ccf297d7
commit 858af3d63d
12 changed files with 462 additions and 42 deletions
+43
View File
@@ -194,6 +194,35 @@ static int set_stderr_mode(const char* value) {
return 0;
}
/* Parse --outbuf=N|L|B into the config's OutbufMode. N=none (unbuffered),
* L=line-buffered, B=block-buffered (the stdio default). Anything else is a
* clear error, never a silent fallback. */
static int set_outbuf_option(Config* config, const char* value) {
if (strcmp(value, "N") == 0 || strcmp(value, "n") == 0)
config->outbuf = OUTBUF_NONE;
else if (strcmp(value, "L") == 0 || strcmp(value, "l") == 0)
config->outbuf = OUTBUF_LINE;
else if (strcmp(value, "B") == 0 || strcmp(value, "b") == 0)
config->outbuf = OUTBUF_BLOCK;
else {
log_message(LOG_LEVEL_ERROR, "--outbuf must be N (none), L (line), or B (block)");
return -1;
}
return 0;
}
#ifndef FASTSYNC_TEST_BUILD
/* Apply the parsed --outbuf style to stdout/stderr via setvbuf, matching stdio
* semantics: N -> _IONBF (unbuffered), L -> _IOLBF (line), B -> _IOFBF (block,
* the default). */
static void apply_output_buffering(const Config* config) {
int mode = config->outbuf;
int stdio_mode = (mode == OUTBUF_NONE) ? _IONBF : (mode == OUTBUF_LINE) ? _IOLBF : _IOFBF;
setvbuf(stdout, NULL, stdio_mode, 0);
setvbuf(stderr, NULL, stdio_mode, 0);
}
#endif
static int read_patterns_from_file(const char* filepath, char*** patterns, int* count);
static int parse_debug_flags(const char* value, Config* config) {
@@ -472,6 +501,8 @@ static const OptionEntry OPTION_TABLE[] = {
{"--secluded-args", NULL, OPT_NOOP, 0},
{"--update", "-u", OPT_FLAG, offsetof(Config, update)},
{"--old-args", NULL, OPT_FLAG, offsetof(Config, old_args)},
{"--rsh", "-e", OPT_STRING, offsetof(Config, rsh_command)},
{"--blocking-io", NULL, OPT_FLAG, offsetof(Config, blocking_io)},
{"--links", "-l", OPT_FLAG, offsetof(Config, follow_symlinks)},
{"--copy-links", NULL, OPT_FLAG, offsetof(Config, copy_links)},
{"--safe-links", NULL, OPT_FLAG, offsetof(Config, safe_links)},
@@ -521,6 +552,9 @@ static const OptionEntry OPTION_TABLE[] = {
{"--ca", NULL, OPT_STRING, offsetof(Config, tls_ca)},
{"--backup-dir", NULL, OPT_STRING, offsetof(Config, backup_dir)},
{"--fastsync-server-path", NULL, OPT_STRING, offsetof(Config, fastsync_server_path)},
/* --rsync-path is rsync's spelling for the same "server program path"; it
* is a pure alias for fastsync_server_path (never a distinct field). */
{"--rsync-path", NULL, OPT_STRING, offsetof(Config, fastsync_server_path)},
{"--temp-dir", NULL, OPT_STRING, offsetof(Config, temp_dir)},
{"--partial-dir", NULL, OPT_STRING, offsetof(Config, partial_dir)},
{"--suffix", NULL, OPT_STRING, offsetof(Config, suffix)},
@@ -1191,6 +1225,12 @@ int parse_args(Config* config, int argc, char* argv[], int* positional_args,
if (identity_parse_chown(config, argv[++i]) != 0)
return -1;
config->use_metadata = true;
} else if (strncmp(argv[i], "--outbuf=", 9) == 0) {
if (set_outbuf_option(config, argv[i] + 9) != 0)
return -1;
} else if (opt_is(argv[i], "--outbuf", NULL)) {
if (i + 1 >= argc || set_outbuf_option(config, argv[++i]) != 0)
return -1;
} else if (argv[i][0] == '-') {
char* escaped = output_escape(argv[i], false);
fprintf(stderr, "Unknown option: %s\n", escaped ? escaped : "<allocation failed>");
@@ -1395,6 +1435,9 @@ int main(int argc, char* argv[]) {
goto cleanup;
}
/* Apply the requested --outbuf style now that the mode is parsed. */
apply_output_buffering(config);
/* --open-noatime is a sender-side policy: install it for every source read
(scan + data path) without touching the receiver. */
file_set_open_noatime(config->open_noatime);
+2 -1
View File
@@ -367,7 +367,8 @@ static Client* connect_transfer_client(const Config* config) {
return NULL;
}
return client_connect_ssh(config->ssh_destination, config->ssh_port,
config->fastsync_server_path, config->old_args);
config->fastsync_server_path, config->old_args, config->rsh_command,
config->blocking_io);
}
Client* client = client_create();
+9
View File
@@ -20,6 +20,15 @@ void print_usage(void) {
printf(" -n, --dry-run Show what would be transferred\n");
printf(" --remove-source-files Remove regular source files after successful transfer\n");
printf(" -p <port> SSH port (default: 22)\n");
printf(" -e, --rsh <command> Remote shell to launch on the client for the SSH\n");
printf(" transport (default: ssh). The command may include\n");
printf(" arguments, e.g. -e \"ssh -p 2222\"\n");
printf(" --rsync-path <path> Alias for --fastsync-server-path (path to the\n");
printf(" fastsync server binary on the remote side)\n");
printf(" --blocking-io Leave the SSH transport socket without read/write\n");
printf(" timeouts so it blocks naturally\n");
printf(" --outbuf=MODE stdout/stderr buffering: N (none/unbuffered),\n");
printf(" L (line-buffered), or B (block-buffered, default)\n");
printf(" --progress Show transfer progress\n");
printf(" -P Partial mode with progress (retention incomplete)\n");
printf(" -8, --8-bit-output Leave high-bit characters unescaped in output\n");
+2 -2
View File
@@ -119,7 +119,8 @@ static void config_set_defaults(Config* config) {
config->dirs = false;
config->mkpath = false;
config->rsh_command = NULL;
config->rsync_path = NULL;
config->blocking_io = false;
config->outbuf = OUTBUF_BLOCK;
config->old_args = false;
config->temp_dir = NULL;
config->basis_dirs = NULL;
@@ -393,7 +394,6 @@ void config_delete(Config* config) {
free(config->files_from);
file_list_destroy((FileListSet*)config->files_from_set);
free(config->rsh_command);
free(config->rsync_path);
free(config->temp_dir);
for (int i = 0; i < config->basis_count; i++) {
free(config->basis_dirs[i].path);
+19 -1
View File
@@ -9,6 +9,15 @@
typedef enum { TRANSPORT_TCP, TRANSPORT_SSH } TransportType;
/* --outbuf stdout/stderr buffering style (client-only launch concern, never
* crosses the wire). OUTBUF_BLOCK is the default, matching the stdio default
* (fully buffered when output is not a terminal). */
typedef enum {
OUTBUF_BLOCK = 0, /* _IOFBF */
OUTBUF_LINE, /* _IOLBF */
OUTBUF_NONE /* _IONBF */
} OutbufMode;
/* Receiver-side staging state for --delay-updates. Forward-declared here so
Config can carry it; the concrete type lives in delay_updates.h. */
typedef struct DelayUpdatesContext DelayUpdatesContext;
@@ -226,8 +235,17 @@ typedef struct Config {
bool mkpath;
// Issue #130: Remote shell/connection options
/* -e/--rsh: the remote-shell program used to establish the SSH transport.
* NULL means the default "ssh". Client-only launch concern: NEVER crosses
* the wire (it is not meaningful to the daemon/server handshake). */
char* rsh_command;
char* rsync_path;
/* --blocking-io: leave the SSH transport socket without
* SO_RCVTIMEO/SO_SNDTIMEO so it blocks naturally instead of timing out.
* Client-only launch concern: NEVER crosses the wire. */
bool blocking_io;
/* --outbuf mode (OutbufMode): stdout/stderr buffering. Client-only launch
* concern: NEVER crosses the wire. */
int outbuf;
bool old_args;
char* temp_dir;
/* Alternate basis directories, ordered by command-line appearance. Each
+113 -22
View File
@@ -7,6 +7,7 @@
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/time.h>
#include <sys/wait.h>
#include <unistd.h>
@@ -16,7 +17,9 @@ typedef struct {
char* remote_path;
} RemoteDest;
static void ssh_child_setup_failed(int status_fd) {
/* Writes the exec-failure marker and exits the child. Marked noreturn so
* static analyzers prove the caller's error path never falls through. */
__attribute__((noreturn)) static void ssh_child_setup_failed(int status_fd) {
ssize_t wret = write(status_fd, "x", 1);
(void)wret;
_exit(1);
@@ -119,8 +122,97 @@ char* ssh_build_remote_command(const char* server_path, bool old_args) {
return command;
}
/* A heap-owned, NULL-terminated argv whose every string is separately malloc'd
* (str_dup'd) so a caller can free arbitrary slots, including argv[0]. */
char** ssh_build_client_argv(const char* rsh_command, int port, const char* userhost,
const char* remote_command) {
const char* rsh = (rsh_command && *rsh_command) ? rsh_command : "ssh";
/* Whitespace-split the remote-shell command into the leading argv words so
* "-e 'ssh -p 2222'" (or "--rsh=ssh -p 2222") works like rsync's rsh. A
* blank command falls back to the default "ssh". */
char* copy = str_dup(rsh);
if (!copy)
return NULL;
char* save = NULL;
int nwords = 0;
char** words = NULL;
for (char* tok = strtok_r(copy, " \t", &save); tok; tok = strtok_r(NULL, " \t", &save)) {
char** grown = realloc(words, (size_t)(nwords + 1) * sizeof(char*));
if (!grown) {
for (int i = 0; i < nwords; i++)
free(words[i]);
free(words);
free(copy);
return NULL;
}
words = grown;
words[nwords] = str_dup(tok);
if (!words[nwords]) {
for (int i = 0; i < nwords; i++)
free(words[i]);
free(words);
free(copy);
return NULL;
}
nwords++;
}
free(copy);
if (nwords == 0) {
words = malloc(sizeof(char*));
if (!words)
return NULL;
words[0] = str_dup("ssh");
if (!words[0]) {
free(words);
return NULL;
}
nwords = 1;
}
/* Fixed tail: three -o pairs (6) + optional -p/value (2) + user@host +
* remote command + terminating NULL. */
int port_extra = (port > 0 && port != 22) ? 2 : 0;
size_t total = (size_t)nwords + 6 + (size_t)port_extra + 3;
char** argv = calloc(total, sizeof(char*));
if (!argv) {
for (int i = 0; i < nwords; i++)
free(words[i]);
free(words);
return NULL;
}
int ac = 0;
for (int i = 0; i < nwords; i++)
argv[ac++] = words[i];
free(words);
char* tail[] = {"-o", "Compression=no",
"-o", "ControlMaster=auto",
"-o", "ControlPath=~/.cache/fastsync-%r@%h:%p"};
for (size_t i = 0; i < sizeof(tail) / sizeof(tail[0]); i++)
argv[ac++] = str_dup(tail[i]);
if (port_extra) {
char port_str[16];
snprintf(port_str, sizeof(port_str), "%d", port);
argv[ac++] = str_dup("-p");
argv[ac++] = str_dup(port_str);
}
argv[ac++] = str_dup(userhost);
argv[ac++] = str_dup(remote_command);
argv[ac] = NULL;
return argv;
}
void ssh_free_client_argv(char** argv) {
if (!argv)
return;
for (int i = 0; argv[i]; i++)
free(argv[i]);
free(argv);
}
Client* client_connect_ssh(const char* destination, int port, const char* server_path,
bool old_args) {
bool old_args, const char* rsh_command, bool blocking_io) {
RemoteDest r;
if (parse_remote_dest(destination, &r) != 0) {
char* escaped = output_escape(destination, false);
@@ -142,6 +234,17 @@ Client* client_connect_ssh(const char* destination, int port, const char* server
setsockopt(sv[1], SOL_SOCKET, SO_SNDBUF, &buf_size, sizeof(buf_size));
setsockopt(sv[1], SOL_SOCKET, SO_RCVBUF, &buf_size, sizeof(buf_size));
/* By default the SSH transport socket gets the same read/write timeout as
* the TCP transport so a wedged remote shell cannot hang forever. With
* --blocking-io the timeouts are skipped and the socket blocks naturally. */
if (!blocking_io) {
struct timeval tv;
tv.tv_sec = tcp_get_timeout_sec();
tv.tv_usec = 0;
setsockopt(sv[0], SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
setsockopt(sv[0], SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv));
}
int exec_pipe[2];
if (pipe(exec_pipe) < 0) {
log_perror("pipe failed");
@@ -187,29 +290,17 @@ Client* client_connect_ssh(const char* destination, int port, const char* server
else
snprintf(ssh_user, ssh_user_len, "%s", r.host);
char* ssh_argv[16];
int ac = 0;
char port_str[16];
char* remote_command = ssh_build_remote_command(server_path, old_args);
if (!remote_command)
ssh_child_setup_failed(exec_pipe[1]);
ssh_argv[ac++] = "ssh";
ssh_argv[ac++] = "-o";
ssh_argv[ac++] = "Compression=no";
ssh_argv[ac++] = "-o";
ssh_argv[ac++] = "ControlMaster=auto";
ssh_argv[ac++] = "-o";
ssh_argv[ac++] = "ControlPath=~/.cache/fastsync-%r@%h:%p";
if (port > 0 && port != 22) {
ssh_argv[ac++] = "-p";
snprintf(port_str, sizeof(port_str), "%d", port);
ssh_argv[ac++] = port_str;
}
ssh_argv[ac++] = ssh_user;
ssh_argv[ac++] = remote_command;
ssh_argv[ac] = NULL;
execvp("ssh", ssh_argv);
log_perror("exec of ssh failed");
char** ssh_argv = ssh_build_client_argv(rsh_command, port, ssh_user, remote_command);
free(ssh_user);
free(remote_command);
if (!ssh_argv)
ssh_child_setup_failed(exec_pipe[1]);
execvp(ssh_argv[0], ssh_argv);
log_perror("exec of remote shell failed");
ssh_free_client_argv(ssh_argv);
ssh_child_setup_failed(exec_pipe[1]);
}
+9 -1
View File
@@ -4,7 +4,15 @@
#include "transport_tcp.h"
Client* client_connect_ssh(const char* destination, int port, const char* server_path,
bool old_args);
bool old_args, const char* rsh_command, bool blocking_io);
char* ssh_build_remote_command(const char* server_path, bool old_args);
/* Build the NULL-terminated child argv for the remote-shell client (argv[0] is
* the exec/execvp program). rsh_command is whitespace-split into leading argv
* words (NULL or "" selects the default "ssh"); the standard -o family, the
* optional -p port, the user@host and the remote command are appended. Every
* string (including argv[0]) is heap-owned; free with ssh_free_client_argv. */
char** ssh_build_client_argv(const char* rsh_command, int port, const char* userhost,
const char* remote_command);
void ssh_free_client_argv(char** argv);
#endif