feat(client): --port alias, --threads=N, graceful abort, keepalive
This commit is contained in:
+1
-6
@@ -23,6 +23,7 @@ static void config_set_defaults(Config* config) {
|
||||
config->receive_root_directory = NULL;
|
||||
config->save_to_disk = false;
|
||||
config->use_multithreading = false;
|
||||
config->scanner_threads = 0;
|
||||
config->use_chunk_serialization = false;
|
||||
config->use_compression = false;
|
||||
config->use_metadata = false;
|
||||
@@ -79,7 +80,6 @@ static void config_set_defaults(Config* config) {
|
||||
config->stats = false;
|
||||
config->max_depth = 0;
|
||||
config->log_file = NULL;
|
||||
config->queue_size = 100;
|
||||
config->follow_symlinks = false;
|
||||
config->partial = false;
|
||||
config->copy_links = false;
|
||||
@@ -147,14 +147,11 @@ static void config_set_defaults(Config* config) {
|
||||
config->delete_during = false;
|
||||
config->delete_delay = false;
|
||||
config->address = NULL;
|
||||
config->bind_address = NULL;
|
||||
config->ipv6 = false;
|
||||
config->ipv4 = false;
|
||||
config->sockopts = NULL;
|
||||
config->sockopt_count = 0;
|
||||
config->daemon = false;
|
||||
config->daemon_config = NULL;
|
||||
config->server_mode = false;
|
||||
config->no_motd = false;
|
||||
config->checksum = false;
|
||||
config->checksum_algo = CHECKSUM_ALGO_XXH64;
|
||||
@@ -789,9 +786,7 @@ void config_delete(Config* config) {
|
||||
free(config->partial_dir);
|
||||
free(config->suffix);
|
||||
free(config->address);
|
||||
free(config->bind_address);
|
||||
free(config->sockopts);
|
||||
free(config->daemon_config);
|
||||
free(config->compress_choice);
|
||||
free(config->chmod_spec);
|
||||
if (config->skip_compress_suffixes) {
|
||||
|
||||
+6
-6
@@ -81,6 +81,11 @@ typedef struct Config {
|
||||
char* receive_root_directory;
|
||||
bool save_to_disk;
|
||||
bool use_multithreading;
|
||||
/* -j/--threads=N: number of parallel scanner worker threads for the -m
|
||||
* pipeline. 0 (the default, also set by bare -j/--threads) means "use the
|
||||
* scanner's built-in default" (4). CLIENT-ONLY: it is a local scheduling
|
||||
* concern and is NEVER serialized into the wire config frame. */
|
||||
int scanner_threads;
|
||||
bool use_chunk_serialization;
|
||||
bool use_compression;
|
||||
bool use_sendfile;
|
||||
@@ -170,7 +175,6 @@ typedef struct Config {
|
||||
bool stats;
|
||||
int max_depth;
|
||||
FILE* log_file;
|
||||
int queue_size;
|
||||
bool follow_symlinks;
|
||||
bool partial;
|
||||
|
||||
@@ -348,21 +352,17 @@ typedef struct Config {
|
||||
|
||||
// PR #181: IPv6 and bind address
|
||||
char* address;
|
||||
char* bind_address;
|
||||
bool ipv6;
|
||||
bool ipv4;
|
||||
/* --sockopts=OPTIONS (Phase 5, Wave B): strict allowlist of TCP/socket
|
||||
* options applied via setsockopt after socket() and before connect()/bind().
|
||||
* These are LOCAL socket concerns: they never cross the wire config frame.
|
||||
* .address is the outgoing/source bind address (--address); .bind_address is
|
||||
* reserved for daemon-side binding and is not wired yet. */
|
||||
* .address is the outgoing/source bind address (--address). */
|
||||
SockOptEntry* sockopts;
|
||||
int sockopt_count;
|
||||
|
||||
// PR #182: Daemon/server mode
|
||||
bool daemon;
|
||||
char* daemon_config;
|
||||
bool server_mode;
|
||||
/* --no-motd (Wave C): CLIENT-ONLY, never crosses the wire. Suppresses
|
||||
* DISPLAY of the daemon's MOTD; the daemon still sends the MOTD frame, so
|
||||
* the client reads and discards it to keep the stream in sync. rsync's
|
||||
|
||||
@@ -609,6 +609,136 @@ bool protocol_receive_status_timed(ProtocolSession* session, Status* status, int
|
||||
return true;
|
||||
}
|
||||
|
||||
/* Read exactly one Status frame within `deadline` (CLOCK_MONOTONIC). Unlike
|
||||
* protocol_receive_status_keepalive this never emits a keepalive: it is used
|
||||
* to consume the first byte(s) of an already-signalled frame and to drain the
|
||||
* peer's outstanding keepalive replies, where injecting a write could split a
|
||||
* reply across a frame boundary. Returns false on timeout/EOF/error. */
|
||||
static bool protocol_read_status_until(ProtocolSession* session, Status* status,
|
||||
const struct timespec* deadline) {
|
||||
Status received = STATUS_ERROR;
|
||||
size_t got = 0;
|
||||
while (got < sizeof(Status)) {
|
||||
if (!session->ssl || SSL_pending(session->ssl) == 0) {
|
||||
int remaining_ms = deadline_remaining_ms(deadline);
|
||||
if (remaining_ms <= 0) {
|
||||
log_message(LOG_LEVEL_ERROR, "Receive timeout while reading status");
|
||||
return false;
|
||||
}
|
||||
struct pollfd pfd = {.fd = session->read_fd, .events = POLLIN};
|
||||
int poll_result = poll(&pfd, 1, remaining_ms);
|
||||
if (poll_result == 0) {
|
||||
log_message(LOG_LEVEL_ERROR, "Receive timeout while reading status");
|
||||
return false;
|
||||
}
|
||||
if (poll_result < 0) {
|
||||
if (errno == EINTR)
|
||||
continue;
|
||||
return false;
|
||||
}
|
||||
if (pfd.revents & (POLLERR | POLLNVAL))
|
||||
return false;
|
||||
}
|
||||
ssize_t bytes_received;
|
||||
if (session->ssl)
|
||||
bytes_received = SSL_read(session->ssl, (char*)&received + got, sizeof(Status) - got);
|
||||
else
|
||||
bytes_received = read(session->read_fd, (char*)&received + got, sizeof(Status) - got);
|
||||
if (bytes_received <= 0) {
|
||||
if (session->ssl) {
|
||||
int ssl_err = SSL_get_error(session->ssl, (int)bytes_received);
|
||||
if (ssl_err == SSL_ERROR_WANT_READ || ssl_err == SSL_ERROR_WANT_WRITE)
|
||||
continue;
|
||||
}
|
||||
if (bytes_received < 0 && errno == EINTR)
|
||||
continue;
|
||||
log_message(LOG_LEVEL_ERROR, "Connection closed while receiving status");
|
||||
return false;
|
||||
}
|
||||
got += (size_t)bytes_received;
|
||||
}
|
||||
*status = received;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool protocol_receive_status_keepalive(ProtocolSession* session, Status* status, int timeout_sec,
|
||||
int keepalive_interval_sec, ProtocolWaitAbort abort_check) {
|
||||
if (!session || !status)
|
||||
return false;
|
||||
if (timeout_sec <= 0)
|
||||
timeout_sec = RECEIVE_TIMEOUT_SEC;
|
||||
if (keepalive_interval_sec <= 0)
|
||||
keepalive_interval_sec = timeout_sec;
|
||||
|
||||
struct timespec deadline;
|
||||
clock_gettime(CLOCK_MONOTONIC, &deadline);
|
||||
deadline.tv_sec += timeout_sec;
|
||||
|
||||
unsigned long keepalives_sent = 0;
|
||||
unsigned long replies_seen = 0;
|
||||
Status final = STATUS_ERROR;
|
||||
while (true) {
|
||||
if (abort_check && abort_check())
|
||||
return false;
|
||||
if (!session->ssl || SSL_pending(session->ssl) == 0) {
|
||||
int remaining_ms = deadline_remaining_ms(&deadline);
|
||||
if (remaining_ms <= 0) {
|
||||
log_message(LOG_LEVEL_ERROR, "Receive timeout after %ds", timeout_sec);
|
||||
return false;
|
||||
}
|
||||
/* Only interleave a keepalive while waiting for the FIRST byte of a
|
||||
* frame; once part of a frame is buffered a write could race the peer's
|
||||
* reply into the middle of it. */
|
||||
int interval_ms = keepalive_interval_sec * 1000;
|
||||
int wait_ms = interval_ms < remaining_ms ? interval_ms : remaining_ms;
|
||||
struct pollfd pfd = {.fd = session->read_fd, .events = POLLIN};
|
||||
int poll_result = poll(&pfd, 1, wait_ms);
|
||||
if (poll_result == 0) {
|
||||
if (abort_check && abort_check())
|
||||
return false;
|
||||
if (!protocol_send_status(session, STATUS_KEEPALIVE))
|
||||
return false;
|
||||
keepalives_sent++;
|
||||
continue;
|
||||
}
|
||||
if (poll_result < 0) {
|
||||
if (errno == EINTR)
|
||||
continue;
|
||||
return false;
|
||||
}
|
||||
if (pfd.revents & (POLLERR | POLLNVAL))
|
||||
return false;
|
||||
}
|
||||
Status received;
|
||||
if (!protocol_read_status_until(session, &received, &deadline))
|
||||
return false;
|
||||
if (received == STATUS_KEEPALIVE) {
|
||||
/* The receiver's answer to one of our keepalives. */
|
||||
replies_seen++;
|
||||
continue;
|
||||
}
|
||||
final = received;
|
||||
break;
|
||||
}
|
||||
/* Drain the replies the receiver still owes for keepalives we sent while it
|
||||
* was busy. It answers them only after the real status, so leaving them
|
||||
* unread would put stale KEEPALIVE frames ahead of the next exchange and
|
||||
* desynchronize the protocol. */
|
||||
while (replies_seen < keepalives_sent) {
|
||||
Status drained;
|
||||
if (!protocol_read_status_until(session, &drained, &deadline))
|
||||
return false;
|
||||
if (drained != STATUS_KEEPALIVE) {
|
||||
log_message(LOG_LEVEL_ERROR, "Unexpected status while draining keepalive replies");
|
||||
return false;
|
||||
}
|
||||
replies_seen++;
|
||||
}
|
||||
*status = final;
|
||||
log_debug_message(LOG_DEBUG_PROTO, "Received Status: %s", status_to_string(*status));
|
||||
return true;
|
||||
}
|
||||
|
||||
bool send_str(int fd, const char* data) {
|
||||
return protocol_send_str(legacy_session(-1, fd), data);
|
||||
}
|
||||
@@ -648,3 +778,8 @@ bool receive_status(int fd, Status* status) {
|
||||
bool receive_status_timed(int fd, Status* status, int timeout_sec) {
|
||||
return protocol_receive_status_timed(legacy_session(fd, -1), status, timeout_sec);
|
||||
}
|
||||
bool receive_status_keepalive(int fd, Status* status, int timeout_sec, int keepalive_interval_sec,
|
||||
ProtocolWaitAbort abort_check) {
|
||||
return protocol_receive_status_keepalive(legacy_session(fd, -1), status, timeout_sec,
|
||||
keepalive_interval_sec, abort_check);
|
||||
}
|
||||
|
||||
@@ -198,4 +198,23 @@ bool receive_status(int file_descriptor, Status* status);
|
||||
this so the sender does not abort after the deletion already committed. */
|
||||
bool receive_status_timed(int file_descriptor, Status* status, int timeout_sec);
|
||||
|
||||
/* Callback polled by protocol_receive_status_keepalive once per keepalive
|
||||
interval. Return true to stop waiting (e.g. a SIGINT/SIGTERM abort flag was
|
||||
set). Kept as a function pointer so the protocol layer does not depend on
|
||||
client signal state. */
|
||||
typedef bool (*ProtocolWaitAbort)(void);
|
||||
|
||||
/* Like receive_status_timed, but while the peer is silent it emits
|
||||
STATUS_KEEPALIVE every keepalive_interval_sec (the receiver answers each with
|
||||
STATUS_KEEPALIVE, which this function consumes and skips) so a long
|
||||
server-side operation does not look like a dead connection. The total wait
|
||||
is still bounded by timeout_sec; abort_check (may be NULL) is polled every
|
||||
interval and, when it returns true, ends the wait immediately with false.
|
||||
Runs entirely on the calling thread: the protocol send path is NOT safe for
|
||||
concurrent writers, so this must not be paired with a helper thread. */
|
||||
bool receive_status_keepalive(int file_descriptor, Status* status, int timeout_sec,
|
||||
int keepalive_interval_sec, ProtocolWaitAbort abort_check);
|
||||
bool protocol_receive_status_keepalive(ProtocolSession* session, Status* status, int timeout_sec,
|
||||
int keepalive_interval_sec, ProtocolWaitAbort abort_check);
|
||||
|
||||
#endif
|
||||
|
||||
Reference in New Issue
Block a user