From fbd7ccf4dd9e2cea539f01e91b73d13fa42204c5 Mon Sep 17 00:00:00 2001 From: TapTap Date: Mon, 6 Jul 2026 18:42:33 +0200 Subject: [PATCH 01/11] feat: add SSH transport, rsync-style CLI, and --stdio server mode - Replace send()/recv() with read()/write() + io_set_fds() for fd abstraction - Add client_connect_ssh() via socketpair + fork/exec; reap child in client_disconnect() - Add server --stdio flag for SSH invocation - Replace handle_arg() with rsync-style positional args (fastsync ) - Add SSH vs TCP auto-detection via is_remote_dest() - Add TransportType and ssh_destination to Config, fix uninitialized fields in config_receive() (pre-existing multithreaded crash) - Sender threads now wait for server STATUS_OK before disconnecting - Add --help, error handling for sendfile+SSH combos - Add pre-flight checks and Posix Args test case to test.py - Add test_config_ssh_dest() unit test --- src/client/client.c | 155 +++++++++++++++++++++++++++++++++++--------- src/server/server.c | 9 ++- src/shared/config.c | 5 ++ src/shared/config.h | 7 ++ src/shared/socket.c | 146 ++++++++++++++++++++++++++++++++++++++--- src/shared/socket.h | 3 + test.py | 47 +++++++++++++- tests/test_config.c | 15 +++++ 8 files changed, 347 insertions(+), 40 deletions(-) diff --git a/src/client/client.c b/src/client/client.c index f2e3875..8d9a8d1 100644 --- a/src/client/client.c +++ b/src/client/client.c @@ -99,12 +99,21 @@ int load_files_multithreaded(void *pipeline_context) { int send_chunks_multithreaded(void *pipeline_context) { PipelineContextSender *context = (PipelineContextSender *)pipeline_context; - Client *client = client_create(); - const char *env_ip = getenv("FASTSYNC_SERVER_IP"); - const char *ip = env_ip ? env_ip : "127.0.0.1"; - const char *env_port = getenv("FASTSYNC_SERVER_PORT"); - int port = env_port ? atoi(env_port) : 8080; - client_connect(client, (char *)ip, port); + Client *client; + if (context->config->transport == TRANSPORT_SSH) { + if (context->config->use_sendfile) { + fprintf(stderr, "Error: -f/--sendfile is not supported with SSH transport\n"); + return 1; + } + client = client_connect_ssh(context->config->ssh_destination); + } else { + client = client_create(); + const char *env_ip = getenv("FASTSYNC_SERVER_IP"); + const char *ip = env_ip ? env_ip : "127.0.0.1"; + const char *env_port = getenv("FASTSYNC_SERVER_PORT"); + int port = env_port ? atoi(env_port) : 8080; + client_connect(client, (char *)ip, port); + } config_send(client->file_descriptor, context->config); while (true) { @@ -114,6 +123,11 @@ int send_chunks_multithreaded(void *pipeline_context) { &context->condition_not_full_loader, &context->loader_done); if (current_chunk == NULL) { send_status(client->file_descriptor, STATUS_FINISHED); + if (receive_status(client->file_descriptor) != STATUS_OK) { + client_disconnect(client); + client_delete(client); + return 1; + } client_disconnect(client); client_delete(client); return thrd_success; @@ -127,12 +141,21 @@ int send_chunks_multithreaded(void *pipeline_context) { } int send_files(Config *config) { - Client *client = client_create(); - const char *env_ip = getenv("FASTSYNC_SERVER_IP"); - const char *ip = env_ip ? env_ip : "127.0.0.1"; - const char *env_port = getenv("FASTSYNC_SERVER_PORT"); - int port = env_port ? atoi(env_port) : 8080; - client_connect(client, (char *)ip, port); + Client *client; + if (config->transport == TRANSPORT_SSH) { + if (config->use_sendfile) { + fprintf(stderr, "Error: -f/--sendfile is not supported with SSH transport\n"); + return 1; + } + client = client_connect_ssh(config->ssh_destination); + } else { + client = client_create(); + const char *env_ip = getenv("FASTSYNC_SERVER_IP"); + const char *ip = env_ip ? env_ip : "127.0.0.1"; + const char *env_port = getenv("FASTSYNC_SERVER_PORT"); + int port = env_port ? atoi(env_port) : 8080; + client_connect(client, (char *)ip, port); + } config_send(client->file_descriptor, config); DirectoryScanner *scanner = directory_scanner_create(config->send_directory, config->use_metadata); Chunk *current_chunk; @@ -176,37 +199,62 @@ int send_files_multithreaded(Config *config) { return 0; } -void handle_arg(char *argument_given, char *argument_to_set, bool *result, - char *message) { - if (strcmp(argument_given, argument_to_set) == 0) { - *result = true; - log_message(LOG_LEVEL_INFO, message); +static int is_remote_dest(const char *s) { + const char *colon = strchr(s, ':'); + if (!colon) return 0; + if (colon == s) return 0; + 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] \n"); + printf(" fastsync [options] --source-dir --dest-dir \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(" -M, --preserve Preserve file metadata\n"); + printf(" --source-dir Source directory\n"); + printf(" --dest-dir Destination directory\n"); + printf(" --save-to-disk Write received files to disk\n"); + printf(" --help Show this help\n"); +} + int main(int argc, char *argv[]) { const char *env_source = getenv("FASTSYNC_SOURCE_DIR"); const char *env_dest = getenv("FASTSYNC_DEST_DIR"); 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; if (env_save && (strcmp(env_save, "true") == 0 || strcmp(env_save, "1") == 0)) { 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); + + int positional_args[2]; + int positional_count = 0; + 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; log_message(LOG_LEVEL_INFO, "Enabled Compression"); - if (i + 1 < argc) { char *end_ptr; int level = strtol(argv[i + 1], &end_ptr, 10); @@ -214,6 +262,7 @@ int main(int argc, char *argv[]) { config->compression_level = level; log_message(LOG_LEVEL_INFO, "Set Compression level to %d", config->compression_level); + i++; } } } else if (strcmp(argv[i], "--source-dir") == 0 && i + 1 < argc) { @@ -230,11 +279,52 @@ int main(int argc, char *argv[]) { } else if (strcmp(argv[i], "-f") == 0 || strcmp(argv[i], "--sendfile") == 0) { config->use_sendfile = true; 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 (argv[i][0] == '-') { + fprintf(stderr, "Unknown option: %s\n", argv[i]); + print_usage(); + return 1; } else { - handle_arg(argv[i], "-m", &config->use_multithreading, - "Enabled Multithreading"); - handle_arg(argv[i], "-s", &config->use_chunk_serialization, - "Enabled Chunk Serialization"); + if (positional_count < 2) + positional_args[positional_count++] = i; + else { + fprintf(stderr, "Unexpected argument: %s\n", argv[i]); + print_usage(); + return 1; + } + } + } + + if (positional_count == 2) { + free(config->send_directory); + config->send_directory = str_dup(argv[positional_args[0]]); + free(config->receive_root_directory); + 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); + } + } else if (positional_count == 1) { + fprintf(stderr, "Error: missing destination argument\n"); + print_usage(); + return 1; + } else { + if (!config->send_directory) { + config->send_directory = + env_source ? str_dup((char *)env_source) + : str_dup("/home/taptap/Nextcloud/Uni/moodle/B. Schnor: " + "Konzepte Paralleler Programmierung, SoSe 2026"); + } + if (!config->receive_root_directory) { + config->receive_root_directory = + env_dest ? str_dup((char *)env_dest) : str_dup("./data_copied"); } } @@ -243,6 +333,11 @@ int main(int argc, char *argv[]) { 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) return send_files_multithreaded(config); return send_files(config); diff --git a/src/server/server.c b/src/server/server.c index 0a5fb3b..83dba1d 100644 --- a/src/server/server.c +++ b/src/server/server.c @@ -10,6 +10,7 @@ #include "utils.h" #include #include +#include #include File *file_receive(Config *config, int file_descriptor) { @@ -161,12 +162,18 @@ void handler(int file_descriptor) { thrd_join(receiver, NULL); thrd_join(writer, NULL); pipeline_context_receiver_destroy(context); + send_status(file_descriptor, STATUS_OK); } else receive_files(config, file_descriptor); close(file_descriptor); } -int main() { +int main(int argc, char *argv[]) { + if (argc > 1 && strcmp(argv[1], "--stdio") == 0) { + io_set_fds(STDIN_FILENO, STDOUT_FILENO); + handler(STDIN_FILENO); + return 0; + } Server *server = server_create(8080); server_listen(server, handler); server_delete(server); diff --git a/src/shared/config.c b/src/shared/config.c index 149b448..4db3409 100644 --- a/src/shared/config.c +++ b/src/shared/config.c @@ -22,6 +22,8 @@ Config *config_create(char *version, char *send_directory, config->compression_level = compression_level; config->num_connections = num_connections; config->use_sendfile = use_sendfile; + config->transport = TRANSPORT_TCP; + config->ssh_destination = NULL; return config; } @@ -29,6 +31,7 @@ void config_delete(Config *config) { free(config->version); free(config->send_directory); free(config->receive_root_directory); + free(config->ssh_destination); free(config); } @@ -63,6 +66,8 @@ Config *config_receive(int file_descriptor) { config->compression_level = receive_int(file_descriptor); config->num_connections = 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); return config; } diff --git a/src/shared/config.h b/src/shared/config.h index 2a645d7..72e0524 100644 --- a/src/shared/config.h +++ b/src/shared/config.h @@ -3,6 +3,11 @@ #include +typedef enum { + TRANSPORT_TCP, + TRANSPORT_SSH +} TransportType; + typedef struct Config { char *version; char *send_directory; @@ -16,6 +21,8 @@ typedef struct Config { bool use_metadata; int compression_level; int num_connections; + TransportType transport; + char *ssh_destination; } Config; Config *config_create(char *version, char *send_directory, diff --git a/src/shared/socket.c b/src/shared/socket.c index 0d6f31e..6e3b8a7 100644 --- a/src/shared/socket.c +++ b/src/shared/socket.c @@ -1,12 +1,134 @@ #include "socket.h" #include "log.h" #include +#include #include #include #include #include +#include +#include #include +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 = (Server *)malloc(sizeof(Server)); if (server == NULL) { @@ -82,6 +204,7 @@ Client *client_create() { client->file_descriptor = file_descriptor; client->address.sin_family = AF_INET; client->address_length = sizeof(client->address); + client->ssh_child_pid = -1; 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) { 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) { 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; while (total_bytes_send < data_size) { - printf("Trying: %zu\n", data_size - total_bytes_send); - ssize_t bytes_send = send(file_descriptor, (char *)data + total_bytes_send, - data_size - total_bytes_send, 0); - printf("Bytes send: %zd\n", bytes_send); + ssize_t bytes_send = write(fd, (char *)data + total_bytes_send, + data_size - total_bytes_send); if (bytes_send <= 0) { perror("Could not send data!"); 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) { 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; while (total_bytes_received < data_size) { - long long bytes_received = - recv(file_descriptor, data + total_bytes_received, - data_size - total_bytes_received, 0); + ssize_t bytes_received = + read(fd, (char *)data + total_bytes_received, + data_size - total_bytes_received); if (bytes_received == -1 || bytes_received == 0) { perror("Could not receive bytes!"); exit(EXIT_FAILURE); diff --git a/src/shared/socket.h b/src/shared/socket.h index db694f1..7973edf 100644 --- a/src/shared/socket.h +++ b/src/shared/socket.h @@ -21,13 +21,16 @@ typedef struct Client { struct sockaddr_in address; unsigned int address_length; int file_descriptor; + pid_t ssh_child_pid; } Client; Client *client_create(); void client_disconnect(Client *client); void client_delete(Client *client); 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 receive_n_data(int file_descriptor, void *data, size_t data_size); void send_str(int file_descriptor, char *data); diff --git a/test.py b/test.py index cce5cae..6c48e8a 100755 --- a/test.py +++ b/test.py @@ -51,6 +51,7 @@ BASE_CLIENT_FLAGS = ["--save-to-disk"] TEST_CASES = [ {"name": "Standard", "flags": []}, + {"name": "Posix Args (no flags)", "flags": [], "posix": True}, {"name": "Standard (no metadata)", "flags": [], "use_metadata": False}, {"name": "Multithreading (-m)", "flags": ["-m"]}, {"name": "Compression (-c)", "flags": ["-c"]}, @@ -246,7 +247,10 @@ def run_profile(profile_name, source_dir, dest_dir): results = [] for case in TEST_CASES: 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)}") try: r = run_single_test(cmd, case["name"], source_dir, dest_dir) @@ -344,7 +348,48 @@ def format_throughput(bps): return f"{bps:.0f} B/s" +def preflight_checks(): + 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=" ") + 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) + + 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") + + 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(): + preflight_checks() parser = argparse.ArgumentParser(description="FastSync integration test / benchmark") parser.add_argument("--source-dir", default=DEFAULT_SOURCE_DIR) parser.add_argument("--dest-dir", default=DEFAULT_DEST_DIR) diff --git a/tests/test_config.c b/tests/test_config.c index 664ec70..973a330 100644 --- a/tests/test_config.c +++ b/tests/test_config.c @@ -18,6 +18,20 @@ static void test_config_lifecycle() { EXPECT_FALSE(cfg->use_chunk_serialization); EXPECT_FALSE(cfg->use_compression); 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); } @@ -55,6 +69,7 @@ static void test_pipeline_receiver_lifecycle() { void test_config() { test_config_lifecycle(); + test_config_ssh_dest(); test_pipeline_sender_lifecycle(); test_pipeline_receiver_lifecycle(); } From c0dfa52f6774cbb4fe48e2230dad17d96d521a02 Mon Sep 17 00:00:00 2001 From: TapTap Date: Mon, 6 Jul 2026 18:49:02 +0200 Subject: [PATCH 02/11] fix: handle TimeoutExpired in server --stdio pre-flight check --- test.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/test.py b/test.py index 6c48e8a..c5b63f5 100755 --- a/test.py +++ b/test.py @@ -368,11 +368,14 @@ def preflight_checks(): errors.append("SSH detection failed") print(" [3] Server --stdio flag...", end=" ") - 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) + 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) From 5b4395ada1d4159261ceef55490168c518f4ffa6 Mon Sep 17 00:00:00 2001 From: TapTap Date: Mon, 6 Jul 2026 18:52:56 +0200 Subject: [PATCH 03/11] fix: build binaries before pre-flight checks in test.py --- test.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/test.py b/test.py index c5b63f5..ac5281d 100755 --- a/test.py +++ b/test.py @@ -392,6 +392,10 @@ def preflight_checks(): 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.add_argument("--source-dir", default=DEFAULT_SOURCE_DIR) @@ -401,11 +405,6 @@ def main(): parser.add_argument("--wan", action="store_true") 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) if os.path.exists(args.dest_dir): shutil.rmtree(args.dest_dir) From 1012c5c1510654d3119303698ed5fdf5b6f33c11 Mon Sep 17 00:00:00 2001 From: TapTap Date: Mon, 6 Jul 2026 19:54:22 +0200 Subject: [PATCH 04/11] refactor: address PR review feedback - Replace FASTSYNC_SERVER_IP/FASTSYNC_SERVER_PORT env vars with --server-host and --server-port CLI flags - Simplify STATUS_OK handshake: single disconnect/delete path - Remove hardcoded default directory (require explicit source/dest) - Clean up free(NULL) on positional args path - Fix send_files() resource leak on STATUS_OK failure --- src/client/client.c | 56 +++++++++++++++++++++------------------------ 1 file changed, 26 insertions(+), 30 deletions(-) diff --git a/src/client/client.c b/src/client/client.c index 8d9a8d1..fdb4bdb 100644 --- a/src/client/client.c +++ b/src/client/client.c @@ -16,6 +16,9 @@ #include "utils.h" #include +static char *server_host = "127.0.0.1"; +static int server_port = 8080; + int send_chunk(Client *client, Chunk *chunk, Config *config) { if (config->use_chunk_serialization) { send_status(client->file_descriptor, STATUS_CHUNK); @@ -108,11 +111,7 @@ int send_chunks_multithreaded(void *pipeline_context) { client = client_connect_ssh(context->config->ssh_destination); } else { client = client_create(); - const char *env_ip = getenv("FASTSYNC_SERVER_IP"); - const char *ip = env_ip ? env_ip : "127.0.0.1"; - const char *env_port = getenv("FASTSYNC_SERVER_PORT"); - int port = env_port ? atoi(env_port) : 8080; - client_connect(client, (char *)ip, port); + client_connect(client, server_host, server_port); } config_send(client->file_descriptor, context->config); @@ -123,14 +122,10 @@ int send_chunks_multithreaded(void *pipeline_context) { &context->condition_not_full_loader, &context->loader_done); if (current_chunk == NULL) { send_status(client->file_descriptor, STATUS_FINISHED); - if (receive_status(client->file_descriptor) != STATUS_OK) { - client_disconnect(client); - client_delete(client); - return 1; - } + int ok = receive_status(client->file_descriptor) == STATUS_OK; client_disconnect(client); client_delete(client); - return thrd_success; + return ok ? thrd_success : 1; } if (send_chunk(client, current_chunk, context->config) != 0) { perror("Something unexpected happend while sending the chunk"); @@ -150,11 +145,7 @@ int send_files(Config *config) { client = client_connect_ssh(config->ssh_destination); } else { client = client_create(); - const char *env_ip = getenv("FASTSYNC_SERVER_IP"); - const char *ip = env_ip ? env_ip : "127.0.0.1"; - const char *env_port = getenv("FASTSYNC_SERVER_PORT"); - int port = env_port ? atoi(env_port) : 8080; - client_connect(client, (char *)ip, port); + client_connect(client, server_host, server_port); } config_send(client->file_descriptor, config); DirectoryScanner *scanner = directory_scanner_create(config->send_directory, config->use_metadata); @@ -168,12 +159,11 @@ int send_files(Config *config) { chunk_destroy(current_chunk); } send_status(client->file_descriptor, STATUS_FINISHED); - if (receive_status(client->file_descriptor) != STATUS_OK) - return -1; + int ok = receive_status(client->file_descriptor) == STATUS_OK; directory_scanner_destroy(scanner); client_disconnect(client); client_delete(client); - return 0; + return ok ? 0 : -1; } int send_files_multithreaded(Config *config) { @@ -228,6 +218,8 @@ static void print_usage(void) { printf(" --source-dir Source directory\n"); printf(" --dest-dir Destination directory\n"); printf(" --save-to-disk Write received files to disk\n"); + printf(" --server-host Server IP address (default: 127.0.0.1)\n"); + printf(" --server-port Server port (default: 8080)\n"); printf(" --help Show this help\n"); } @@ -285,6 +277,11 @@ int main(int argc, char *argv[]) { } 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 (argv[i][0] == '-') { fprintf(stderr, "Unknown option: %s\n", argv[i]); print_usage(); @@ -302,8 +299,8 @@ int main(int argc, char *argv[]) { if (positional_count == 2) { free(config->send_directory); - config->send_directory = str_dup(argv[positional_args[0]]); 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; @@ -316,18 +313,17 @@ int main(int argc, char *argv[]) { print_usage(); return 1; } else { - if (!config->send_directory) { - config->send_directory = - env_source ? str_dup((char *)env_source) - : str_dup("/home/taptap/Nextcloud/Uni/moodle/B. Schnor: " - "Konzepte Paralleler Programmierung, SoSe 2026"); - } - if (!config->receive_root_directory) { - config->receive_root_directory = - env_dest ? str_dup((char *)env_dest) : str_dup("./data_copied"); - } + 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)) { fprintf(stderr, "Error: -f/--sendfile cannot be combined with -c (compression) or -s (chunk serialization)\n"); return 1; From 3b92501dc7a7f569bd7181cd83ee744f2b23b71e Mon Sep 17 00:00:00 2001 From: TapTap Date: Mon, 6 Jul 2026 20:03:57 +0200 Subject: [PATCH 05/11] feat: add SSH test cases to test.py - Add check_ssh_localhost() that tests SSH connectivity and symlinks fastsync-server into PATH if needed - Add SSH_CASES with single-threaded, multithreaded, and compression tests - Support no_server mode in run_single_test for SSH tests - Skip SSH tests gracefully when SSH/localhost/fastsync-server unavailable --- test.py | 59 +++++++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 55 insertions(+), 4 deletions(-) diff --git a/test.py b/test.py index ac5281d..2575ab1 100755 --- a/test.py +++ b/test.py @@ -67,6 +67,12 @@ TEST_CASES = [ {"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"]}, +] + RSYNC_CASES = [ {"name": "rsync (archive)", "args": ["-aH"]}, {"name": "rsync (archive + compress)", "args": ["-aHz"]}, @@ -184,17 +190,21 @@ def start_rsync_daemon(source_dir): 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): shutil.rmtree(dest_dir) - server = subprocess.Popen(SERVER_CMD, stdout=subprocess.DEVNULL, stderr=None) - time.sleep(0.5) + if no_server: + server = None + else: + server = subprocess.Popen(SERVER_CMD, stdout=subprocess.DEVNULL, stderr=None) + time.sleep(0.5) try: start = time.monotonic() result = subprocess.run(cmd, text=True, capture_output=True) duration = time.monotonic() - start finally: - wait_proc(server) + if server: + wait_proc(server) mismatches, missing = [], [] if result.returncode == 0: @@ -259,6 +269,19 @@ def run_profile(profile_name, source_dir, dest_dir): except Exception as 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 = client_prefix + 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) try: for case in RSYNC_CASES: @@ -348,7 +371,28 @@ def format_throughput(bps): 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: + install = subprocess.run(["ssh", "-o", "BatchMode=yes", "localhost", + f"mkdir -p ~/.local/bin && ln -sf {server_path} ~/.local/bin/fastsync-server"], + capture_output=True, timeout=10) + if install.returncode == 0: + r = subprocess.run(["ssh", "-o", "BatchMode=yes", "localhost", + "which", "fastsync-server"], capture_output=True, timeout=10) + SSH_AVAILABLE = r.returncode == 0 + + def preflight_checks(): + global SSH_AVAILABLE errors = [] print("Pre-flight checks:") print(" [1] --help flag...", end=" ") @@ -385,6 +429,13 @@ def preflight_checks(): 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) From 372c559ef2cb1291e2fe26d1a3d4ff8c5845aaf6 Mon Sep 17 00:00:00 2001 From: TapTap Date: Mon, 6 Jul 2026 20:15:36 +0200 Subject: [PATCH 06/11] fix: SSH transport - log to stderr, use-after-free, systemd scope conflict --- src/client/client.c | 4 ++++ src/shared/log.c | 11 +++++------ test.py | 4 ++-- 3 files changed, 11 insertions(+), 8 deletions(-) diff --git a/src/client/client.c b/src/client/client.c index fdb4bdb..1ce3e61 100644 --- a/src/client/client.c +++ b/src/client/client.c @@ -307,6 +307,10 @@ int main(int argc, char *argv[]) { 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"); diff --git a/src/shared/log.c b/src/shared/log.c index ffdc748..872f046 100644 --- a/src/shared/log.c +++ b/src/shared/log.c @@ -12,14 +12,13 @@ void log_message(LogLevel log_level, char *format, ...) { time_t now = time(NULL); struct tm *t = localtime(&now); - // Print timestamp and log level to the file - 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, - log_level_strings[log_level]); + fprintf(stderr, "%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]); va_list args; va_start(args, format); - vprintf(format, args); + vfprintf(stderr, format, args); va_end(args); - printf("\n"); + fprintf(stderr, "\n"); } diff --git a/test.py b/test.py index 2575ab1..f41e74e 100755 --- a/test.py +++ b/test.py @@ -273,7 +273,7 @@ def run_profile(profile_name, source_dir, dest_dir): 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 = client_prefix + BASE_CLIENT_CMD + [source_dir, ssh_dest] + flags + 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) @@ -281,7 +281,7 @@ def run_profile(profile_name, source_dir, dest_dir): 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) try: for case in RSYNC_CASES: From a96d48f0ed5cd48874b56d2969f790178127892d Mon Sep 17 00:00:00 2001 From: TapTap Date: Mon, 6 Jul 2026 20:26:20 +0200 Subject: [PATCH 07/11] feat: default log level to WARNING, add -v/--verbose flag --- src/client/client.c | 3 +++ src/server/server.c | 12 ++++++++---- src/shared/log.c | 6 +++++- src/shared/log.h | 1 + 4 files changed, 17 insertions(+), 5 deletions(-) diff --git a/src/client/client.c b/src/client/client.c index 1ce3e61..beb2253 100644 --- a/src/client/client.c +++ b/src/client/client.c @@ -214,6 +214,7 @@ static void print_usage(void) { 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 Source directory\n"); printf(" --dest-dir Destination directory\n"); @@ -282,6 +283,8 @@ int main(int argc, char *argv[]) { 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(); diff --git a/src/server/server.c b/src/server/server.c index 83dba1d..a0f731f 100644 --- a/src/server/server.c +++ b/src/server/server.c @@ -169,10 +169,14 @@ void handler(int file_descriptor) { } int main(int argc, char *argv[]) { - if (argc > 1 && strcmp(argv[1], "--stdio") == 0) { - io_set_fds(STDIN_FILENO, STDOUT_FILENO); - handler(STDIN_FILENO); - return 0; + 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_listen(server, handler); diff --git a/src/shared/log.c b/src/shared/log.c index 872f046..953a117 100644 --- a/src/shared/log.c +++ b/src/shared/log.c @@ -4,7 +4,11 @@ #include 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, ...) { if (log_level < current_log_level) diff --git a/src/shared/log.h b/src/shared/log.h index f1274b1..1510b6b 100644 --- a/src/shared/log.h +++ b/src/shared/log.h @@ -9,5 +9,6 @@ typedef enum { } LogLevel; void log_message(LogLevel log_level, char *message, ...); +void set_log_level(LogLevel level); #endif From 1a1248446912a6695b67a1155b81febde09cdd39 Mon Sep 17 00:00:00 2001 From: TapTap Date: Mon, 6 Jul 2026 20:30:38 +0200 Subject: [PATCH 08/11] fix: add build dir to PATH in nix-shell; robust SSH PATH detection in check_ssh_localhost --- shell.nix | 1 + test.py | 19 +++++++++++++------ 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/shell.nix b/shell.nix index ad1a267..1259d7b 100644 --- a/shell.nix +++ b/shell.nix @@ -20,5 +20,6 @@ pkgs.mkShell { shellHook = '' export NIX_ENFORCE_PURITY=0 cmake -B build + export PATH="$PWD/build:$PATH" ''; } diff --git a/test.py b/test.py index f41e74e..ce4ca2d 100755 --- a/test.py +++ b/test.py @@ -382,12 +382,19 @@ def check_ssh_localhost(): "localhost", "which", "fastsync-server"], capture_output=True, timeout=10) if r.returncode != 0: - install = subprocess.run(["ssh", "-o", "BatchMode=yes", "localhost", - f"mkdir -p ~/.local/bin && ln -sf {server_path} ~/.local/bin/fastsync-server"], - capture_output=True, timeout=10) - if install.returncode == 0: - r = subprocess.run(["ssh", "-o", "BatchMode=yes", "localhost", - "which", "fastsync-server"], capture_output=True, timeout=10) + path_r = subprocess.run( + ["ssh", "-o", "BatchMode=yes", "localhost", + "echo $PATH | tr ':' '\\n' | while read d; do [ -w \"$d\" ] && echo \"$d\" && break; done"], + capture_output=True, timeout=10, text=True) + remote_bin_dir = path_r.stdout.strip() + if remote_bin_dir: + install = subprocess.run( + ["ssh", "-o", "BatchMode=yes", "localhost", + f"ln -sf {server_path} {remote_bin_dir}/fastsync-server"], + capture_output=True, timeout=10) + if install.returncode == 0: + r = subprocess.run(["ssh", "-o", "BatchMode=yes", "localhost", + "which", "fastsync-server"], capture_output=True, timeout=10) SSH_AVAILABLE = r.returncode == 0 From c2bdf68429c4d1efe6a12388b9eb58ddbb5db1c4 Mon Sep 17 00:00:00 2001 From: TapTap Date: Mon, 6 Jul 2026 20:33:51 +0200 Subject: [PATCH 09/11] fix: check SSH symlink by file existence instead of PATH lookup --- test.py | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/test.py b/test.py index ce4ca2d..589185d 100755 --- a/test.py +++ b/test.py @@ -382,20 +382,26 @@ def check_ssh_localhost(): "localhost", "which", "fastsync-server"], capture_output=True, timeout=10) if r.returncode != 0: - path_r = subprocess.run( - ["ssh", "-o", "BatchMode=yes", "localhost", - "echo $PATH | tr ':' '\\n' | while read d; do [ -w \"$d\" ] && echo \"$d\" && break; done"], - capture_output=True, timeout=10, text=True) - remote_bin_dir = path_r.stdout.strip() - if remote_bin_dir: + SSH_AVAILABLE = False + # Try common directories that are typically in PATH + for d in ["~/.local/bin", "~/bin", "~/.local/state/nix/profile/bin"]: install = subprocess.run( ["ssh", "-o", "BatchMode=yes", "localhost", - f"ln -sf {server_path} {remote_bin_dir}/fastsync-server"], + f"mkdir -p {d} && ln -sf {server_path} {d}/fastsync-server && test -f {d}/fastsync-server"], capture_output=True, timeout=10) if install.returncode == 0: - r = subprocess.run(["ssh", "-o", "BatchMode=yes", "localhost", - "which", "fastsync-server"], capture_output=True, timeout=10) - SSH_AVAILABLE = r.returncode == 0 + SSH_AVAILABLE = True + break + if not SSH_AVAILABLE: + # Last resort: find a writable dir in PATH + r2 = subprocess.run( + ["ssh", "-o", "BatchMode=yes", "localhost", + 'd=$(echo $PATH | tr ":" "\\n" | while read p; do [ -w "$p" ] && echo "$p" && break; done) && ' + f'ln -sf {server_path} "$d"/fastsync-server && test -f "$d"/fastsync-server'], + capture_output=True, timeout=10) + SSH_AVAILABLE = r2.returncode == 0 + else: + SSH_AVAILABLE = True def preflight_checks(): From e099dc48434f4cb4362ce05c7f0564f7bdd7b090 Mon Sep 17 00:00:00 2001 From: TapTap Date: Mon, 6 Jul 2026 20:39:59 +0200 Subject: [PATCH 10/11] fix: skip wrappers dirs in SSH PATH scan; verify symlink with which --- test.py | 44 ++++++++++++++++++++++++-------------------- 1 file changed, 24 insertions(+), 20 deletions(-) diff --git a/test.py b/test.py index 589185d..7243928 100755 --- a/test.py +++ b/test.py @@ -381,27 +381,31 @@ def check_ssh_localhost(): 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 = False - # Try common directories that are typically in PATH - for d in ["~/.local/bin", "~/bin", "~/.local/state/nix/profile/bin"]: - install = subprocess.run( - ["ssh", "-o", "BatchMode=yes", "localhost", - f"mkdir -p {d} && ln -sf {server_path} {d}/fastsync-server && test -f {d}/fastsync-server"], - capture_output=True, timeout=10) - if install.returncode == 0: - SSH_AVAILABLE = True - break - if not SSH_AVAILABLE: - # Last resort: find a writable dir in PATH - r2 = subprocess.run( - ["ssh", "-o", "BatchMode=yes", "localhost", - 'd=$(echo $PATH | tr ":" "\\n" | while read p; do [ -w "$p" ] && echo "$p" && break; done) && ' - f'ln -sf {server_path} "$d"/fastsync-server && test -f "$d"/fastsync-server'], - capture_output=True, timeout=10) - SSH_AVAILABLE = r2.returncode == 0 - else: + 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(): From afca5e4f690472555bacd184448f204d22915266 Mon Sep 17 00:00:00 2001 From: TapTap Date: Tue, 7 Jul 2026 18:55:47 +0200 Subject: [PATCH 11/11] test: expand SSH tests to all valid flag combinations --- test.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/test.py b/test.py index 7243928..26a55b4 100755 --- a/test.py +++ b/test.py @@ -71,6 +71,11 @@ 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 = [