feat(client): --port alias, --threads=N, graceful abort, keepalive

This commit is contained in:
2026-09-13 06:22:28 +02:00
parent 10b18ab2d2
commit 1fa2fbd266
13 changed files with 501 additions and 36 deletions
+78
View File
@@ -1,5 +1,6 @@
#include "test_client_cli.h"
#include "checksum.h"
#include "client_send.h"
#include "client_validation.h"
#include "chmod.h"
#include "config.h"
@@ -579,6 +580,80 @@ static void test_parse_args_invalid_server_port() {
config_delete(cfg);
}
/* --port is a documented rsync-style alias for --server-port; both the
* two-argument and the inline "=" spellings must work. */
static void test_parse_args_port_alias() {
Config* cfg = config_create();
char* argv_space[] = {"fastsync", "--port", "9000", "/src", "/dst"};
int positional_args[2];
int positional_count = 0;
EXPECT_EQ_INT(parse_args(cfg, 5, argv_space, positional_args, &positional_count), 0);
EXPECT_EQ_INT(cfg->server_port, 9000);
config_delete(cfg);
cfg = config_create();
char* argv_inline[] = {"fastsync", "--port=9001", "/src", "/dst"};
positional_count = 0;
EXPECT_EQ_INT(parse_args(cfg, 4, argv_inline, positional_args, &positional_count), 0);
EXPECT_EQ_INT(cfg->server_port, 9001);
config_delete(cfg);
cfg = config_create();
char* argv_long[] = {"fastsync", "--server-port=9002", "/src", "/dst"};
positional_count = 0;
EXPECT_EQ_INT(parse_args(cfg, 4, argv_long, positional_args, &positional_count), 0);
EXPECT_EQ_INT(cfg->server_port, 9002);
config_delete(cfg);
}
/* --threads=N sizes the pipeline scanner; bare -j/--threads keeps the default
* (scanner_threads == 0), and invalid values are rejected. */
static void test_parse_args_threads() {
Config* cfg = config_create();
char* argv_eq[] = {"fastsync", "--threads=8", "/src", "/dst"};
int positional_args[2];
int positional_count = 0;
EXPECT_EQ_INT(parse_args(cfg, 4, argv_eq, positional_args, &positional_count), 0);
EXPECT_TRUE(cfg->use_multithreading);
EXPECT_EQ_INT(cfg->scanner_threads, 8);
config_delete(cfg);
cfg = config_create();
char* argv_short[] = {"fastsync", "-j", "/src", "/dst"};
positional_count = 0;
EXPECT_EQ_INT(parse_args(cfg, 4, argv_short, positional_args, &positional_count), 0);
EXPECT_TRUE(cfg->use_multithreading);
EXPECT_EQ_INT(cfg->scanner_threads, 0);
config_delete(cfg);
cfg = config_create();
char* argv_long[] = {"fastsync", "--threads", "/src", "/dst"};
positional_count = 0;
EXPECT_EQ_INT(parse_args(cfg, 4, argv_long, positional_args, &positional_count), 0);
EXPECT_TRUE(cfg->use_multithreading);
EXPECT_EQ_INT(cfg->scanner_threads, 0);
config_delete(cfg);
const char* bad[] = {"--threads=0", "--threads=-3", "--threads=abc", "--threads=257"};
for (size_t i = 0; i < sizeof(bad) / sizeof(bad[0]); i++) {
cfg = config_create();
char* argv_bad[] = {"fastsync", (char*)bad[i], "/src", "/dst"};
positional_count = 0;
EXPECT_EQ_INT(parse_args(cfg, 4, argv_bad, positional_args, &positional_count), -1);
config_delete(cfg);
}
}
/* The graceful-abort flag is a plain sig_atomic_t toggled by the handler. */
static void test_client_abort_flag() {
client_abort_requested = 0;
EXPECT_FALSE(client_abort_pending());
client_abort_requested = 1;
EXPECT_TRUE(client_abort_pending());
client_abort_requested = 0;
EXPECT_FALSE(client_abort_pending());
}
/* Test parse_args rejects invalid compression level (-z/--compress) */
static void test_parse_args_invalid_compression_level() {
Config* cfg = config_create();
@@ -3224,6 +3299,9 @@ void test_client_cli() {
test_parse_args_invalid_port();
test_parse_args_non_numeric_port();
test_parse_args_invalid_server_port();
test_parse_args_port_alias();
test_parse_args_threads();
test_client_abort_flag();
test_parse_args_invalid_compression_level();
test_parse_args_valid_compression_level();
test_parse_args_debug_flags();
+93
View File
@@ -460,6 +460,96 @@ static void test_send_receive_status_timed() {
close(p[0]);
}
static bool keepalive_always_abort(void) {
return true;
}
/* A pre-buffered KEEPALIVE reply from the peer must be consumed transparently,
leaving the first real status visible to the caller. */
static void test_receive_status_keepalive_skips_reply() {
int p[2];
EXPECT_EQ_INT(pipe(p), 0);
ProtocolSession session;
protocol_session_init(&session, p[0], p[1]);
EXPECT_TRUE(protocol_send_status(&session, STATUS_KEEPALIVE));
EXPECT_TRUE(protocol_send_status(&session, STATUS_OK));
Status received = STATUS_ERROR;
EXPECT_TRUE(protocol_receive_status_keepalive(&session, &received, 5, 1, NULL));
EXPECT_EQ_INT((int)received, (int)STATUS_OK);
close(p[0]);
close(p[1]);
}
/* The abort callback ends the wait immediately, before any keepalive traffic. */
static void test_receive_status_keepalive_aborts() {
int p[2];
EXPECT_EQ_INT(pipe(p), 0);
ProtocolSession session;
protocol_session_init(&session, p[0], p[1]);
Status received = STATUS_ERROR;
EXPECT_FALSE(
protocol_receive_status_keepalive(&session, &received, 5, 1, keepalive_always_abort));
close(p[0]);
close(p[1]);
}
typedef struct {
int peer_read_fd;
int peer_write_fd;
bool replied;
} KeepalivePeerArg;
static int keepalive_peer(void* arg) {
KeepalivePeerArg* peer = arg;
ProtocolSession session;
protocol_session_init(&session, peer->peer_read_fd, peer->peer_write_fd);
Status status = STATUS_ERROR;
if (protocol_receive_status(&session, &status) && status == STATUS_KEEPALIVE) {
/* Model the busy receiver: it sends the real ack first, then the keepalive
reply it owes for the queued keepalive (which the client must drain so it
does not desynchronize the stream). */
peer->replied = protocol_send_status(&session, STATUS_OK) &&
protocol_send_status(&session, STATUS_KEEPALIVE);
}
return thrd_success;
}
/* While the peer is silent the helper must emit STATUS_KEEPALIVE, then consume
the peer's ack and drain the keepalive reply that follows it -- proving the
inline keepalive loop works without a second writer racing the send path. */
static void test_receive_status_keepalive_emits() {
int to_client[2];
int to_peer[2];
EXPECT_EQ_INT(pipe(to_client), 0);
EXPECT_EQ_INT(pipe(to_peer), 0);
ProtocolSession session;
protocol_session_init(&session, to_client[0], to_peer[1]);
KeepalivePeerArg peer = {.peer_read_fd = to_peer[0], .peer_write_fd = to_client[1]};
thrd_t thread;
EXPECT_EQ_INT(thrd_create(&thread, keepalive_peer, &peer), thrd_success);
Status received = STATUS_ERROR;
EXPECT_TRUE(protocol_receive_status_keepalive(&session, &received, 10, 1, NULL));
EXPECT_EQ_INT((int)received, (int)STATUS_OK);
int result = 0;
EXPECT_EQ_INT(thrd_join(thread, &result), thrd_success);
EXPECT_EQ_INT(result, thrd_success);
EXPECT_TRUE(peer.replied);
close(to_client[0]);
close(to_client[1]);
close(to_peer[0]);
close(to_peer[1]);
}
void test_protocol() {
test_send_receive_n_data();
test_send_receive_n_data_zero();
@@ -471,6 +561,9 @@ void test_protocol() {
test_send_receive_status();
test_protocol_session_io_timeout();
test_send_receive_status_timed();
test_receive_status_keepalive_skips_reply();
test_receive_status_keepalive_aborts();
test_receive_status_keepalive_emits();
test_receive_n_data_truncated();
test_receive_str_truncated();
test_max_alloc_rejects_single_buffer();