-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathlogging.m
More file actions
278 lines (253 loc) · 11.4 KB
/
Copy pathlogging.m
File metadata and controls
278 lines (253 loc) · 11.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
#import "logging.h"
#import <os/log.h>
#import <stdatomic.h>
#ifndef FINAL_RELEASE
#import "logserver.h"
#endif
// ===========================================================================
// logging.m — implementation backing logging.h.
//
// Three destinations on every IPALog(), plus a fourth in debug builds:
// * NSLog — Console.app, always on
// * os_log — unified logging, subsystem-scoped
// * g_logSandbox file — append-only file inside the host app's sandbox
// * LAN TCP — 0.0.0.0:18082 when FINAL_RELEASE is undefined
// The LAN stream exists so operators can tail logs from a PC on the same
// network. Debug builds only; FINAL_RELEASE strips the sink back out.
// 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 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/<UUID>/tmp/), so the full path is
// .../tmp/Logs/<tag>.log
//
// When IPA_LOG_TO_DOCUMENTS=1 is defined at build time, the base moves to
// <sandbox>/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):
//
// <tag>.log -> <tag>.1.log
// <tag>.1.log -> <tag>.2.log
// ...
// <tag>.{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.
// ===========================================================================
#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 <tag>.{N-2}.log → <tag>.{N-1}.log, ..., <tag>.log → <tag>.1.log,
// dropping anything past <tag>.{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]; // "<tag>"
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 <tag>.log → <tag>.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.
//
// `counter` lets each destination own its own sampling cadence so a busy
// side channel does not consume the main log's rotation budget.
static void IPAMaybeRotateWithCounter(NSString *path, atomic_uint *counter) {
unsigned n = atomic_fetch_add_explicit(counter, 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 IPAMaybeRotate(NSString *path) {
IPAMaybeRotateWithCounter(path, &g_writeCount);
}
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",
[df stringFromDate:[NSDate date]], msg];
NSFileHandle *fh = [NSFileHandle fileHandleForWritingAtPath:path];
if (!fh) {
[line writeToFile:path atomically:YES encoding:NSUTF8StringEncoding error:nil];
} else {
[fh seekToEndOfFile];
[fh writeData:[line dataUsingEncoding:NSUTF8StringEncoding]];
[fh closeFile];
}
} @catch (NSException *e) {}
}
void IPALog(NSString *msg) {
if (!msg) msg = @"(null)";
NSLog(@"[%@] %@", g_tag, msg);
if (g_log) {
os_log(g_log, "%{public}s", msg.UTF8String);
}
if (g_logSandbox) IPALogPath(g_logSandbox, msg);
#ifndef FINAL_RELEASE
IPALogServerPush(msg);
#endif
}
void IPALoggingInit(const char *subsystem) {
if (!subsystem) return;
g_log = os_log_create(subsystem, "tweak");
// Derive a short tag from the last dot-separated segment of the
// subsystem. The tag is reused for the sandbox log filename so multiple
// tweaks loaded into the same process don't clobber each other's files.
NSString *sub = [NSString stringWithUTF8String:subsystem];
NSArray *parts = [sub componentsSeparatedByString:@"."];
if (parts.count > 0) {
NSString *last = [parts lastObject];
if (last.length > 0) g_tag = last;
}
NSString *filename = [g_tag stringByAppendingString:@".log"];
#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
NSArray<NSString *> *docs = NSSearchPathForDirectoriesInDomains(
NSDocumentDirectory, NSUserDomainMask, YES);
NSString *base = (docs.count > 0)
? docs[0]
: NSTemporaryDirectory();
#else
NSString *base = NSTemporaryDirectory();
#endif
// Group every flavor's logs under `<base>/Logs/` so the rotated set
// (<tag>.log, <tag>.1.log, <tag>.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];
#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
}
// ---------------------------------------------------------------------------
// Side-log destinations. Each `IPASideLog(tag, ...)` call routes to
// `<logs-dir>/<tag>.log`, where <logs-dir> is derived from g_logSandbox
// (same directory the main IPALog uses). Rotation state is kept per tag
// so a busy side channel doesn't starve the main log's sampling cadence.
// ---------------------------------------------------------------------------
static NSMutableDictionary<NSString *, NSValue *> *g_sideCounters = nil;
static NSLock *g_sideCountersLock = nil;
static atomic_uint *IPASideCounterForTag(NSString *tag) {
// Lazy-init the registry — logging.m has no global constructor and we
// do not want to force IPALoggingInit to allocate a counter for every
// possible side channel up front.
static dispatch_once_t once;
dispatch_once(&once, ^{
g_sideCounters = [NSMutableDictionary dictionary];
g_sideCountersLock = [[NSLock alloc] init];
});
[g_sideCountersLock lock];
NSValue *v = g_sideCounters[tag];
atomic_uint *counter = (atomic_uint *)[v pointerValue];
if (!counter) {
counter = (atomic_uint *)calloc(1, sizeof(atomic_uint));
atomic_init(counter, 0);
g_sideCounters[tag] = [NSValue valueWithPointer:counter];
}
[g_sideCountersLock unlock];
return counter;
}
void IPASideLog(NSString *tag, NSString *line) {
if (!tag || tag.length == 0 || !line) return;
if (!g_logSandbox) return; // Init hasn't run — nowhere to write.
NSString *dir = [g_logSandbox stringByDeletingLastPathComponent];
NSString *path =
[dir stringByAppendingPathComponent:
[tag stringByAppendingString:@".log"]];
atomic_uint *counter = IPASideCounterForTag(tag);
@try {
IPAMaybeRotateWithCounter(path, counter);
NSString *out = [line hasSuffix:@"\n"]
? line
: [line stringByAppendingString:@"\n"];
NSFileHandle *fh = [NSFileHandle fileHandleForWritingAtPath:path];
if (!fh) {
[out writeToFile:path
atomically:YES
encoding:NSUTF8StringEncoding
error:nil];
} else {
[fh seekToEndOfFile];
[fh writeData:[out dataUsingEncoding:NSUTF8StringEncoding]];
[fh closeFile];
}
} @catch (NSException *e) {}
}