Merge pull request 'feat: add SSH transport, rsync-style CLI, and --stdio server mode' (#16) from transport-abstraction into main

Reviewed-on: #16
This commit is contained in:
2026-07-07 18:59:45 +02:00
11 changed files with 454 additions and 60 deletions
+1
View File
@@ -20,5 +20,6 @@ pkgs.mkShell {
shellHook = '' shellHook = ''
export NIX_ENFORCE_PURITY=0 export NIX_ENFORCE_PURITY=0
cmake -B build cmake -B build
export PATH="$PWD/build:$PATH"
''; '';
} }
+132 -34
View File
@@ -16,6 +16,9 @@
#include "utils.h" #include "utils.h"
#include <dirent.h> #include <dirent.h>
static char *server_host = "127.0.0.1";
static int server_port = 8080;
int send_chunk(Client *client, Chunk *chunk, Config *config) { int send_chunk(Client *client, Chunk *chunk, Config *config) {
if (config->use_chunk_serialization) { if (config->use_chunk_serialization) {
send_status(client->file_descriptor, STATUS_CHUNK); send_status(client->file_descriptor, STATUS_CHUNK);
@@ -99,12 +102,17 @@ int load_files_multithreaded(void *pipeline_context) {
int send_chunks_multithreaded(void *pipeline_context) { int send_chunks_multithreaded(void *pipeline_context) {
PipelineContextSender *context = (PipelineContextSender *)pipeline_context; PipelineContextSender *context = (PipelineContextSender *)pipeline_context;
Client *client = client_create(); Client *client;
const char *env_ip = getenv("FASTSYNC_SERVER_IP"); if (context->config->transport == TRANSPORT_SSH) {
const char *ip = env_ip ? env_ip : "127.0.0.1"; if (context->config->use_sendfile) {
const char *env_port = getenv("FASTSYNC_SERVER_PORT"); fprintf(stderr, "Error: -f/--sendfile is not supported with SSH transport\n");
int port = env_port ? atoi(env_port) : 8080; return 1;
client_connect(client, (char *)ip, port); }
client = client_connect_ssh(context->config->ssh_destination);
} else {
client = client_create();
client_connect(client, server_host, server_port);
}
config_send(client->file_descriptor, context->config); config_send(client->file_descriptor, context->config);
while (true) { while (true) {
@@ -114,9 +122,10 @@ int send_chunks_multithreaded(void *pipeline_context) {
&context->condition_not_full_loader, &context->loader_done); &context->condition_not_full_loader, &context->loader_done);
if (current_chunk == NULL) { if (current_chunk == NULL) {
send_status(client->file_descriptor, STATUS_FINISHED); send_status(client->file_descriptor, STATUS_FINISHED);
int ok = receive_status(client->file_descriptor) == STATUS_OK;
client_disconnect(client); client_disconnect(client);
client_delete(client); client_delete(client);
return thrd_success; return ok ? thrd_success : 1;
} }
if (send_chunk(client, current_chunk, context->config) != 0) { if (send_chunk(client, current_chunk, context->config) != 0) {
perror("Something unexpected happend while sending the chunk"); perror("Something unexpected happend while sending the chunk");
@@ -127,12 +136,17 @@ int send_chunks_multithreaded(void *pipeline_context) {
} }
int send_files(Config *config) { int send_files(Config *config) {
Client *client = client_create(); Client *client;
const char *env_ip = getenv("FASTSYNC_SERVER_IP"); if (config->transport == TRANSPORT_SSH) {
const char *ip = env_ip ? env_ip : "127.0.0.1"; if (config->use_sendfile) {
const char *env_port = getenv("FASTSYNC_SERVER_PORT"); fprintf(stderr, "Error: -f/--sendfile is not supported with SSH transport\n");
int port = env_port ? atoi(env_port) : 8080; return 1;
client_connect(client, (char *)ip, port); }
client = client_connect_ssh(config->ssh_destination);
} else {
client = client_create();
client_connect(client, server_host, server_port);
}
config_send(client->file_descriptor, config); config_send(client->file_descriptor, config);
DirectoryScanner *scanner = directory_scanner_create(config->send_directory, config->use_metadata); DirectoryScanner *scanner = directory_scanner_create(config->send_directory, config->use_metadata);
Chunk *current_chunk; Chunk *current_chunk;
@@ -145,12 +159,11 @@ int send_files(Config *config) {
chunk_destroy(current_chunk); chunk_destroy(current_chunk);
} }
send_status(client->file_descriptor, STATUS_FINISHED); send_status(client->file_descriptor, STATUS_FINISHED);
if (receive_status(client->file_descriptor) != STATUS_OK) int ok = receive_status(client->file_descriptor) == STATUS_OK;
return -1;
directory_scanner_destroy(scanner); directory_scanner_destroy(scanner);
client_disconnect(client); client_disconnect(client);
client_delete(client); client_delete(client);
return 0; return ok ? 0 : -1;
} }
int send_files_multithreaded(Config *config) { int send_files_multithreaded(Config *config) {
@@ -176,37 +189,65 @@ int send_files_multithreaded(Config *config) {
return 0; return 0;
} }
void handle_arg(char *argument_given, char *argument_to_set, bool *result, static int is_remote_dest(const char *s) {
char *message) { const char *colon = strchr(s, ':');
if (strcmp(argument_given, argument_to_set) == 0) { if (!colon) return 0;
*result = true; if (colon == s) return 0;
log_message(LOG_LEVEL_INFO, message); for (const char *p = s; p < colon; p++) {
if (*p == '/') return 0;
} }
return 1;
} }
static void print_usage(void) {
printf("Usage:\n");
printf(" fastsync [options] <source> <destination>\n");
printf(" fastsync [options] --source-dir <src> --dest-dir <dst>\n");
printf("\n");
printf("Destination formats:\n");
printf(" user@host:/path SSH transport (rsync-style)\n");
printf(" host:/path SSH transport (current user)\n");
printf(" /local/path TCP transport (requires server on localhost:8080)\n");
printf("\n");
printf("Options:\n");
printf(" -c [level] Enable compression (level 1-22, default 5)\n");
printf(" -m Enable multithreading\n");
printf(" -s Enable chunk serialization\n");
printf(" -f Enable sendfile (TCP only, not with -c or -s)\n");
printf(" -v, --verbose Enable debug logging\n");
printf(" -M, --preserve Preserve file metadata\n");
printf(" --source-dir <path> Source directory\n");
printf(" --dest-dir <path> Destination directory\n");
printf(" --save-to-disk Write received files to disk\n");
printf(" --server-host <ip> Server IP address (default: 127.0.0.1)\n");
printf(" --server-port <n> Server port (default: 8080)\n");
printf(" --help Show this help\n");
}
int main(int argc, char *argv[]) { int main(int argc, char *argv[]) {
const char *env_source = getenv("FASTSYNC_SOURCE_DIR"); const char *env_source = getenv("FASTSYNC_SOURCE_DIR");
const char *env_dest = getenv("FASTSYNC_DEST_DIR"); const char *env_dest = getenv("FASTSYNC_DEST_DIR");
const char *env_save = getenv("FASTSYNC_SAVE_TO_DISK"); const char *env_save = getenv("FASTSYNC_SAVE_TO_DISK");
char *source_dir =
env_source ? str_dup((char *)env_source)
: str_dup("/home/taptap/Nextcloud/Uni/moodle/B. Schnor "
"Konzepte Paralleler Programmierung, SoSe 2026");
char *dest_dir =
env_dest ? str_dup((char *)env_dest) : str_dup("./data_copied");
bool save_to_disk = false; bool save_to_disk = false;
if (env_save && if (env_save &&
(strcmp(env_save, "true") == 0 || strcmp(env_save, "1") == 0)) { (strcmp(env_save, "true") == 0 || strcmp(env_save, "1") == 0)) {
save_to_disk = true; save_to_disk = true;
} }
Config *config = config_create(str_dup("1.0.0"), source_dir, dest_dir, Config *config = config_create(str_dup("1.0.0"), NULL, NULL,
save_to_disk, false, false, false, false, 5, 20, false); save_to_disk, false, false, false, false, 5, 20, false);
int positional_args[2];
int positional_count = 0;
for (int i = 1; i < argc; i++) { for (int i = 1; i < argc; i++) {
if (strcmp(argv[i], "-c") == 0) { if (strcmp(argv[i], "--help") == 0) {
print_usage();
return 0;
} else if (strcmp(argv[i], "-c") == 0) {
config->use_compression = true; config->use_compression = true;
log_message(LOG_LEVEL_INFO, "Enabled Compression"); log_message(LOG_LEVEL_INFO, "Enabled Compression");
if (i + 1 < argc) { if (i + 1 < argc) {
char *end_ptr; char *end_ptr;
int level = strtol(argv[i + 1], &end_ptr, 10); int level = strtol(argv[i + 1], &end_ptr, 10);
@@ -214,6 +255,7 @@ int main(int argc, char *argv[]) {
config->compression_level = level; config->compression_level = level;
log_message(LOG_LEVEL_INFO, "Set Compression level to %d", log_message(LOG_LEVEL_INFO, "Set Compression level to %d",
config->compression_level); config->compression_level);
i++;
} }
} }
} else if (strcmp(argv[i], "--source-dir") == 0 && i + 1 < argc) { } else if (strcmp(argv[i], "--source-dir") == 0 && i + 1 < argc) {
@@ -230,19 +272,75 @@ int main(int argc, char *argv[]) {
} else if (strcmp(argv[i], "-f") == 0 || strcmp(argv[i], "--sendfile") == 0) { } else if (strcmp(argv[i], "-f") == 0 || strcmp(argv[i], "--sendfile") == 0) {
config->use_sendfile = true; config->use_sendfile = true;
log_message(LOG_LEVEL_INFO, "Enabled sendfile"); log_message(LOG_LEVEL_INFO, "Enabled sendfile");
} else if (strcmp(argv[i], "-m") == 0) {
config->use_multithreading = true;
log_message(LOG_LEVEL_INFO, "Enabled Multithreading");
} else if (strcmp(argv[i], "-s") == 0) {
config->use_chunk_serialization = true;
log_message(LOG_LEVEL_INFO, "Enabled Chunk Serialization");
} else if (strcmp(argv[i], "--server-host") == 0 && i + 1 < argc) {
free(server_host);
server_host = str_dup(argv[++i]);
} else if (strcmp(argv[i], "--server-port") == 0 && i + 1 < argc) {
server_port = atoi(argv[++i]);
} else if (strcmp(argv[i], "-v") == 0 || strcmp(argv[i], "--verbose") == 0) {
set_log_level(LOG_LEVEL_DEBUG);
} else if (argv[i][0] == '-') {
fprintf(stderr, "Unknown option: %s\n", argv[i]);
print_usage();
return 1;
} else { } else {
handle_arg(argv[i], "-m", &config->use_multithreading, if (positional_count < 2)
"Enabled Multithreading"); positional_args[positional_count++] = i;
handle_arg(argv[i], "-s", &config->use_chunk_serialization, else {
"Enabled Chunk Serialization"); fprintf(stderr, "Unexpected argument: %s\n", argv[i]);
print_usage();
return 1;
}
} }
} }
if (positional_count == 2) {
free(config->send_directory);
free(config->receive_root_directory);
config->send_directory = str_dup(argv[positional_args[0]]);
config->receive_root_directory = str_dup(argv[positional_args[1]]);
config->save_to_disk = true;
if (is_remote_dest(config->receive_root_directory)) {
config->transport = TRANSPORT_SSH;
config->ssh_destination = str_dup(config->receive_root_directory);
char *colon = strchr(config->receive_root_directory, ':');
char *path = str_dup(colon + 1);
free(config->receive_root_directory);
config->receive_root_directory = path;
}
} else if (positional_count == 1) {
fprintf(stderr, "Error: missing destination argument\n");
print_usage();
return 1;
} else {
if (!config->send_directory && env_source)
config->send_directory = str_dup((char *)env_source);
if (!config->receive_root_directory && env_dest)
config->receive_root_directory = str_dup((char *)env_dest);
}
if (!config->send_directory || !config->receive_root_directory) {
fprintf(stderr, "Error: source and destination directories are required\n");
print_usage();
return 1;
}
if (config->use_sendfile && (config->use_chunk_serialization || config->use_compression)) { if (config->use_sendfile && (config->use_chunk_serialization || config->use_compression)) {
fprintf(stderr, "Error: -f/--sendfile cannot be combined with -c (compression) or -s (chunk serialization)\n"); fprintf(stderr, "Error: -f/--sendfile cannot be combined with -c (compression) or -s (chunk serialization)\n");
return 1; return 1;
} }
if (config->transport == TRANSPORT_SSH && config->use_sendfile) {
fprintf(stderr, "Error: -f/--sendfile is not supported with SSH transport\n");
return 1;
}
if (config->use_multithreading) if (config->use_multithreading)
return send_files_multithreaded(config); return send_files_multithreaded(config);
return send_files(config); return send_files(config);
+12 -1
View File
@@ -10,6 +10,7 @@
#include "utils.h" #include "utils.h"
#include <stdio.h> #include <stdio.h>
#include <stdlib.h> #include <stdlib.h>
#include <string.h>
#include <threads.h> #include <threads.h>
File *file_receive(Config *config, int file_descriptor) { File *file_receive(Config *config, int file_descriptor) {
@@ -161,12 +162,22 @@ void handler(int file_descriptor) {
thrd_join(receiver, NULL); thrd_join(receiver, NULL);
thrd_join(writer, NULL); thrd_join(writer, NULL);
pipeline_context_receiver_destroy(context); pipeline_context_receiver_destroy(context);
send_status(file_descriptor, STATUS_OK);
} else } else
receive_files(config, file_descriptor); receive_files(config, file_descriptor);
close(file_descriptor); close(file_descriptor);
} }
int main() { int main(int argc, char *argv[]) {
for (int i = 1; i < argc; i++) {
if (strcmp(argv[i], "--stdio") == 0) {
io_set_fds(STDIN_FILENO, STDOUT_FILENO);
handler(STDIN_FILENO);
return 0;
} else if (strcmp(argv[i], "-v") == 0 || strcmp(argv[i], "--verbose") == 0) {
set_log_level(LOG_LEVEL_DEBUG);
}
}
Server *server = server_create(8080); Server *server = server_create(8080);
server_listen(server, handler); server_listen(server, handler);
server_delete(server); server_delete(server);
+5
View File
@@ -22,6 +22,8 @@ Config *config_create(char *version, char *send_directory,
config->compression_level = compression_level; config->compression_level = compression_level;
config->num_connections = num_connections; config->num_connections = num_connections;
config->use_sendfile = use_sendfile; config->use_sendfile = use_sendfile;
config->transport = TRANSPORT_TCP;
config->ssh_destination = NULL;
return config; return config;
} }
@@ -29,6 +31,7 @@ void config_delete(Config *config) {
free(config->version); free(config->version);
free(config->send_directory); free(config->send_directory);
free(config->receive_root_directory); free(config->receive_root_directory);
free(config->ssh_destination);
free(config); free(config);
} }
@@ -63,6 +66,8 @@ Config *config_receive(int file_descriptor) {
config->compression_level = receive_int(file_descriptor); config->compression_level = receive_int(file_descriptor);
config->num_connections = receive_int(file_descriptor); config->num_connections = receive_int(file_descriptor);
config->use_sendfile = receive_int(file_descriptor); config->use_sendfile = receive_int(file_descriptor);
config->transport = TRANSPORT_TCP;
config->ssh_destination = NULL;
send_status(file_descriptor, STATUS_OK); send_status(file_descriptor, STATUS_OK);
return config; return config;
} }
+7
View File
@@ -3,6 +3,11 @@
#include <stdbool.h> #include <stdbool.h>
typedef enum {
TRANSPORT_TCP,
TRANSPORT_SSH
} TransportType;
typedef struct Config { typedef struct Config {
char *version; char *version;
char *send_directory; char *send_directory;
@@ -16,6 +21,8 @@ typedef struct Config {
bool use_metadata; bool use_metadata;
int compression_level; int compression_level;
int num_connections; int num_connections;
TransportType transport;
char *ssh_destination;
} Config; } Config;
Config *config_create(char *version, char *send_directory, Config *config_create(char *version, char *send_directory,
+10 -7
View File
@@ -4,7 +4,11 @@
#include <time.h> #include <time.h>
static const char *log_level_strings[] = {"DEBUG", "INFO", "WARN", "ERROR"}; static const char *log_level_strings[] = {"DEBUG", "INFO", "WARN", "ERROR"};
static LogLevel current_log_level = LOG_LEVEL_DEBUG; static LogLevel current_log_level = LOG_LEVEL_WARNING;
void set_log_level(LogLevel level) {
current_log_level = level;
}
void log_message(LogLevel log_level, char *format, ...) { void log_message(LogLevel log_level, char *format, ...) {
if (log_level < current_log_level) if (log_level < current_log_level)
@@ -12,14 +16,13 @@ void log_message(LogLevel log_level, char *format, ...) {
time_t now = time(NULL); time_t now = time(NULL);
struct tm *t = localtime(&now); struct tm *t = localtime(&now);
// Print timestamp and log level to the file fprintf(stderr, "%04d-%02d-%02d %02d:%02d:%02d [%s]: ", t->tm_year + 1900,
printf("%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,
t->tm_mon + 1, t->tm_mday, t->tm_hour, t->tm_min, t->tm_sec, log_level_strings[log_level]);
log_level_strings[log_level]);
va_list args; va_list args;
va_start(args, format); va_start(args, format);
vprintf(format, args); vfprintf(stderr, format, args);
va_end(args); va_end(args);
printf("\n"); fprintf(stderr, "\n");
} }
+1
View File
@@ -9,5 +9,6 @@ typedef enum {
} LogLevel; } LogLevel;
void log_message(LogLevel log_level, char *message, ...); void log_message(LogLevel log_level, char *message, ...);
void set_log_level(LogLevel level);
#endif #endif
+138 -8
View File
@@ -1,12 +1,134 @@
#include "socket.h" #include "socket.h"
#include "log.h" #include "log.h"
#include <arpa/inet.h> #include <arpa/inet.h>
#include <fcntl.h>
#include <stddef.h> #include <stddef.h>
#include <stdio.h> #include <stdio.h>
#include <stdlib.h> #include <stdlib.h>
#include <string.h> #include <string.h>
#include <sys/socket.h>
#include <sys/wait.h>
#include <unistd.h> #include <unistd.h>
static __thread int io_read_fd = -1;
static __thread int io_write_fd = -1;
void io_set_fds(int read_fd, int write_fd) {
io_read_fd = read_fd;
io_write_fd = write_fd;
}
static int io_fd(int dir_fd, int file_descriptor) {
return (dir_fd != -1) ? dir_fd : file_descriptor;
}
typedef struct {
char user[256];
char host[256];
char remote_path[4096];
} RemoteDest;
static int parse_remote_dest(const char *dest, RemoteDest *r) {
const char *colon = strchr(dest, ':');
if (!colon) return -1;
size_t remote_path_len = strlen(colon + 1);
if (remote_path_len >= sizeof(r->remote_path)) return -1;
memcpy(r->remote_path, colon + 1, remote_path_len + 1);
const char *at = memchr(dest, '@', colon - dest);
if (at) {
size_t user_len = at - dest;
if (user_len >= sizeof(r->user)) return -1;
memcpy(r->user, dest, user_len);
r->user[user_len] = '\0';
size_t host_len = colon - at - 1;
if (host_len >= sizeof(r->host)) return -1;
memcpy(r->host, at + 1, host_len);
r->host[host_len] = '\0';
} else {
r->user[0] = '\0';
size_t host_len = colon - dest;
if (host_len >= sizeof(r->host)) return -1;
memcpy(r->host, dest, host_len);
r->host[host_len] = '\0';
}
return 0;
}
Client *client_connect_ssh(char *destination) {
RemoteDest r;
if (parse_remote_dest(destination, &r) != 0) {
fprintf(stderr, "Invalid remote destination: %s\n", destination);
exit(EXIT_FAILURE);
}
int sv[2];
if (socketpair(AF_UNIX, SOCK_STREAM, 0, sv) < 0) {
perror("socketpair failed");
exit(EXIT_FAILURE);
}
int exec_pipe[2];
if (pipe(exec_pipe) < 0) {
perror("pipe failed");
exit(EXIT_FAILURE);
}
pid_t pid = fork();
if (pid < 0) {
perror("fork failed");
exit(EXIT_FAILURE);
}
if (pid == 0) {
close(sv[0]);
close(exec_pipe[0]);
fcntl(exec_pipe[1], F_SETFD, FD_CLOEXEC);
if (sv[1] != STDIN_FILENO)
dup2(sv[1], STDIN_FILENO);
if (sv[1] != STDOUT_FILENO)
dup2(sv[1], STDOUT_FILENO);
if (sv[1] > 1) close(sv[1]);
char ssh_user[512];
if (r.user[0] != '\0')
snprintf(ssh_user, sizeof(ssh_user), "%s@%s", r.user, r.host);
else
snprintf(ssh_user, sizeof(ssh_user), "%s", r.host);
execlp("ssh", "ssh", "-o", "Compression=no", "-o",
"ControlMaster=no", ssh_user, "fastsync-server", "--stdio",
(char *)NULL);
perror("exec of ssh failed");
(void)write(exec_pipe[1], "x", 1);
_exit(1);
}
close(sv[1]);
close(exec_pipe[1]);
char exec_status;
ssize_t n = read(exec_pipe[0], &exec_status, 1);
close(exec_pipe[0]);
if (n > 0) {
close(sv[0]);
waitpid(pid, NULL, 0);
fprintf(stderr, "Error: could not launch 'fastsync-server --stdio' on remote\n");
exit(EXIT_FAILURE);
}
Client *client = malloc(sizeof(Client));
client->file_descriptor = sv[0];
client->address.sin_family = AF_UNIX;
client->address_length = 0;
client->ssh_child_pid = pid;
return client;
}
Server *server_create(int port) { Server *server_create(int port) {
Server *server = (Server *)malloc(sizeof(Server)); Server *server = (Server *)malloc(sizeof(Server));
if (server == NULL) { if (server == NULL) {
@@ -82,6 +204,7 @@ Client *client_create() {
client->file_descriptor = file_descriptor; client->file_descriptor = file_descriptor;
client->address.sin_family = AF_INET; client->address.sin_family = AF_INET;
client->address_length = sizeof(client->address); client->address_length = sizeof(client->address);
client->ssh_child_pid = -1;
return client; return client;
} }
@@ -100,7 +223,14 @@ void client_connect(Client *client, char *host, int port) {
} }
} }
void client_disconnect(Client *client) { close(client->file_descriptor); } void client_disconnect(Client *client) {
close(client->file_descriptor);
if (client->ssh_child_pid > 0) {
int status;
waitpid(client->ssh_child_pid, &status, 0);
client->ssh_child_pid = -1;
}
}
void client_delete(Client *client) { void client_delete(Client *client) {
if (client == NULL) if (client == NULL)
@@ -110,12 +240,11 @@ void client_delete(Client *client) {
void send_n_data(int file_descriptor, void *data, size_t data_size) { void send_n_data(int file_descriptor, void *data, size_t data_size) {
log_message(LOG_LEVEL_DEBUG, " Sending n Data: %zu", data_size); log_message(LOG_LEVEL_DEBUG, " Sending n Data: %zu", data_size);
int fd = io_fd(io_write_fd, file_descriptor);
ssize_t total_bytes_send = 0; ssize_t total_bytes_send = 0;
while (total_bytes_send < data_size) { while (total_bytes_send < data_size) {
printf("Trying: %zu\n", data_size - total_bytes_send); ssize_t bytes_send = write(fd, (char *)data + total_bytes_send,
ssize_t bytes_send = send(file_descriptor, (char *)data + total_bytes_send, data_size - total_bytes_send);
data_size - total_bytes_send, 0);
printf("Bytes send: %zd\n", bytes_send);
if (bytes_send <= 0) { if (bytes_send <= 0) {
perror("Could not send data!"); perror("Could not send data!");
exit(EXIT_FAILURE); exit(EXIT_FAILURE);
@@ -127,11 +256,12 @@ void send_n_data(int file_descriptor, void *data, size_t data_size) {
void receive_n_data(int file_descriptor, void *data, size_t data_size) { void receive_n_data(int file_descriptor, void *data, size_t data_size) {
log_message(LOG_LEVEL_DEBUG, " Receiving n Data: %zu", data_size); log_message(LOG_LEVEL_DEBUG, " Receiving n Data: %zu", data_size);
int fd = io_fd(io_read_fd, file_descriptor);
size_t total_bytes_received = 0; size_t total_bytes_received = 0;
while (total_bytes_received < data_size) { while (total_bytes_received < data_size) {
long long bytes_received = ssize_t bytes_received =
recv(file_descriptor, data + total_bytes_received, read(fd, (char *)data + total_bytes_received,
data_size - total_bytes_received, 0); data_size - total_bytes_received);
if (bytes_received == -1 || bytes_received == 0) { if (bytes_received == -1 || bytes_received == 0) {
perror("Could not receive bytes!"); perror("Could not receive bytes!");
exit(EXIT_FAILURE); exit(EXIT_FAILURE);
+3
View File
@@ -21,13 +21,16 @@ typedef struct Client {
struct sockaddr_in address; struct sockaddr_in address;
unsigned int address_length; unsigned int address_length;
int file_descriptor; int file_descriptor;
pid_t ssh_child_pid;
} Client; } Client;
Client *client_create(); Client *client_create();
void client_disconnect(Client *client); void client_disconnect(Client *client);
void client_delete(Client *client); void client_delete(Client *client);
void client_connect(Client *client, char *host, int port); void client_connect(Client *client, char *host, int port);
Client *client_connect_ssh(char *destination);
void io_set_fds(int read_fd, int write_fd);
void send_n_data(int file_descriptor, void *data, size_t data_size); void send_n_data(int file_descriptor, void *data, size_t data_size);
void receive_n_data(int file_descriptor, void *data, size_t data_size); void receive_n_data(int file_descriptor, void *data, size_t data_size);
void send_str(int file_descriptor, char *data); void send_str(int file_descriptor, char *data);
+130 -10
View File
@@ -51,6 +51,7 @@ BASE_CLIENT_FLAGS = ["--save-to-disk"]
TEST_CASES = [ TEST_CASES = [
{"name": "Standard", "flags": []}, {"name": "Standard", "flags": []},
{"name": "Posix Args (no flags)", "flags": [], "posix": True},
{"name": "Standard (no metadata)", "flags": [], "use_metadata": False}, {"name": "Standard (no metadata)", "flags": [], "use_metadata": False},
{"name": "Multithreading (-m)", "flags": ["-m"]}, {"name": "Multithreading (-m)", "flags": ["-m"]},
{"name": "Compression (-c)", "flags": ["-c"]}, {"name": "Compression (-c)", "flags": ["-c"]},
@@ -66,6 +67,17 @@ TEST_CASES = [
{"name": "Sendfile + Multithreading (-f -m)", "flags": ["-f", "-m"]}, {"name": "Sendfile + Multithreading (-f -m)", "flags": ["-f", "-m"]},
] ]
SSH_CASES = [
{"name": "SSH (localhost)", "flags": []},
{"name": "SSH Multithreading (-m)", "flags": ["-m"]},
{"name": "SSH Compression (-c)", "flags": ["-c"]},
{"name": "SSH Chunk Serialization (-s)", "flags": ["-s"]},
{"name": "SSH Compression + Chunk Serialization (-c -s)", "flags": ["-c", "-s"]},
{"name": "SSH Multithreading + Compression (-m -c)", "flags": ["-m", "-c"]},
{"name": "SSH Multithreading + Chunk Serialization (-m -s)", "flags": ["-m", "-s"]},
{"name": "SSH Multithreading + Compression + Chunk Serialization (-m -c -s)", "flags": ["-m", "-c", "-s"]},
]
RSYNC_CASES = [ RSYNC_CASES = [
{"name": "rsync (archive)", "args": ["-aH"]}, {"name": "rsync (archive)", "args": ["-aH"]},
{"name": "rsync (archive + compress)", "args": ["-aHz"]}, {"name": "rsync (archive + compress)", "args": ["-aHz"]},
@@ -183,17 +195,21 @@ def start_rsync_daemon(source_dir):
return port, conf, daemon return port, conf, daemon
def run_single_test(cmd, name, source_dir, dest_dir, *, source_prefix=None): def run_single_test(cmd, name, source_dir, dest_dir, *, source_prefix=None, no_server=False):
if os.path.exists(dest_dir): if os.path.exists(dest_dir):
shutil.rmtree(dest_dir) shutil.rmtree(dest_dir)
server = subprocess.Popen(SERVER_CMD, stdout=subprocess.DEVNULL, stderr=None) if no_server:
time.sleep(0.5) server = None
else:
server = subprocess.Popen(SERVER_CMD, stdout=subprocess.DEVNULL, stderr=None)
time.sleep(0.5)
try: try:
start = time.monotonic() start = time.monotonic()
result = subprocess.run(cmd, text=True, capture_output=True) result = subprocess.run(cmd, text=True, capture_output=True)
duration = time.monotonic() - start duration = time.monotonic() - start
finally: finally:
wait_proc(server) if server:
wait_proc(server)
mismatches, missing = [], [] mismatches, missing = [], []
if result.returncode == 0: if result.returncode == 0:
@@ -246,7 +262,10 @@ def run_profile(profile_name, source_dir, dest_dir):
results = [] results = []
for case in TEST_CASES: for case in TEST_CASES:
flags = BASE_CLIENT_FLAGS + (["-M"] if case.get("use_metadata", True) else []) + case["flags"] flags = BASE_CLIENT_FLAGS + (["-M"] if case.get("use_metadata", True) else []) + case["flags"]
cmd = client_prefix + BASE_CLIENT_CMD + ["--source-dir", source_dir, "--dest-dir", dest_dir] + flags if case.get("posix"):
cmd = client_prefix + BASE_CLIENT_CMD + [source_dir, dest_dir] + flags
else:
cmd = client_prefix + BASE_CLIENT_CMD + ["--source-dir", source_dir, "--dest-dir", dest_dir] + flags
print(f"\n --- {case['name']} ---\n Running: {' '.join(cmd)}") print(f"\n --- {case['name']} ---\n Running: {' '.join(cmd)}")
try: try:
r = run_single_test(cmd, case["name"], source_dir, dest_dir) r = run_single_test(cmd, case["name"], source_dir, dest_dir)
@@ -255,6 +274,19 @@ def run_profile(profile_name, source_dir, dest_dir):
except Exception as e: except Exception as e:
results.append({"name": case["name"], "suite": profile_name, "status": "Error", "time": "N/A", "error": str(e)}) results.append({"name": case["name"], "suite": profile_name, "status": "Error", "time": "N/A", "error": str(e)})
if SSH_AVAILABLE:
for case in SSH_CASES:
flags = BASE_CLIENT_FLAGS + (["-M"] if case.get("use_metadata", True) else []) + case["flags"]
ssh_dest = f"localhost:{dest_dir}_ssh"
cmd = BASE_CLIENT_CMD + [source_dir, ssh_dest] + flags
print(f"\n --- {case['name']} ---\n Running: {' '.join(cmd)}")
try:
r = run_single_test(cmd, case["name"], source_dir, f"{dest_dir}_ssh", no_server=True)
r["suite"] = profile_name
results.append(r)
except Exception as e:
results.append({"name": case["name"], "suite": profile_name, "status": "Error", "time": "N/A", "error": str(e)})
port, conf, daemon = start_rsync_daemon(source_dir) port, conf, daemon = start_rsync_daemon(source_dir)
try: try:
for case in RSYNC_CASES: for case in RSYNC_CASES:
@@ -344,7 +376,100 @@ def format_throughput(bps):
return f"{bps:.0f} B/s" return f"{bps:.0f} B/s"
SSH_AVAILABLE = False
def check_ssh_localhost():
global SSH_AVAILABLE
build_dir = os.path.abspath("build")
server_path = os.path.join(build_dir, "server")
r = subprocess.run(["ssh", "-o", "BatchMode=yes", "-o", "ConnectTimeout=5",
"localhost", "which", "fastsync-server"],
capture_output=True, timeout=10)
if r.returncode == 0:
SSH_AVAILABLE = True
return
SSH_AVAILABLE = False
# Try each PATH dir: create symlink, then verify with which
r = subprocess.run(
["ssh", "-o", "BatchMode=yes", "localhost",
'echo "$PATH"'],
capture_output=True, timeout=10, text=True)
if r.returncode != 0:
return
for d in r.stdout.strip().split(":"):
d = d.strip()
if not d:
continue
if "wrappers" in d:
continue
test = subprocess.run(
["ssh", "-o", "BatchMode=yes", "localhost",
f'test -w "{d}" && ln -sf {server_path} "{d}/fastsync-server" && which fastsync-server'],
capture_output=True, timeout=10)
if test.returncode == 0:
SSH_AVAILABLE = True
return
def preflight_checks():
global SSH_AVAILABLE
errors = []
print("Pre-flight checks:")
print(" [1] --help flag...", end=" ")
r = subprocess.run(BASE_CLIENT_CMD + ["--help"], capture_output=True, text=True)
if r.returncode == 0 and "Usage:" in r.stdout and "SSH transport" in r.stdout:
print("OK")
else:
print("FAIL")
errors.append("--help failed")
print(" [2] Remote SSH dest detection...", end=" ")
r = subprocess.run(BASE_CLIENT_CMD + ["/x", "somehost:/y"], capture_output=True, text=True, timeout=5)
if r.returncode != 0 and ("ssh" in r.stderr or "Could not receive" in r.stderr or "could not launch" in r.stderr or "Error" in r.stderr):
print("OK (detected as SSH)")
else:
print("FAIL (not detected as SSH dest)")
errors.append("SSH detection failed")
print(" [3] Server --stdio flag...", end=" ")
try:
r = subprocess.run(["./build/server", "--stdio"], capture_output=True, text=True, timeout=3)
if r.returncode != 0 and ("receiving" in r.stderr or "receiving" in r.stdout or "Receiving" in r.stderr):
print("OK (started in stdio mode)")
else:
print("WARN (stdio exited: rc=%d)" % r.returncode)
except subprocess.TimeoutExpired:
print("OK (waiting for stdin)")
print(" [4] Posix arg syntax (no server, expect failure)...", end=" ")
r = subprocess.run(BASE_CLIENT_CMD + ["/tmp/x", "/tmp/y"], capture_output=True, text=True, timeout=5)
if r.returncode != 0 and "connect" in r.stderr:
print("OK (TCP fallback)")
else:
print("FAIL")
errors.append("Posix arg syntax failed")
check_ssh_localhost()
print(" [5] SSH to localhost...", end=" ")
if SSH_AVAILABLE:
print("OK")
else:
print("SKIP (install fastsync-server in PATH on remote)")
if errors:
print(f"\n {len(errors)} pre-flight check(s) failed: {', '.join(errors)}")
sys.exit(1)
print(" All pre-flight checks passed.\n")
def main(): def main():
os.system("cmake -B build -S . > /dev/null 2>&1")
if os.system("cd build && make -j$(nproc) 2>&1 | tail -3") != 0:
print("Build failed")
sys.exit(1)
preflight_checks()
parser = argparse.ArgumentParser(description="FastSync integration test / benchmark") parser = argparse.ArgumentParser(description="FastSync integration test / benchmark")
parser.add_argument("--source-dir", default=DEFAULT_SOURCE_DIR) parser.add_argument("--source-dir", default=DEFAULT_SOURCE_DIR)
parser.add_argument("--dest-dir", default=DEFAULT_DEST_DIR) parser.add_argument("--dest-dir", default=DEFAULT_DEST_DIR)
@@ -353,11 +478,6 @@ def main():
parser.add_argument("--wan", action="store_true") parser.add_argument("--wan", action="store_true")
args = parser.parse_args() args = parser.parse_args()
os.system("cmake -B build -S . > /dev/null 2>&1")
if os.system("cd build && make -j$(nproc) 2>&1 | tail -3") != 0:
print("Build failed")
sys.exit(1)
total_bytes = generate_test_files(args.source_dir) total_bytes = generate_test_files(args.source_dir)
if os.path.exists(args.dest_dir): if os.path.exists(args.dest_dir):
shutil.rmtree(args.dest_dir) shutil.rmtree(args.dest_dir)
+15
View File
@@ -18,6 +18,20 @@ static void test_config_lifecycle() {
EXPECT_FALSE(cfg->use_chunk_serialization); EXPECT_FALSE(cfg->use_chunk_serialization);
EXPECT_FALSE(cfg->use_compression); EXPECT_FALSE(cfg->use_compression);
EXPECT_EQ_INT(cfg->num_connections, 4); EXPECT_EQ_INT(cfg->num_connections, 4);
EXPECT_EQ_INT(cfg->transport, TRANSPORT_TCP);
EXPECT_NULL(cfg->ssh_destination);
config_delete(cfg);
}
static void test_config_ssh_dest() {
Config *cfg = config_create(str_dup("1.0"), str_dup("/src"), str_dup("user@host:/dst"),
true, false, false, false, false, 1, 4, false);
cfg->transport = TRANSPORT_SSH;
cfg->ssh_destination = str_dup("user@host:/dst");
EXPECT_NOT_NULL(cfg);
EXPECT_EQ_INT(cfg->transport, TRANSPORT_SSH);
EXPECT_EQ_STR(cfg->ssh_destination, "user@host:/dst");
EXPECT_EQ_STR(cfg->receive_root_directory, "user@host:/dst");
config_delete(cfg); config_delete(cfg);
} }
@@ -55,6 +69,7 @@ static void test_pipeline_receiver_lifecycle() {
void test_config() { void test_config() {
test_config_lifecycle(); test_config_lifecycle();
test_config_ssh_dest();
test_pipeline_sender_lifecycle(); test_pipeline_sender_lifecycle();
test_pipeline_receiver_lifecycle(); test_pipeline_receiver_lifecycle();
} }