diff --git a/logging.m b/logging.m index 193431a..1592b84 100644 --- a/logging.m +++ b/logging.m @@ -206,4 +206,9 @@ void IPALoggingInit(const char *subsystem) { } g_logSandbox = [logsDir stringByAppendingPathComponent:filename]; +#ifndef FINAL_RELEASE + // Let the log server replay the tail of this file to clients that connect + // after launch — they see history instead of only the live stream. + IPALogServerSetReplayPath(g_logSandbox); +#endif } diff --git a/logserver.h b/logserver.h index 0fbc33c..d5766e2 100644 --- a/logserver.h +++ b/logserver.h @@ -34,6 +34,13 @@ // subsequent calls after the first are silent no-ops. void IPALogServerStart(uint16_t port); +// Register the sandbox log file path for replay-on-connect. +// Must be called after IPALogServerStart(). When a new client connects, +// the server tails the last IPA_LOG_REPLAY_BYTES (default 100 KB) of this +// file and sends them before switching to the live stream. Safe to call +// with nil to disable replay. +void IPALogServerSetReplayPath(NSString *path); + // Broadcast a log line to all connected clients. Called from IPALog(). // line must be non-nil; the implementation appends a trailing newline if // absent. Returns immediately when no clients are connected. diff --git a/logserver.m b/logserver.m index 30ed769..917be25 100644 --- a/logserver.m +++ b/logserver.m @@ -40,6 +40,12 @@ #define LOG_SERVER_RECV_CHUNK 256 #define LOG_SERVER_QUEUE_DEPTH_MAX 256 +// How many bytes from the end of the log file to replay to a new client. +// Aligned up to the next newline boundary so no partial lines are sent. +#ifndef IPA_LOG_REPLAY_BYTES +#define IPA_LOG_REPLAY_BYTES (100 * 1024) +#endif + // --------------------------------------------------------------------------- // Module state. // --------------------------------------------------------------------------- @@ -53,6 +59,10 @@ static _Atomic uint32_t g_dropped = 0; static _Atomic uint32_t g_pending = 0; +// Log file path for replay-on-connect. Set via IPALogServerSetReplayPath(). +// Accessed on g_queue only. +static NSString *g_replayPath = nil; + // --------------------------------------------------------------------------- // Socket helpers. // --------------------------------------------------------------------------- @@ -85,6 +95,49 @@ static BOOL ls_send_all(int fd, const uint8_t *buf, size_t len) { return YES; } +// --------------------------------------------------------------------------- +// Replay history — send the tail of the log file to a freshly connected +// client, then return so the caller can add it to the live-stream set. +// Runs on g_queue so g_replayPath access is safe. +// --------------------------------------------------------------------------- +static void ls_replay_to(int fd) { + if (!g_replayPath) return; + + int fileFd = open(g_replayPath.UTF8String, O_RDONLY | O_NONBLOCK); + if (fileFd < 0) return; + + off_t fileSize = lseek(fileFd, 0, SEEK_END); + if (fileSize <= 0) { close(fileFd); return; } + + // Start IPA_LOG_REPLAY_BYTES before EOF (or at BOF if file is smaller). + off_t startOff = (fileSize > (off_t)IPA_LOG_REPLAY_BYTES) + ? fileSize - (off_t)IPA_LOG_REPLAY_BYTES + : 0; + + if (lseek(fileFd, startOff, SEEK_SET) < 0) { close(fileFd); return; } + + // Skip forward to the next newline so we don't send a partial first line. + if (startOff > 0) { + char ch; + while (read(fileFd, &ch, 1) == 1 && ch != '\n') {} + } + + // Stream the rest to the client in chunks. + uint8_t buf[4096]; + ssize_t n; + while ((n = read(fileFd, buf, sizeof(buf))) > 0) { + const uint8_t *p = buf; + size_t rem = (size_t)n; + while (rem > 0) { + ssize_t sent = send(fd, p, rem, 0); + if (sent <= 0) { close(fileFd); return; } + p += sent; + rem -= (size_t)sent; + } + } + close(fileFd); +} + // --------------------------------------------------------------------------- // Client management — must be called on g_queue. // --------------------------------------------------------------------------- @@ -96,6 +149,9 @@ static void ls_add_client(int fd) { close(fd); return; } + // Replay history before admitting to the live stream so the client sees + // events that happened before it connected. + ls_replay_to(fd); g_clients[g_clientCount++] = fd; IPALog([NSString stringWithFormat: @"[LOGSVR] client connected fd=%d (%d/%d)", @@ -148,6 +204,11 @@ static void ls_handle_accept(void) { // --------------------------------------------------------------------------- // Public API. // --------------------------------------------------------------------------- +void IPALogServerSetReplayPath(NSString *path) { + if (!g_queue) return; + dispatch_async(g_queue, ^{ g_replayPath = [path copy]; }); +} + void IPALogServerStart(uint16_t port) { if (g_listenFd >= 0) return; // already running