Repository navigation
Expand file tree
/
Copy pathlogserver.m
More file actions
318 lines (278 loc) · 11.9 KB
/
Copy pathlogserver.m
File metadata and controls
318 lines (278 loc) · 11.9 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
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
#import "logserver.h"
#ifndef FINAL_RELEASE
#import "logging.h"
#import <arpa/inet.h>
#import <errno.h>
#import <fcntl.h>
#import <netinet/in.h>
#import <netinet/tcp.h>
#import <stdatomic.h>
#import <sys/socket.h>
#import <unistd.h>
// ===========================================================================
// logserver.m — debug-only TCP log streaming server.
//
// Architecture mirrors Server_CSA.m but fans out to up to
// IPA_LOG_SERVER_MAX_CLIENTS simultaneous clients instead of one. The
// tradeoff vs. CSA's single-client design is intentional: for log reading
// you often want two terminals open at once (one raw, one grepped).
//
// Transport:
// - Binds to 0.0.0.0 so operators can tail from a PC on the same LAN.
// - Debug-build only; FINAL_RELEASE compiles this file down to nothing.
// - Line-oriented UTF-8. Each IPALogServerPush() sends one LF-terminated
// line to every client whose fd is live.
// - A new client beyond MAX_CLIENTS is closed immediately; the caller
// is never blocked.
// - Dead clients are detected on send failure and evicted inline.
// - A soft queue-depth cap (LOG_SERVER_QUEUE_DEPTH_MAX) prevents a
// slow reader from building up unbounded backlog; lines are dropped
// with a counter rather than blocking the IPALog() call chain.
//
// All operations that touch g_clients[] run on g_queue (serial), so no
// locking primitives beyond the atomic client-count are needed.
// ===========================================================================
#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.
// ---------------------------------------------------------------------------
static dispatch_queue_t g_queue = NULL;
static dispatch_source_t g_listenSrc = NULL;
static int g_listenFd = -1;
// Client fd array and atomic drop counter, both accessed on g_queue.
static int g_clients[IPA_LOG_SERVER_MAX_CLIENTS];
static int g_clientCount = 0;
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.
// ---------------------------------------------------------------------------
static void ls_set_nonblock(int fd) {
int flags = fcntl(fd, F_GETFL, 0);
if (flags >= 0) fcntl(fd, F_SETFL, flags | O_NONBLOCK);
}
static void ls_set_keepalive(int fd) {
int on = 1;
(void)setsockopt(fd, SOL_SOCKET, SO_KEEPALIVE, &on, sizeof(on));
int idle = 5, intvl = 3, count = 3;
(void)setsockopt(fd, IPPROTO_TCP, TCP_KEEPALIVE, &idle, sizeof(idle));
(void)setsockopt(fd, IPPROTO_TCP, TCP_KEEPINTVL, &intvl, sizeof(intvl));
(void)setsockopt(fd, IPPROTO_TCP, TCP_KEEPCNT, &count, sizeof(count));
}
// Send all bytes to fd. Returns NO and closes fd on any error.
static BOOL ls_send_all(int fd, const uint8_t *buf, size_t len) {
size_t off = 0;
while (off < len) {
ssize_t n = send(fd, buf + off, len - off, 0);
if (n < 0) {
if (errno == EINTR) continue;
return NO;
}
if (n == 0) return NO;
off += (size_t)n;
}
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.
// ---------------------------------------------------------------------------
static void ls_add_client(int fd) {
if (g_clientCount >= IPA_LOG_SERVER_MAX_CLIENTS) {
IPALog([NSString stringWithFormat:
@"[LOGSVR] skipped: reason=maxClientsReached maxClients=%d fd=%d",
IPA_LOG_SERVER_MAX_CLIENTS, 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] connected: fd=%d clients=%d/%d",
fd, g_clientCount, IPA_LOG_SERVER_MAX_CLIENTS]);
}
// Evict the client at index i, compacting the array. Called on g_queue.
static void ls_evict_at(int i) {
int fd = g_clients[i];
close(fd);
IPALog([NSString stringWithFormat:@"[LOGSVR] evicted: fd=%d", fd]);
// Compact: move the last entry into the vacated slot.
g_clients[i] = g_clients[--g_clientCount];
}
// ---------------------------------------------------------------------------
// Drain inbound bytes from a client. We don't speak any protocol so we
// discard everything, but we need to detect EOF/RST to evict the client.
// Returns NO if the client has gone away.
// ---------------------------------------------------------------------------
static BOOL ls_drain_client(int fd) {
uint8_t buf[LOG_SERVER_RECV_CHUNK];
ssize_t n = recv(fd, buf, sizeof(buf), MSG_DONTWAIT);
if (n > 0) return YES; // data discarded
if (n < 0 && (errno == EAGAIN || errno == EWOULDBLOCK)) return YES;
return NO; // EOF or error — evict
}
// ---------------------------------------------------------------------------
// accept() handler — fires on g_queue via dispatch source.
// ---------------------------------------------------------------------------
static void ls_handle_accept(void) {
struct sockaddr_in peer;
socklen_t peerLen = sizeof(peer);
int fd = accept(g_listenFd, (struct sockaddr *)&peer, &peerLen);
if (fd < 0) {
if (errno != EAGAIN && errno != EWOULDBLOCK) {
IPALog([NSString stringWithFormat:
@"[LOGSVR] exception: op=accept errno=%d", errno]);
}
return;
}
// Flip to non-blocking so push doesn't stall on a slow reader.
ls_set_nonblock(fd);
ls_set_keepalive(fd);
ls_add_client(fd);
}
// ---------------------------------------------------------------------------
// 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
g_queue = dispatch_queue_create("io.kiou.logserver", DISPATCH_QUEUE_SERIAL);
int s = socket(AF_INET, SOCK_STREAM, 0);
if (s < 0) {
IPALog([NSString stringWithFormat:@"[LOGSVR] exception: op=socket errno=%d", errno]);
return;
}
int one = 1;
setsockopt(s, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one));
struct sockaddr_in addr = {0};
addr.sin_family = AF_INET;
addr.sin_port = htons(port);
#if IPA_CHINLAN
// Chinlan sideloads run under the standard iOS sandbox — on iOS 14+
// a bind() to 0.0.0.0 there is silently denied by the Local Network
// gate unless the app declares NSLocalNetworkUsageDescription AND
// the user grants the permission on first launch. Neither is
// reliably true for a chinlan IPA, so `iproxy` gets "Bad file
// descriptor" back on every connect. Loopback bypasses the gate;
// usbmuxd's TCP tunnel from `iproxy` reaches us on 127.0.0.1
// anyway, so tailing via `nc host.docker.internal 18082` from a
// dev host still works. Direct LAN access from another host is
// unavailable on chinlan builds — swap flavour for that.
addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
#else
// JB (libsubstrate) and jailed (Dobby-static) both run outside the
// App Store sandbox and the Local Network gate does not apply,
// so keep the LAN-visible bind for operator convenience: any host
// on the same network can `nc <device-ip> 18082` without a Mac
// side proxy in the middle.
addr.sin_addr.s_addr = htonl(INADDR_ANY);
#endif
if (bind(s, (struct sockaddr *)&addr, sizeof(addr)) < 0) {
IPALog([NSString stringWithFormat:@"[LOGSVR] exception: op=bind errno=%d port=%u",
errno, (unsigned)port]);
close(s);
return;
}
if (listen(s, IPA_LOG_SERVER_MAX_CLIENTS) < 0) {
IPALog([NSString stringWithFormat:@"[LOGSVR] exception: op=listen errno=%d", errno]);
close(s);
return;
}
ls_set_nonblock(s);
g_listenFd = s;
g_listenSrc = dispatch_source_create(DISPATCH_SOURCE_TYPE_READ,
(uintptr_t)s, 0, g_queue);
dispatch_source_set_event_handler(g_listenSrc, ^{ ls_handle_accept(); });
dispatch_resume(g_listenSrc);
IPALog([NSString stringWithFormat:
#if IPA_CHINLAN
@"[LOGSVR] armed: host=127.0.0.1 port=%u build=debug", (unsigned)port]);
#else
@"[LOGSVR] armed: host=0.0.0.0 port=%u build=debug", (unsigned)port]);
#endif
}
void IPALogServerPush(NSString *line) {
if (!line || !g_queue) return;
uint32_t pending = atomic_load(&g_pending);
if (pending >= LOG_SERVER_QUEUE_DEPTH_MAX) {
uint32_t dropped = atomic_fetch_add(&g_dropped, 1) + 1;
if ((dropped % 64) == 0) {
// Can't call IPALog here (would recurse); use NSLog directly.
NSLog(@"[LOGSVR] drop backlog=%u dropped=%u", pending, dropped);
}
return;
}
atomic_fetch_add(&g_pending, 1);
NSString *withNewline = [line hasSuffix:@"\n"]
? [line copy]
: [line stringByAppendingString:@"\n"];
dispatch_async(g_queue, ^{
atomic_fetch_sub(&g_pending, 1);
if (g_clientCount == 0) return;
NSData *data = [withNewline dataUsingEncoding:NSUTF8StringEncoding];
const uint8_t *bytes = (const uint8_t *)data.bytes;
size_t len = data.length;
// Drain inbound bytes first to catch dead peers before we try to send.
for (int i = g_clientCount - 1; i >= 0; i--) {
if (!ls_drain_client(g_clients[i])) {
ls_evict_at(i);
}
}
// Broadcast to surviving clients; evict on send failure.
for (int i = g_clientCount - 1; i >= 0; i--) {
if (!ls_send_all(g_clients[i], bytes, len)) {
ls_evict_at(i);
}
}
});
}
#endif // !FINAL_RELEASE