From 6d899d8655d1a89fc81fc79cf2599717ca3d4d1b Mon Sep 17 00:00:00 2001 From: tkgstrator Date: Mon, 22 Jun 2026 01:57:21 +0000 Subject: [PATCH] feat(logging): rotate the sandbox file log into a Logs/ subdirectory MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replaces the previous "single append-only .log" with a size-bounded rotation scheme so a long-running consumer can't fill the sandbox with a multi-GB log. - Files now live under /Logs/ (created on init), so an operator can grab the whole rotated set (.log + .{1..N-1}.log) by copying one directory instead of fishing individual files out. PascalCase matches iOS's own sandbox conventions (Documents/, Library/, etc.). Base stays the existing tmp/ or, with IPA_LOG_TO_DOCUMENTS=1, the Files.app-visible Documents/. - IPALogPath samples the current size every IPA_LOG_ROTATE_CHECK_EVERY writes (default 64) and, once it crosses IPA_LOG_MAX_BYTES (default 4 MiB), shifts .log -> .1.log -> ... -> .{N-1}.log, dropping anything past N-1. N defaults to 3 generations (so up to 12 MiB on disk per consumer at the default ceiling). - All three knobs (max bytes, generation count, sample interval) are -D overridable at build time so a noisy consumer can dial up retention without forking this file. Best-effort throughout: failed mkdir falls back to writing into the parent directory, and rotation runs without an explicit lock — the sampling cadence already keeps two concurrent writers from both rotating in the same window, and a stray double-rotate just produces an empty .1.log instead of corrupting data. Co-Authored-By: Claude Opus 4.7 --- logging.m | 149 +++++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 126 insertions(+), 23 deletions(-) diff --git a/logging.m b/logging.m index 5f08bb9..193431a 100644 --- a/logging.m +++ b/logging.m @@ -1,5 +1,6 @@ #import "logging.h" #import +#import #ifndef FINAL_RELEASE #import "logserver.h" #endif @@ -19,28 +20,112 @@ // The TCP stream is implemented in logserver.m so this file stays the // shared logging facade rather than becoming a socket server itself. // -// File destination defaults to NSTemporaryDirectory()/.log, which -// resolves to /var/mobile/Containers/Data/Application//tmp/.log. +// File destination layout — every flavor writes into a `Logs/` subdirectory +// of its base path (created on init), so operators can grab the whole +// directory at once instead of fishing individual files out of tmp/ or +// Documents/. The directory name follows iOS's own sandbox PascalCase +// convention (sibling to `Documents/`, `Library/`). The default base is +// NSTemporaryDirectory() (which resolves to +// /var/mobile/Containers/Data/Application//tmp/), so the full path is +// .../tmp/Logs/.log // -// When IPA_LOG_TO_DOCUMENTS=1 is defined at build time, the file destination -// is moved to /Documents/.log instead. That directory is -// exposed through Files.app once the host app's Info.plist carries -// UIFileSharingEnabled + LSSupportsOpeningDocumentsInPlace — typical for -// the statically-patched / sideload-injected distribution flavor where the -// operator has no SSH access. The same log can then be read over the -// Files app on a non-jailbroken device. +// When IPA_LOG_TO_DOCUMENTS=1 is defined at build time, the base moves to +// /Documents/ instead. That directory is exposed through Files.app +// once the host app's Info.plist carries UIFileSharingEnabled + +// LSSupportsOpeningDocumentsInPlace — typical for the statically-patched / +// sideload-injected distribution flavor where the operator has no SSH +// access. The same log can then be read over the Files app on a +// non-jailbroken device. +// +// Rotation — to keep a runaway process from filling the sandbox with one +// multi-GB log file, the file destination rotates once it crosses +// IPA_LOG_MAX_BYTES (default 4 MiB): +// +// .log -> .1.log +// .1.log -> .2.log +// ... +// .{N-1}.log -> dropped +// +// where N = IPA_LOG_GENERATIONS (default 3). The size check is sampled +// every IPA_LOG_ROTATE_CHECK_EVERY writes (default 64) — exact bytes-on- +// disk control is intentionally relaxed so per-line stat() cost stays +// bounded. A stray double-rotate just leaves an empty .1.log instead of +// corrupting data. // // The sandbox file write is best-effort and silently swallows exceptions // so a flaky filesystem can't take down the host process. // =========================================================================== -static os_log_t g_log = NULL; -static NSString *g_logSandbox = nil; -static NSString *g_tag = @"tweak"; +#ifndef IPA_LOG_MAX_BYTES +#define IPA_LOG_MAX_BYTES (4 * 1024 * 1024) // 4 MiB per file +#endif +#ifndef IPA_LOG_GENERATIONS +#define IPA_LOG_GENERATIONS 3 // .log + .1.log + .2.log +#endif +#ifndef IPA_LOG_ROTATE_CHECK_EVERY +#define IPA_LOG_ROTATE_CHECK_EVERY 64 // stat() every N writes +#endif + +static os_log_t g_log = NULL; +static NSString *g_logSandbox = nil; +static NSString *g_tag = @"tweak"; +static atomic_uint g_writeCount __attribute__((unused)) = 0; + +// Slide .{N-2}.log → .{N-1}.log, ..., .log → .1.log, +// dropping anything past .{N-1}.log. Caller is the rare sampled +// rotation tick, so no explicit lock is needed. +static void IPALogRotate(NSString *path) { + NSFileManager *fm = [NSFileManager defaultManager]; + NSString *dir = [path stringByDeletingLastPathComponent]; + NSString *file = [path lastPathComponent]; + NSString *stem = [file stringByDeletingPathExtension]; // "" + NSString *ext = [file pathExtension]; // "log" + int gens = IPA_LOG_GENERATIONS; + if (gens < 2) return; // nothing to rotate into + + @try { + // Drop the oldest generation (gens-1). + NSString *drop = [dir stringByAppendingPathComponent: + [NSString stringWithFormat:@"%@.%d.%@", stem, gens - 1, ext]]; + [fm removeItemAtPath:drop error:nil]; + + // Shift gens-2 .. 1 each down one slot. + for (int i = gens - 2; i >= 1; i--) { + NSString *src = [dir stringByAppendingPathComponent: + [NSString stringWithFormat:@"%@.%d.%@", stem, i, ext]]; + NSString *dst = [dir stringByAppendingPathComponent: + [NSString stringWithFormat:@"%@.%d.%@", stem, i + 1, ext]]; + if ([fm fileExistsAtPath:src]) + [fm moveItemAtPath:src toPath:dst error:nil]; + } + + // Current .log → .1.log. + NSString *first = [dir stringByAppendingPathComponent: + [NSString stringWithFormat:@"%@.1.%@", stem, ext]]; + [fm moveItemAtPath:path toPath:first error:nil]; + } @catch (NSException *e) {} +} + +// Check size every IPA_LOG_ROTATE_CHECK_EVERY writes and rotate if we're +// past IPA_LOG_MAX_BYTES. Sampling avoids a stat() per IPALog while still +// catching runaway logs within ~64 lines of the limit. +static void IPAMaybeRotate(NSString *path) { + unsigned n = atomic_fetch_add_explicit(&g_writeCount, 1, memory_order_relaxed); + if ((n % IPA_LOG_ROTATE_CHECK_EVERY) != 0) return; + + NSDictionary *attrs = + [[NSFileManager defaultManager] attributesOfItemAtPath:path error:nil]; + if (!attrs) return; + unsigned long long size = [attrs fileSize]; + if (size < (unsigned long long)IPA_LOG_MAX_BYTES) return; + + IPALogRotate(path); +} static void IPALogPath(NSString *path, NSString *msg) { if (!path) return; @try { + IPAMaybeRotate(path); NSDateFormatter *df = [[NSDateFormatter alloc] init]; df.dateFormat = @"HH:mm:ss.SSS"; NSString *line = [NSString stringWithFormat:@"%@ %@\n", @@ -87,20 +172,38 @@ void IPALoggingInit(const char *subsystem) { #ifndef FINAL_RELEASE IPALogServerStart(IPA_LOG_SERVER_DEFAULT_PORT); #endif + + // Pick the base directory. Documents/ on the IPA-distribution flavor so + // Files.app can read it; tmp/ everywhere else. #if defined(IPA_LOG_TO_DOCUMENTS) && IPA_LOG_TO_DOCUMENTS - // Files.app-exposed log path. Used by the statically-patched build so - // operators on non-jailbroken devices can read the log without SSH. NSArray *docs = NSSearchPathForDirectoriesInDomains( NSDocumentDirectory, NSUserDomainMask, YES); - if (docs.count > 0) { - g_logSandbox = [docs[0] stringByAppendingPathComponent:filename]; - } else { - // Fallback to tmp/ if NSDocumentDirectory resolves to nothing. - g_logSandbox = [NSTemporaryDirectory() - stringByAppendingPathComponent:filename]; - } + NSString *base = (docs.count > 0) + ? docs[0] + : NSTemporaryDirectory(); #else - g_logSandbox = [NSTemporaryDirectory() - stringByAppendingPathComponent:filename]; + NSString *base = NSTemporaryDirectory(); #endif + + // Group every flavor's logs under `/Logs/` so the rotated set + // (.log, .1.log, .2.log…) lives in one folder operators + // can grab in one shot. PascalCase matches iOS's own sandbox + // conventions (`Documents/`, `Library/`, etc.). Best-effort mkdir — + // a stale symlink or perms issue downgrades us to writing into the + // base directly. + NSString *logsDir = [base stringByAppendingPathComponent:@"Logs"]; + NSFileManager *fm = [NSFileManager defaultManager]; + NSError *mkdirErr = nil; + BOOL ok = [fm createDirectoryAtPath:logsDir + withIntermediateDirectories:YES + attributes:nil + error:&mkdirErr]; + if (!ok) { + BOOL isDir = NO; + if (![fm fileExistsAtPath:logsDir isDirectory:&isDir] || !isDir) { + logsDir = base; // fall back to the parent + } + } + + g_logSandbox = [logsDir stringByAppendingPathComponent:filename]; }