Skip to content

Commit fe21db5

Browse files
committed
Add FLEXSwiftPrintRedirector to capture Swift print output.
- Implement stdout/stderr pipe redirection in Objective-C - Capture Swift print() and debugPrint() output - Use callback-based message handler for direct FLEX integration - Add line buffering to handle partial pipe reads correctly
1 parent 690acaa commit fe21db5

File tree

2 files changed

+212
-0
lines changed

2 files changed

+212
-0
lines changed
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
//
2+
// FLEXSwiftPrintRedirector.h
3+
// FLEX
4+
//
5+
// Created by 김인환 on 2025.
6+
// Copyright © 2025 FLEX Team. All rights reserved.
7+
//
8+
9+
#import <Foundation/Foundation.h>
10+
11+
NS_ASSUME_NONNULL_BEGIN
12+
13+
@class FLEXSystemLogMessage;
14+
15+
/// Redirects Swift print() output to FLEX System Log
16+
@interface FLEXSwiftPrintRedirector : NSObject
17+
18+
/// Enable redirection of stdout/stderr to capture Swift print output
19+
+ (void)enableSwiftPrintRedirection;
20+
21+
/// Disable redirection and restore original stdout/stderr
22+
+ (void)disableSwiftPrintRedirection;
23+
24+
/// Check if redirection is currently enabled
25+
+ (BOOL)isRedirectionEnabled;
26+
27+
/// Set a callback to receive captured messages
28+
+ (void)setMessageHandler:(void(^)(FLEXSystemLogMessage *message))handler;
29+
30+
@end
31+
32+
NS_ASSUME_NONNULL_END
Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
//
2+
// FLEXSwiftPrintRedirector.m
3+
// FLEX
4+
//
5+
// Created by 김인환 on 2025.
6+
// Copyright © 2025 FLEX Team. All rights reserved.
7+
//
8+
9+
#import "FLEXSwiftPrintRedirector.h"
10+
#import "FLEXSystemLogMessage.h"
11+
12+
@implementation FLEXSwiftPrintRedirector
13+
14+
static BOOL _isRedirectionEnabled = NO;
15+
static int _originalStdout = -1;
16+
static int _originalStderr = -1;
17+
static NSPipe *_stdoutPipe = nil;
18+
static NSPipe *_stderrPipe = nil;
19+
static id _stdoutObserver = nil;
20+
static id _stderrObserver = nil;
21+
static dispatch_queue_t _logQueue = nil;
22+
static void(^_messageHandler)(FLEXSystemLogMessage *) = nil;
23+
static NSMutableString *_stdoutBuffer = nil;
24+
static NSMutableString *_stderrBuffer = nil;
25+
26+
+ (void)enableSwiftPrintRedirection {
27+
@synchronized(self) {
28+
if (_isRedirectionEnabled) {
29+
return;
30+
}
31+
32+
_isRedirectionEnabled = YES;
33+
_logQueue = dispatch_queue_create("com.flex.swiftprint", DISPATCH_QUEUE_SERIAL);
34+
_stdoutBuffer = [NSMutableString string];
35+
_stderrBuffer = [NSMutableString string];
36+
37+
// Save original stdout and stderr
38+
_originalStdout = dup(STDOUT_FILENO);
39+
_originalStderr = dup(STDERR_FILENO);
40+
41+
// Create pipes
42+
_stdoutPipe = [NSPipe pipe];
43+
_stderrPipe = [NSPipe pipe];
44+
45+
// Redirect stdout
46+
dup2(_stdoutPipe.fileHandleForWriting.fileDescriptor, STDOUT_FILENO);
47+
48+
// Redirect stderr
49+
dup2(_stderrPipe.fileHandleForWriting.fileDescriptor, STDERR_FILENO);
50+
51+
// Start reading from stdout pipe
52+
NSFileHandle *stdoutReadHandle = _stdoutPipe.fileHandleForReading;
53+
_stdoutObserver = [[NSNotificationCenter defaultCenter]
54+
addObserverForName:NSFileHandleReadCompletionNotification
55+
object:stdoutReadHandle
56+
queue:nil
57+
usingBlock:^(NSNotification *note) {
58+
NSData *data = note.userInfo[NSFileHandleNotificationDataItem];
59+
if (data.length > 0) {
60+
[self processOutputData:data isError:NO buffer:_stdoutBuffer];
61+
[stdoutReadHandle readInBackgroundAndNotify];
62+
}
63+
}];
64+
[stdoutReadHandle readInBackgroundAndNotify];
65+
66+
// Start reading from stderr pipe
67+
NSFileHandle *stderrReadHandle = _stderrPipe.fileHandleForReading;
68+
_stderrObserver = [[NSNotificationCenter defaultCenter]
69+
addObserverForName:NSFileHandleReadCompletionNotification
70+
object:stderrReadHandle
71+
queue:nil
72+
usingBlock:^(NSNotification *note) {
73+
NSData *data = note.userInfo[NSFileHandleNotificationDataItem];
74+
if (data.length > 0) {
75+
[self processOutputData:data isError:YES buffer:_stderrBuffer];
76+
[stderrReadHandle readInBackgroundAndNotify];
77+
}
78+
}];
79+
[stderrReadHandle readInBackgroundAndNotify];
80+
}
81+
}
82+
83+
+ (void)disableSwiftPrintRedirection {
84+
@synchronized(self) {
85+
if (!_isRedirectionEnabled) {
86+
return;
87+
}
88+
89+
_isRedirectionEnabled = NO;
90+
91+
// Remove observers
92+
if (_stdoutObserver) {
93+
[[NSNotificationCenter defaultCenter] removeObserver:_stdoutObserver];
94+
_stdoutObserver = nil;
95+
}
96+
if (_stderrObserver) {
97+
[[NSNotificationCenter defaultCenter] removeObserver:_stderrObserver];
98+
_stderrObserver = nil;
99+
}
100+
101+
// Restore original stdout and stderr
102+
if (_originalStdout != -1) {
103+
dup2(_originalStdout, STDOUT_FILENO);
104+
close(_originalStdout);
105+
_originalStdout = -1;
106+
}
107+
if (_originalStderr != -1) {
108+
dup2(_originalStderr, STDERR_FILENO);
109+
close(_originalStderr);
110+
_originalStderr = -1;
111+
}
112+
113+
// Clean up pipes
114+
_stdoutPipe = nil;
115+
_stderrPipe = nil;
116+
}
117+
}
118+
119+
+ (BOOL)isRedirectionEnabled {
120+
@synchronized(self) {
121+
return _isRedirectionEnabled;
122+
}
123+
}
124+
125+
+ (void)setMessageHandler:(void(^)(FLEXSystemLogMessage *message))handler {
126+
@synchronized(self) {
127+
_messageHandler = [handler copy];
128+
}
129+
}
130+
131+
+ (void)processOutputData:(NSData *)data isError:(BOOL)isError buffer:(NSMutableString *)buffer {
132+
// Write to original file descriptor first to preserve console output
133+
int originalFd = isError ? _originalStderr : _originalStdout;
134+
if (originalFd != -1) {
135+
write(originalFd, data.bytes, data.length);
136+
}
137+
138+
// Process on background queue to avoid blocking
139+
dispatch_async(_logQueue, ^{
140+
NSString *output = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
141+
if (!output) {
142+
output = [[NSString alloc] initWithData:data encoding:NSASCIIStringEncoding];
143+
}
144+
145+
if (output && _messageHandler) {
146+
// Append to buffer to handle partial lines
147+
@synchronized(buffer) {
148+
[buffer appendString:output];
149+
150+
// Process complete lines (ending with newline)
151+
while (YES) {
152+
NSRange newlineRange = [buffer rangeOfString:@"\n"];
153+
if (newlineRange.location == NSNotFound) {
154+
break;
155+
}
156+
157+
// Extract complete line
158+
NSString *line = [buffer substringToIndex:newlineRange.location];
159+
[buffer deleteCharactersInRange:NSMakeRange(0, newlineRange.location + 1)];
160+
161+
NSString *trimmedLine = [line stringByTrimmingCharactersInSet:
162+
[NSCharacterSet whitespaceCharacterSet]];
163+
if (trimmedLine.length > 0) {
164+
// Create FLEX message directly without going through NSLog/os_log
165+
NSString *formattedMessage = [NSString stringWithFormat:@"[SwiftPrint] %@", trimmedLine];
166+
FLEXSystemLogMessage *message = [FLEXSystemLogMessage
167+
logMessageFromDate:[NSDate date]
168+
text:formattedMessage];
169+
170+
dispatch_async(dispatch_get_main_queue(), ^{
171+
_messageHandler(message);
172+
});
173+
}
174+
}
175+
}
176+
}
177+
});
178+
}
179+
180+
@end

0 commit comments

Comments
 (0)