feat(protocol): client-message channel and rsync partial exit 23 (2.30.0)
This commit is contained in:
@@ -433,12 +433,10 @@ static int set_stderr_mode(const char* value) {
|
||||
log_set_stderr_mode(LOG_STDERR_ERRORS);
|
||||
else if (strcmp(value, "all") == 0 || strcmp(value, "a") == 0)
|
||||
log_set_stderr_mode(LOG_STDERR_ALL);
|
||||
else if (strcmp(value, "client") == 0 || strcmp(value, "c") == 0) {
|
||||
log_message(LOG_LEVEL_ERROR,
|
||||
"--stderr=client is not supported: FastSync has no client message channel");
|
||||
return -1;
|
||||
} else {
|
||||
log_message(LOG_LEVEL_ERROR, "--stderr must be errors or all");
|
||||
else if (strcmp(value, "client") == 0 || strcmp(value, "c") == 0)
|
||||
log_set_stderr_mode(LOG_STDERR_CLIENT);
|
||||
else {
|
||||
log_message(LOG_LEVEL_ERROR, "--stderr must be errors, all, or client");
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
@@ -1353,10 +1351,9 @@ static bool cli_handle_pre_negation(CliParseCtx* ctx) {
|
||||
return true;
|
||||
}
|
||||
/* "--no-msgs2stderr" is the deprecated spelling of --stderr=client (rsync
|
||||
* 3.4.1). FastSync has no separate client message channel, so the closest
|
||||
* supported mode is the errors-only default. */
|
||||
* 3.4.1); the client-message channel now exists, so it maps to `client`. */
|
||||
if (strcmp(arg, "--no-msgs2stderr") == 0)
|
||||
return set_stderr_mode("errors") == 0;
|
||||
return set_stderr_mode("client") == 0;
|
||||
/* "--no-motd" is a real rsync option name (client-side daemon MOTD display
|
||||
* suppression), not a negation of a "--motd" flag, so it is handled before
|
||||
* the generic --no-* negation branch. */
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <threads.h>
|
||||
#include <time.h>
|
||||
|
||||
/* Surface a server rejection to the user. When the last status exchange
|
||||
@@ -1051,3 +1052,109 @@ const char* delete_display_path(const Config* config, const char* path) {
|
||||
return path;
|
||||
return utils_strip_transfer_root(path, config->send_directory);
|
||||
}
|
||||
|
||||
/* ---- --stderr=client diagnostic channel (protocol 2.30.0) ----
|
||||
*
|
||||
* When the client's --stderr mode is `client`, log_message() hands each of the
|
||||
* client's own diagnostics to the sink installed here instead of writing them
|
||||
* locally. The sink QUEUES the text (it may be called from scanner worker
|
||||
* threads while the sender is streaming) and the sender thread -- the sole
|
||||
* writer of the protocol stream -- drains the queue over the wire at frame
|
||||
* boundaries via client_flush_client_messages(). A bounded queue caps the
|
||||
* memory a chatty run can pin; overflow falls back to local output so a
|
||||
* diagnostic is never silently dropped. */
|
||||
#define CLIENT_MSG_MAX_QUEUED 256
|
||||
#define CLIENT_MSG_MAX_BYTES (256 * 1024)
|
||||
|
||||
static mtx_t client_msg_mutex;
|
||||
static once_flag client_msg_mutex_once = ONCE_FLAG_INIT;
|
||||
static ArrayList* client_msg_queue = NULL; /* owns char* */
|
||||
static size_t client_msg_bytes = 0;
|
||||
/* True only while a live transfer session exists: before the connection is up
|
||||
(or after it drops) the sink declines so log_message falls back to local
|
||||
output, matching rsync's documented fallback. */
|
||||
static bool client_msg_active = false;
|
||||
|
||||
static void client_msg_mutex_init(void) {
|
||||
mtx_init(&client_msg_mutex, mtx_plain);
|
||||
}
|
||||
|
||||
static bool client_msg_enqueue(const char* message);
|
||||
|
||||
/* Install the global log sink for the duration of one transfer. Safe to call
|
||||
* more than once; the queue is created lazily. */
|
||||
void client_messages_install(void) {
|
||||
call_once(&client_msg_mutex_once, client_msg_mutex_init);
|
||||
mtx_lock(&client_msg_mutex);
|
||||
if (!client_msg_queue)
|
||||
client_msg_queue = array_list_create(free);
|
||||
mtx_unlock(&client_msg_mutex);
|
||||
log_set_client_msg_sink(client_msg_enqueue);
|
||||
}
|
||||
|
||||
void client_messages_activate(bool active) {
|
||||
client_msg_active = active;
|
||||
}
|
||||
|
||||
/* log_message sink: takes ownership (queues) the message when a session is
|
||||
* live; returns false otherwise so the caller writes it locally. */
|
||||
static bool client_msg_enqueue(const char* message) {
|
||||
if (!message || message[0] == '\0')
|
||||
return client_msg_active;
|
||||
if (!client_msg_active)
|
||||
return false;
|
||||
size_t len = strlen(message);
|
||||
call_once(&client_msg_mutex_once, client_msg_mutex_init);
|
||||
mtx_lock(&client_msg_mutex);
|
||||
bool queued = false;
|
||||
if (client_msg_queue && (size_t)client_msg_queue->size < CLIENT_MSG_MAX_QUEUED &&
|
||||
client_msg_bytes + len <= CLIENT_MSG_MAX_BYTES) {
|
||||
char* copy = str_dup(message);
|
||||
if (copy) {
|
||||
if (array_list_add(client_msg_queue, copy)) {
|
||||
client_msg_bytes += len;
|
||||
queued = true;
|
||||
} else {
|
||||
free(copy);
|
||||
}
|
||||
}
|
||||
}
|
||||
mtx_unlock(&client_msg_mutex);
|
||||
return queued;
|
||||
}
|
||||
|
||||
/* Drain the queued diagnostics as STATUS_CLIENT_MSG frames on the sender
|
||||
* thread. Swaps the queue out under the mutex so a concurrent worker logging
|
||||
* never blocks on the wire. Must be called at a protocol frame boundary. */
|
||||
void client_flush_client_messages(int fd) {
|
||||
if (fd < 0)
|
||||
return;
|
||||
call_once(&client_msg_mutex_once, client_msg_mutex_init);
|
||||
mtx_lock(&client_msg_mutex);
|
||||
ArrayList* pending = client_msg_queue;
|
||||
client_msg_queue = array_list_create(free);
|
||||
client_msg_bytes = 0;
|
||||
mtx_unlock(&client_msg_mutex);
|
||||
if (!pending)
|
||||
return;
|
||||
for (int i = 0; i < pending->size; i++) {
|
||||
const char* message = pending->items[i];
|
||||
if (message && message[0] != '\0' && !send_client_message(fd, message))
|
||||
break; /* peer is gone; the rest would fail too */
|
||||
}
|
||||
array_list_delete(pending);
|
||||
}
|
||||
|
||||
/* Tear down the sink after a transfer and free anything still queued. */
|
||||
void client_messages_end(void) {
|
||||
log_set_client_msg_sink(NULL);
|
||||
client_msg_active = false;
|
||||
call_once(&client_msg_mutex_once, client_msg_mutex_init);
|
||||
mtx_lock(&client_msg_mutex);
|
||||
ArrayList* pending = client_msg_queue;
|
||||
client_msg_queue = NULL;
|
||||
client_msg_bytes = 0;
|
||||
mtx_unlock(&client_msg_mutex);
|
||||
if (pending)
|
||||
array_list_delete(pending);
|
||||
}
|
||||
|
||||
+63
-10
@@ -127,6 +127,11 @@ Client* connect_transfer_client(const Config* config) {
|
||||
void disconnect_transfer_client(Client* client) {
|
||||
if (!client)
|
||||
return;
|
||||
/* --stderr=client: push any diagnostics logged during the transfer to the
|
||||
peer before the socket closes; once deactivated, later messages fall back
|
||||
to local output instead of being lost. */
|
||||
client_flush_client_messages(client->file_descriptor);
|
||||
client_messages_activate(false);
|
||||
client_disconnect(client);
|
||||
client_delete(client);
|
||||
}
|
||||
@@ -240,11 +245,16 @@ static void mark_sender_done(PipelineContextSender* context) {
|
||||
When --remove-source-files is active the receiver acknowledges each data
|
||||
file it processed, in send order: STATUS_NEXT means the file was written,
|
||||
STATUS_OK means the file was skipped/unchanged. Skipped sources are marked
|
||||
so the later removal pass keeps them. */
|
||||
so the later removal pass keeps them. `partial_out` is set when the receiver
|
||||
reported STATUS_PARTIAL (a per-entry receiver failure): the transfer is
|
||||
otherwise complete, so successfully stored sources are still removed and the
|
||||
caller exits 23 (rsync's partial transfer) instead of a fatal non-zero. */
|
||||
static bool finalize_transfer(Client* client, const Config* config, ArrayList* remove_sources,
|
||||
bool* delete_limit_out, ReceiverStats* stats_out) {
|
||||
bool* delete_limit_out, bool* partial_out, ReceiverStats* stats_out) {
|
||||
if (delete_limit_out)
|
||||
*delete_limit_out = false;
|
||||
if (partial_out)
|
||||
*partial_out = false;
|
||||
if (!send_status(client->file_descriptor, STATUS_FINISHED))
|
||||
return false;
|
||||
/* The receiver emits its optional wire-stats frame (protocol 2.25.0) FIRST,
|
||||
@@ -298,6 +308,16 @@ static bool finalize_transfer(Client* client, const Config* config, ArrayList* r
|
||||
*delete_limit_out = true;
|
||||
return true;
|
||||
}
|
||||
/* A per-entry receiver failure the receiver chose to continue past is a
|
||||
rsync PARTIAL transfer: everything else succeeded and the stored sources
|
||||
may be removed, but the client must exit 23. */
|
||||
if (status == STATUS_PARTIAL) {
|
||||
log_message(LOG_LEVEL_WARNING,
|
||||
"some files could not be transferred (see the server log for details)");
|
||||
if (partial_out)
|
||||
*partial_out = true;
|
||||
return true;
|
||||
}
|
||||
if (status != STATUS_OK) {
|
||||
log_server_rejection("Receiver reported transfer failure");
|
||||
return false;
|
||||
@@ -815,6 +835,9 @@ static bool source_is_regular_file(const File* file) {
|
||||
|
||||
static int send_chunk_with_removal(Client* client, Chunk* chunk, Config* config,
|
||||
ArrayList* remove_sources, TransferStats* stats) {
|
||||
/* --stderr=client: this is a frame boundary, so forward any diagnostics the
|
||||
scanner/log emitted since the previous chunk before the next frame. */
|
||||
client_flush_client_messages(client->file_descriptor);
|
||||
if (config->use_chunk_serialization) {
|
||||
if (remove_sources) {
|
||||
for (int i = 0; i < chunk->element_count; i++) {
|
||||
@@ -954,6 +977,7 @@ static int send_chunks_multithreaded(void* pipeline_context) {
|
||||
protocol_session_set_io_timeout(&session, context->config->timeout);
|
||||
protocol_session_set_ssl(&session, (SSL*)client->ssl);
|
||||
protocol_session_bind(&session);
|
||||
client_messages_activate(true);
|
||||
if (!config_send(client->file_descriptor, context->config)) {
|
||||
pipeline_cancel(context);
|
||||
disconnect_transfer_client(client);
|
||||
@@ -1117,11 +1141,14 @@ static int send_chunks_multithreaded(void* pipeline_context) {
|
||||
!send_dir_times(client, context->config, context->dir_entries))
|
||||
goto send_fail;
|
||||
bool delete_limit = false;
|
||||
bool partial = false;
|
||||
ReceiverStats recv_stats;
|
||||
memset(&recv_stats, 0, sizeof(recv_stats));
|
||||
client_flush_client_messages(client->file_descriptor);
|
||||
bool ok = finalize_transfer(client, context->config, context->remove_source_files, &delete_limit,
|
||||
&recv_stats);
|
||||
&partial, &recv_stats);
|
||||
context->delete_limit = delete_limit;
|
||||
context->partial = partial;
|
||||
if (!ok && context->config->use_delete)
|
||||
log_message(LOG_LEVEL_ERROR,
|
||||
"server reported a deletion failure (--delete); see the server log for the reason");
|
||||
@@ -1823,9 +1850,12 @@ static int send_files_finalize(const Config* config, SendFilesState* state) {
|
||||
if (!send_dir_times(client, config, state->dir_entries))
|
||||
return 1;
|
||||
bool delete_limit = false;
|
||||
bool partial = false;
|
||||
ReceiverStats recv_stats;
|
||||
memset(&recv_stats, 0, sizeof(recv_stats));
|
||||
bool ok = finalize_transfer(client, config, state->remove_sources, &delete_limit, &recv_stats);
|
||||
client_flush_client_messages(client->file_descriptor);
|
||||
bool ok = finalize_transfer(client, config, state->remove_sources, &delete_limit, &partial,
|
||||
&recv_stats);
|
||||
if (!ok && config->use_delete)
|
||||
log_message(LOG_LEVEL_ERROR,
|
||||
"server reported a deletion failure (--delete); see the server log for the reason");
|
||||
@@ -1843,12 +1873,12 @@ static int send_files_finalize(const Config* config, SendFilesState* state) {
|
||||
(double)state->transfer_stats.transferred_file_size / (double)BYTES_PER_MIB);
|
||||
/* A skipped source entry (--ignore-errors past an unreadable directory, or a
|
||||
dereferenced symlink with no referent) makes rsync report a partial
|
||||
transfer (exit 23) even though the rest of the run succeeded. A
|
||||
--max-delete-capped commit is a successful transfer that rsync reports
|
||||
transfer (exit 23), as does a receiver per-entry failure (STATUS_PARTIAL).
|
||||
A --max-delete-capped commit is a successful transfer that rsync reports
|
||||
with exit code 25. */
|
||||
if (!ok)
|
||||
return 1;
|
||||
if (state->had_scan_io)
|
||||
if (state->had_scan_io || partial)
|
||||
return 23;
|
||||
return delete_limit ? 25 : 0;
|
||||
}
|
||||
@@ -1885,7 +1915,18 @@ static void send_files_cleanup(SendFilesState* state) {
|
||||
client_set_abort_armed(false);
|
||||
}
|
||||
|
||||
static int send_files_impl(Config* config);
|
||||
|
||||
int send_files(Config* config) {
|
||||
/* Install the --stderr=client sink for the whole run (it only queues while a
|
||||
session is live) and release its queue on every return path. */
|
||||
client_messages_install();
|
||||
int rc = send_files_impl(config);
|
||||
client_messages_end();
|
||||
return rc;
|
||||
}
|
||||
|
||||
static int send_files_impl(Config* config) {
|
||||
if (config->list_only)
|
||||
return send_list_only(config);
|
||||
if (config->dry_run)
|
||||
@@ -1931,6 +1972,7 @@ int send_files(Config* config) {
|
||||
protocol_session_set_io_timeout(&session, config->timeout);
|
||||
protocol_session_set_ssl(&session, (SSL*)client->ssl);
|
||||
protocol_session_bind(&session);
|
||||
client_messages_activate(true);
|
||||
|
||||
int ret = 1;
|
||||
if (!send_files_prepare(config, &state))
|
||||
@@ -1946,7 +1988,16 @@ send_fail:
|
||||
return ret;
|
||||
}
|
||||
|
||||
static int send_files_multithreaded_impl(Config* config);
|
||||
|
||||
int send_files_multithreaded(Config* config) {
|
||||
client_messages_install();
|
||||
int rc = send_files_multithreaded_impl(config);
|
||||
client_messages_end();
|
||||
return rc;
|
||||
}
|
||||
|
||||
static int send_files_multithreaded_impl(Config* config) {
|
||||
if (!config)
|
||||
return 1;
|
||||
if (config->list_only)
|
||||
@@ -2202,16 +2253,18 @@ int send_files_multithreaded(Config* config) {
|
||||
mtx_unlock(&context->mutex_scanner);
|
||||
bool sender_ok = sender_result == thrd_success;
|
||||
bool delete_limit = context->delete_limit;
|
||||
bool partial = context->partial;
|
||||
/* A skipped source entry (--ignore-errors past an unreadable directory, or a
|
||||
dereferenced symlink with no referent) makes rsync report a partial
|
||||
transfer (exit 23). A --max-delete-capped commit is a successful transfer
|
||||
that rsync reports with exit code 25. */
|
||||
transfer (exit 23), as does a receiver per-entry failure (STATUS_PARTIAL).
|
||||
A --max-delete-capped commit is a successful transfer that rsync reports
|
||||
with exit code 25. */
|
||||
pipeline_context_sender_destroy(context);
|
||||
client_progress_cleanup();
|
||||
client_set_abort_armed(false);
|
||||
if (!sender_ok)
|
||||
return 1;
|
||||
if (scan_io)
|
||||
if (scan_io || partial)
|
||||
return 23;
|
||||
return delete_limit ? 25 : 0;
|
||||
}
|
||||
|
||||
@@ -72,6 +72,13 @@ void client_progress_uptodate(const Config* config, const File* file);
|
||||
void client_progress_prepare(const Config* config, const ArrayList* plan_dirs,
|
||||
unsigned long long plan_non_dir_count);
|
||||
bool receive_stats_record(int fd, ReceiverStats* stats, ArrayList* would_delete);
|
||||
/* --stderr=client diagnostic channel (client_report.c): install the queueing
|
||||
* log sink for a transfer, mark the session live, flush queued diagnostics over
|
||||
* the wire at a frame boundary, and tear the sink down. */
|
||||
void client_messages_install(void);
|
||||
void client_messages_activate(bool active);
|
||||
void client_flush_client_messages(int fd);
|
||||
void client_messages_end(void);
|
||||
|
||||
/* client_send.c */
|
||||
void receive_daemon_motd(Client* client, const Config* config);
|
||||
|
||||
+5
-3
@@ -297,11 +297,13 @@ void print_usage(void) {
|
||||
printf(" --max-depth <n> Maximum directory depth (0=unlimited)\n");
|
||||
printf(" -x, --one-file-system Do not cross filesystem boundaries\n");
|
||||
printf(" --log-file <path>, --log-file=<path> Write log messages to file\n");
|
||||
printf(" --stderr=MODE Route logging to stderr: errors or all\n");
|
||||
printf(" --stderr=MODE Route logging: errors (default), all, or client\n");
|
||||
printf(" (forward the client's diagnostics to the server's\n");
|
||||
printf(" stderr)\n");
|
||||
printf(" --msgs2stderr Route all messages to stderr (deprecated spelling of\n");
|
||||
printf(" --stderr=all)\n");
|
||||
printf(" --no-msgs2stderr Select errors-only stderr (deprecated spelling; the\n");
|
||||
printf(" default)\n");
|
||||
printf(" --no-msgs2stderr Forward the client's diagnostics to the server\n");
|
||||
printf(" (deprecated spelling of --stderr=client)\n");
|
||||
printf(" --partial Keep partial files on interrupted transfer\n");
|
||||
printf(" --partial-dir <dir> Directory for partial files (implies --partial)\n");
|
||||
printf(" -T, --temp-dir <dir> Scratch dir for temp files before atomic install.\n");
|
||||
|
||||
+28
-5
@@ -277,6 +277,7 @@ static bool status_counts_as_progress(Status status) {
|
||||
case STATUS_ABORT:
|
||||
case STATUS_CHECK_BATCH:
|
||||
case STATUS_DIR_TIMES:
|
||||
case STATUS_CLIENT_MSG:
|
||||
return false;
|
||||
default:
|
||||
return true;
|
||||
@@ -347,6 +348,25 @@ static ReceiverStep receiver_handle_keepalive(ReceiverPendingState* state) {
|
||||
return RECEIVER_STEP_NEXT;
|
||||
}
|
||||
|
||||
/* rsync --stderr=client: a client diagnostic forwarded over the wire. Read the
|
||||
* bounded string and write it to the server's stderr (respecting the server log
|
||||
* destination). The body is peer-controlled text, so it is logged verbatim
|
||||
* (log_client_message adds the standard prefix); trailing newlines are stripped
|
||||
* so a message cannot inject a blank line. A malformed string (over-long or
|
||||
* embedded NUL) is a framing error and tears the connection down. */
|
||||
static ReceiverStep receiver_handle_client_msg(ReceiverPendingState* state) {
|
||||
char* message = receive_str(state->fd);
|
||||
if (!message)
|
||||
return RECEIVER_STEP_FAIL;
|
||||
size_t len = strlen(message);
|
||||
while (len > 0 && (message[len - 1] == '\n' || message[len - 1] == '\r'))
|
||||
message[--len] = '\0';
|
||||
if (message[0] != '\0')
|
||||
log_client_message(message);
|
||||
free(message);
|
||||
return RECEIVER_STEP_NEXT;
|
||||
}
|
||||
|
||||
static ReceiverStep receiver_handle_abort(ReceiverPendingState* state) {
|
||||
(void)state;
|
||||
log_message(LOG_LEVEL_INFO, "Received abort from client, cleaning up");
|
||||
@@ -527,6 +547,8 @@ static ReceiverStep receiver_dispatch_status(ReceiverPendingState* state, Status
|
||||
switch (status) {
|
||||
case STATUS_KEEPALIVE:
|
||||
return receiver_handle_keepalive(state);
|
||||
case STATUS_CLIENT_MSG:
|
||||
return receiver_handle_client_msg(state);
|
||||
case STATUS_ABORT:
|
||||
return receiver_handle_abort(state);
|
||||
case STATUS_CHECK:
|
||||
@@ -610,7 +632,7 @@ int receiver_process_pending(Config* config, int file_descriptor, const Receiver
|
||||
status == STATUS_KEEPALIVE || status == STATUS_ABORT || status == STATUS_CHECK_BATCH ||
|
||||
status == STATUS_MKDIR || status == STATUS_MANIFEST || status == STATUS_HARDLINK ||
|
||||
status == STATUS_SYMLINK || status == STATUS_SPECIAL || status == STATUS_DIR_TIMES ||
|
||||
status == STATUS_DELETE_PLAN) {
|
||||
status == STATUS_DELETE_PLAN || status == STATUS_CLIENT_MSG) {
|
||||
ReceiverStep step = receiver_dispatch_status(&state, status);
|
||||
if (step == RECEIVER_STEP_FAIL)
|
||||
goto fail;
|
||||
@@ -793,13 +815,14 @@ static void receiver_note_delete_limit(void* context_pointer) {
|
||||
|
||||
/* Terminal status for a run. A capped --delete limit wins (rsync exit 25);
|
||||
otherwise any per-entry failure (for example an unprivileged --devices
|
||||
mknod) makes the terminal frame non-OK so the client exits non-zero. rsync
|
||||
reports 23 here; mapping the client's exact exit code to 23 is a separate,
|
||||
pre-existing concern. A clean run keeps STATUS_OK. */
|
||||
mknod) makes the terminal frame STATUS_PARTIAL so the client exits 23
|
||||
(rsync's "partial transfer due to error") while still removing the sources
|
||||
it successfully transferred under --remove-source-files. A fatal stream
|
||||
error keeps STATUS_ERROR (a non-23 exit). A clean run keeps STATUS_OK. */
|
||||
static Status receiver_final_status(bool delete_limit_reached, size_t failed_entries) {
|
||||
if (delete_limit_reached)
|
||||
return STATUS_DELETE_LIMIT;
|
||||
return failed_entries > 0 ? STATUS_ERROR : STATUS_OK;
|
||||
return failed_entries > 0 ? STATUS_PARTIAL : STATUS_OK;
|
||||
}
|
||||
|
||||
static bool receiver_send_success_frame(int fd, void* context_pointer) {
|
||||
|
||||
+1
-1
@@ -1050,7 +1050,7 @@ static void server_run_mt_receiver(ServerSession* state) {
|
||||
context->failed_entries, context->failed_entries == 1 ? "y" : "ies");
|
||||
Status final_status = context->delete_limit_reached
|
||||
? STATUS_DELETE_LIMIT
|
||||
: (context->failed_entries > 0 ? STATUS_ERROR : STATUS_OK);
|
||||
: (context->failed_entries > 0 ? STATUS_PARTIAL : STATUS_OK);
|
||||
/* Emit the optional wire-stats record first (protocol 2.25.0), then the
|
||||
success/outcome frame, exactly like the single-threaded receiver. */
|
||||
if (!receiver_send_stats_frame(state->fd, config, &context->stats, context->would_delete,
|
||||
|
||||
+15
-2
@@ -83,7 +83,7 @@ typedef struct {
|
||||
typedef enum SuperMode { SUPER_MODE_AUTO = 0, SUPER_MODE_ON = 1, SUPER_MODE_OFF = 2 } SuperMode;
|
||||
|
||||
/* ===========================================================================
|
||||
* Config wire-field table (single source of truth for protocol 2.29.0).
|
||||
* Config wire-field table (single source of truth for protocol 2.30.0).
|
||||
*
|
||||
* Every field below crosses the wire. The table is the ONLY place a
|
||||
* serialized field is named: config.h expands CONFIG_WIRE_FIELDS() to declare
|
||||
@@ -1084,7 +1084,20 @@ typedef struct Config {
|
||||
* version must bump; the strict same-version handshake (config_receive rejects a
|
||||
* mismatched version before parsing anything else) keeps a 2.29 client and a
|
||||
* 2.28 server from ever reaching that state. */
|
||||
#define PROTOCOL_VERSION "2.29.0"
|
||||
/* (11) Client-message channel + partial exit (protocol 2.30.0): the
|
||||
* config-frame LAYOUT is unchanged (no new config field), but the frame stream
|
||||
* gains two statuses. STATUS_CLIENT_MSG (client->server) carries a bounded,
|
||||
* length-prefixed diagnostic string so a client running with --stderr=client
|
||||
* (rsync's --no-msgs2stderr spelling) can forward its own diagnostics to the
|
||||
* server's stderr. STATUS_PARTIAL (receiver->client) is the terminal status
|
||||
* sent instead of STATUS_OK when a per-entry receiver failure (e.g. an
|
||||
* unprivileged --devices mknod) did not abort the stream; the sender exits 23
|
||||
* (rsync's partial transfer) and still removes successfully transferred
|
||||
* --remove-source-files sources. A 2.29 peer that does not know these status
|
||||
* values would reject them as an unknown status and tear the connection down,
|
||||
* so the protocol version must bump; the strict same-version handshake keeps a
|
||||
* 2.30 client and a 2.29 server from ever reaching that state. */
|
||||
#define PROTOCOL_VERSION "2.30.0"
|
||||
#define DEFAULT_CHUNK_SIZE (10 * 1024 * 1024)
|
||||
/* Upper bound on total basis-dir entries (rsync caps --link-dest at 20). */
|
||||
#define MAX_BASIS_DIRS 64
|
||||
|
||||
+80
-20
@@ -16,6 +16,7 @@ static bool info_flags_explicit = false;
|
||||
static FILE* log_fp = NULL;
|
||||
static _Thread_local bool eight_bit_output;
|
||||
static LogStderrMode stderr_mode = LOG_STDERR_ERRORS;
|
||||
static LogClientMsgSink client_msg_sink = NULL;
|
||||
|
||||
/* Serializes access to log_fp and makes each emitted line atomic: the
|
||||
* timestamp prefix, formatted body, and trailing newline are written as one
|
||||
@@ -77,6 +78,51 @@ LogStderrMode log_get_stderr_mode(void) {
|
||||
return stderr_mode;
|
||||
}
|
||||
|
||||
void log_set_client_msg_sink(LogClientMsgSink sink) {
|
||||
client_msg_sink = sink;
|
||||
}
|
||||
|
||||
LogClientMsgSink log_get_client_msg_sink(void) {
|
||||
return client_msg_sink;
|
||||
}
|
||||
|
||||
/* Format just the message body (no prefix/newline) into a freshly allocated
|
||||
* buffer. Shared by log_message (which may hand the body to a client-message
|
||||
* sink) and log_client_message. Returns NULL on allocation/format failure. */
|
||||
static char* format_log_body(const char* format, va_list args) {
|
||||
va_list copy;
|
||||
va_copy(copy, args);
|
||||
int body_len = vsnprintf(NULL, 0, format, copy);
|
||||
va_end(copy);
|
||||
if (body_len < 0)
|
||||
return NULL;
|
||||
char* body = malloc((size_t)body_len + 1);
|
||||
if (!body)
|
||||
return NULL;
|
||||
vsnprintf(body, (size_t)body_len + 1, format, args);
|
||||
return body;
|
||||
}
|
||||
|
||||
/* Assemble a complete log line (prefix + body + newline) from an already
|
||||
* formatted body. Returns NULL on allocation failure. */
|
||||
static char* format_log_line_from_body(LogLevel log_level, const struct tm* t, const char* body) {
|
||||
char prefix[64];
|
||||
int prefix_len = snprintf(
|
||||
prefix, sizeof(prefix), "%04d-%02d-%02d %02d:%02d:%02d [%s]: ", t->tm_year + 1900,
|
||||
t->tm_mon + 1, t->tm_mday, t->tm_hour, t->tm_min, t->tm_sec, log_level_strings[log_level]);
|
||||
if (prefix_len < 0 || prefix_len >= (int)sizeof(prefix))
|
||||
return NULL;
|
||||
size_t body_len = strlen(body);
|
||||
char* line = malloc((size_t)prefix_len + body_len + 2); /* body + '\n' + NUL */
|
||||
if (!line)
|
||||
return NULL;
|
||||
memcpy(line, prefix, (size_t)prefix_len);
|
||||
memcpy(line + prefix_len, body, body_len);
|
||||
line[(size_t)prefix_len + body_len] = '\n';
|
||||
line[(size_t)prefix_len + body_len + 1] = '\0';
|
||||
return line;
|
||||
}
|
||||
|
||||
/* Format one complete log line (timestamp prefix + body + newline) into a
|
||||
* freshly allocated buffer. This is pure CPU/malloc work and must happen
|
||||
* OUTSIDE the log mutex: the mutex only guards the log_fp pointer, so a
|
||||
@@ -84,26 +130,11 @@ LogStderrMode log_get_stderr_mode(void) {
|
||||
* on allocation/formatting failure. */
|
||||
static char* format_log_line(LogLevel log_level, const struct tm* t, const char* format,
|
||||
va_list args) {
|
||||
char prefix[64];
|
||||
int prefix_len = snprintf(
|
||||
prefix, sizeof(prefix), "%04d-%02d-%02d %02d:%02d:%02d [%s]: ", t->tm_year + 1900,
|
||||
t->tm_mon + 1, t->tm_mday, t->tm_hour, t->tm_min, t->tm_sec, log_level_strings[log_level]);
|
||||
if (prefix_len < 0 || prefix_len >= (int)sizeof(prefix))
|
||||
char* body = format_log_body(format, args);
|
||||
if (!body)
|
||||
return NULL;
|
||||
va_list copy;
|
||||
va_copy(copy, args);
|
||||
int body_len = vsnprintf(NULL, 0, format, copy);
|
||||
va_end(copy);
|
||||
if (body_len < 0)
|
||||
return NULL;
|
||||
size_t total = (size_t)prefix_len + (size_t)body_len;
|
||||
char* line = malloc(total + 2); /* body bytes + '\n' + NUL */
|
||||
if (!line)
|
||||
return NULL;
|
||||
memcpy(line, prefix, (size_t)prefix_len);
|
||||
vsnprintf(line + prefix_len, (size_t)body_len + 1, format, args);
|
||||
line[total] = '\n';
|
||||
line[total + 1] = '\0';
|
||||
char* line = format_log_line_from_body(log_level, t, body);
|
||||
free(body);
|
||||
return line;
|
||||
}
|
||||
|
||||
@@ -121,6 +152,20 @@ static void emit_log_line(FILE* console, const char* line) {
|
||||
mtx_unlock(&log_mutex);
|
||||
}
|
||||
|
||||
void log_client_message(const char* message) {
|
||||
if (!message)
|
||||
return;
|
||||
time_t now = time(NULL);
|
||||
struct tm t;
|
||||
if (!localtime_r(&now, &t))
|
||||
return;
|
||||
char* line = format_log_line_from_body(LOG_LEVEL_INFO, &t, message);
|
||||
if (!line)
|
||||
return;
|
||||
emit_log_line(stderr, line);
|
||||
free(line);
|
||||
}
|
||||
|
||||
void log_message(LogLevel log_level, const char* format, ...) {
|
||||
if (log_level < current_log_level)
|
||||
return;
|
||||
@@ -138,8 +183,23 @@ void log_message(LogLevel log_level, const char* format, ...) {
|
||||
|
||||
va_list args;
|
||||
va_start(args, format);
|
||||
char* line = format_log_line(log_level, &t, format, args);
|
||||
char* body = format_log_body(format, args);
|
||||
va_end(args);
|
||||
if (!body)
|
||||
return;
|
||||
/* LOG_STDERR_CLIENT: hand the diagnostic to the client-message channel. A
|
||||
sink that takes ownership suppresses the local write; otherwise (no sink
|
||||
yet, or the peer connection is not up) fall through to local output so the
|
||||
diagnostic is never lost. */
|
||||
if (stderr_mode == LOG_STDERR_CLIENT) {
|
||||
LogClientMsgSink sink = client_msg_sink;
|
||||
if (sink && sink(body)) {
|
||||
free(body);
|
||||
return;
|
||||
}
|
||||
}
|
||||
char* line = format_log_line_from_body(log_level, &t, body);
|
||||
free(body);
|
||||
if (!line)
|
||||
return;
|
||||
emit_log_line(dest_io, line);
|
||||
|
||||
+20
-1
@@ -15,7 +15,11 @@
|
||||
#endif
|
||||
|
||||
typedef enum { LOG_LEVEL_DEBUG, LOG_LEVEL_INFO, LOG_LEVEL_WARNING, LOG_LEVEL_ERROR } LogLevel;
|
||||
typedef enum { LOG_STDERR_ERRORS, LOG_STDERR_ALL } LogStderrMode;
|
||||
/* --stderr=MODE destinations. ERRORS keeps errors on stderr and everything
|
||||
* else on stdout; ALL sends every message to stderr; CLIENT routes the client's
|
||||
* own diagnostics over the protocol stream to the peer's stderr (rsync's
|
||||
* --stderr=client / --no-msgs2stderr). */
|
||||
typedef enum { LOG_STDERR_ERRORS, LOG_STDERR_ALL, LOG_STDERR_CLIENT } LogStderrMode;
|
||||
|
||||
typedef enum {
|
||||
LOG_DEBUG_IO = 1u << 0,
|
||||
@@ -86,5 +90,20 @@ void log_set_8_bit_output(bool enabled);
|
||||
bool log_get_8_bit_output(void);
|
||||
void log_set_stderr_mode(LogStderrMode mode);
|
||||
LogStderrMode log_get_stderr_mode(void);
|
||||
/* Write a message a peer forwarded over the client-message channel to this
|
||||
* process's stderr (and log file), with the standard log prefix. Used by the
|
||||
* server side of rsync's --stderr=client. */
|
||||
void log_client_message(const char* message);
|
||||
|
||||
/* Sink for LOG_STDERR_CLIENT. log_message() passes the un-prefixed message
|
||||
* body to the installed sink; a `true` return means the sink took ownership
|
||||
* (e.g. queued it for protocol transmission) and the message must NOT also be
|
||||
* written locally. A `false` return (or a NULL sink) makes log_message fall
|
||||
* back to the normal local destination, so a diagnostic emitted before the peer
|
||||
* connection exists is never lost (rsync's documented fallback). The sink may
|
||||
* be called from any thread and must be tolerant of that. */
|
||||
typedef bool (*LogClientMsgSink)(const char* message);
|
||||
void log_set_client_msg_sink(LogClientMsgSink sink);
|
||||
LogClientMsgSink log_get_client_msg_sink(void);
|
||||
|
||||
#endif
|
||||
|
||||
@@ -53,6 +53,7 @@ PipelineContextSender* pipeline_context_sender_create(Config* config, Queue* que
|
||||
context->dir_entries_mutex_init = false;
|
||||
atomic_init(&context->dir_count, 0);
|
||||
context->delete_limit = false;
|
||||
context->partial = false;
|
||||
int init = 0;
|
||||
if (config->use_metadata) {
|
||||
context->dir_entries = array_list_create(file_destroy);
|
||||
|
||||
@@ -134,6 +134,11 @@ typedef struct {
|
||||
deletion (STATUS_DELETE_LIMIT): the transfer succeeded and the process must
|
||||
exit 25 like rsync. Read by the caller after the sender thread is joined. */
|
||||
bool delete_limit;
|
||||
/* Set by the sender thread when the receiver reported STATUS_PARTIAL (a
|
||||
per-entry receiver failure that did not abort the stream): the transfer
|
||||
otherwise succeeded, successfully stored --remove-source-files sources were
|
||||
removed, and the process must exit 23 like rsync. Read after join. */
|
||||
bool partial;
|
||||
} PipelineContextSender;
|
||||
|
||||
/* `config` is borrowed and must outlive the context: destroy does NOT free it,
|
||||
|
||||
+19
-2
@@ -664,6 +664,10 @@ static const char* status_to_string(Status status) {
|
||||
return "DELETE_LIMIT";
|
||||
case STATUS_DEST_INFO:
|
||||
return "DEST_INFO";
|
||||
case STATUS_CLIENT_MSG:
|
||||
return "CLIENT_MSG";
|
||||
case STATUS_PARTIAL:
|
||||
return "PARTIAL";
|
||||
default:
|
||||
return "UNKNOWN";
|
||||
}
|
||||
@@ -672,10 +676,10 @@ static const char* status_to_string(Status status) {
|
||||
/* Reject a raw wire status outside the known enum range before it is handed to
|
||||
* callers, so an unknown/corrupt frame fails as a protocol error instead of
|
||||
* being silently interpreted as an unexpected-but-valid verdict. STATUS_OK is
|
||||
* the first enumerator and STATUS_STATS the last, so the range check accepts
|
||||
* the first enumerator and STATUS_PARTIAL the last, so the range check accepts
|
||||
* every status the protocol defines. */
|
||||
static bool status_is_valid(Status status) {
|
||||
return status >= STATUS_OK && status <= STATUS_STATS;
|
||||
return status >= STATUS_OK && status <= STATUS_PARTIAL;
|
||||
}
|
||||
|
||||
/* Shared string send/receive implementation. `redact` selects whether the
|
||||
@@ -1144,6 +1148,19 @@ bool send_error_detail(int fd, const char* message) {
|
||||
return send_status(fd, STATUS_ERROR_DETAIL) && send_str(fd, message);
|
||||
}
|
||||
|
||||
bool send_client_message(int fd, const char* message) {
|
||||
if (!message)
|
||||
message = "";
|
||||
char bounded[MAX_CLIENT_MSG_BYTES + 1];
|
||||
size_t len = strlen(message);
|
||||
if (len > MAX_CLIENT_MSG_BYTES) {
|
||||
memcpy(bounded, message, MAX_CLIENT_MSG_BYTES);
|
||||
bounded[MAX_CLIENT_MSG_BYTES] = '\0';
|
||||
message = bounded;
|
||||
}
|
||||
return send_status(fd, STATUS_CLIENT_MSG) && send_str(fd, message);
|
||||
}
|
||||
|
||||
const char* protocol_last_error(void) {
|
||||
return io_error_detail;
|
||||
}
|
||||
|
||||
+30
-1
@@ -15,6 +15,12 @@
|
||||
* this for a rejection and the detail frame stays a small, fixed bound. */
|
||||
#define MAX_ERROR_DETAIL_BYTES 4096
|
||||
|
||||
/* Hard cap on a client diagnostic forwarded over the STATUS_CLIENT_MSG channel
|
||||
* (protocol 2.30.0, rsync's --stderr=client). The body is reused from the
|
||||
* bounded-string wire helper and sliced to this many bytes before it is sent,
|
||||
* so a peer can never be made to retain more than this per message. */
|
||||
#define MAX_CLIENT_MSG_BYTES 4096
|
||||
|
||||
/* Maximum uncompressed file payload accepted by the receiver's whole-file
|
||||
* paths. A single whole file is charged against the per-connection memory
|
||||
* reservation (MAX_CONNECTION_MEMORY) and against the server allocation
|
||||
@@ -240,7 +246,25 @@ enum NET_STATUS {
|
||||
* record (see format_stats_send/receive in format.h) and, when the run is a
|
||||
* --dry-run with --delete, the would-delete path list. Appended after
|
||||
* STATUS_DELETE_PLAN so no existing status is renumbered. */
|
||||
STATUS_STATS
|
||||
STATUS_STATS,
|
||||
/* Client diagnostic channel (protocol 2.30.0, rsync's --stderr=client /
|
||||
* --no-msgs2stderr). When the client's --stderr mode is `client`, the
|
||||
* client forwards its own diagnostics over this client->server frame
|
||||
* (STATUS_CLIENT_MSG followed by a bounded length-prefixed string, capped at
|
||||
* MAX_CLIENT_MSG_BYTES) instead of writing them to its local stderr. The
|
||||
* receiver reads the string and writes it to the server's stderr (respecting
|
||||
* the server log destination). Appended after STATUS_STATS so no existing
|
||||
* status is renumbered. */
|
||||
STATUS_CLIENT_MSG,
|
||||
/* Receiver-side partial transfer (protocol 2.30.0). Sent by the receiver as
|
||||
* the terminal status INSTEAD of STATUS_OK when one or more entries failed
|
||||
* per-entry without aborting the stream (currently a --devices mknod
|
||||
* EPERM/EACCES). The transfer otherwise succeeded and every successfully
|
||||
* stored file was acknowledged, so the sender may still remove
|
||||
* --remove-source-files sources; the sender maps this to rsync's exit code
|
||||
* 23 ("partial transfer due to error"), distinct from a fatal STATUS_ERROR.
|
||||
* Appended after STATUS_CLIENT_MSG so no existing status is renumbered. */
|
||||
STATUS_PARTIAL
|
||||
};
|
||||
|
||||
void io_set_fds(int read_fd, int write_fd);
|
||||
@@ -341,6 +365,11 @@ bool receive_status(int file_descriptor, Status* status);
|
||||
* length-prefixed string. Over-long messages are sliced and NULL is treated
|
||||
* as "". Returns false if the status or the string could not be sent. */
|
||||
bool send_error_detail(int file_descriptor, const char* message);
|
||||
/* Send STATUS_CLIENT_MSG followed by a bounded (<= MAX_CLIENT_MSG_BYTES)
|
||||
* length-prefixed string carrying a client diagnostic. Over-long messages are
|
||||
* sliced and NULL is treated as "". Returns false if the status or the string
|
||||
* could not be sent. */
|
||||
bool send_client_message(int file_descriptor, const char* message);
|
||||
/* Human-readable reason captured from the most recent STATUS_ERROR_DETAIL
|
||||
* received on this thread, or "" when the last status was a bare STATUS_ERROR
|
||||
* (or no detail was seen). Thread-local, and valid until the next non-keepalive
|
||||
|
||||
Reference in New Issue
Block a user