Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions Classes/GlobalStateExplorers/SystemLog/FLEXSwiftPrintRedirector.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
//
// FLEXSwiftPrintRedirector.h
// FLEX
//
// Created by 김인환 on 2025.
// Copyright © 2025 FLEX Team. All rights reserved.
//

#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@class FLEXSystemLogMessage;

/// Redirects Swift print() output to FLEX System Log
@interface FLEXSwiftPrintRedirector : NSObject

/// Enable redirection of stdout/stderr to capture Swift print output
+ (void)enableSwiftPrintRedirection;

/// Disable redirection and restore original stdout/stderr
+ (void)disableSwiftPrintRedirection;

/// Check if redirection is currently enabled
+ (BOOL)isRedirectionEnabled;

/// Set a callback to receive captured messages
+ (void)setMessageHandler:(void(^)(FLEXSystemLogMessage *message))handler;

@end

NS_ASSUME_NONNULL_END
180 changes: 180 additions & 0 deletions Classes/GlobalStateExplorers/SystemLog/FLEXSwiftPrintRedirector.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
//
// FLEXSwiftPrintRedirector.m
// FLEX
//
// Created by 김인환 on 2025.
// Copyright © 2025 FLEX Team. All rights reserved.
//

#import "FLEXSwiftPrintRedirector.h"
#import "FLEXSystemLogMessage.h"

@implementation FLEXSwiftPrintRedirector

static BOOL _isRedirectionEnabled = NO;
static int _originalStdout = -1;
static int _originalStderr = -1;
static NSPipe *_stdoutPipe = nil;
static NSPipe *_stderrPipe = nil;
static id _stdoutObserver = nil;
static id _stderrObserver = nil;
static dispatch_queue_t _logQueue = nil;
static void(^_messageHandler)(FLEXSystemLogMessage *) = nil;
static NSMutableString *_stdoutBuffer = nil;
static NSMutableString *_stderrBuffer = nil;

+ (void)enableSwiftPrintRedirection {
@synchronized(self) {
if (_isRedirectionEnabled) {
return;
}

_isRedirectionEnabled = YES;
_logQueue = dispatch_queue_create("com.flex.swiftprint", DISPATCH_QUEUE_SERIAL);
_stdoutBuffer = [NSMutableString string];
_stderrBuffer = [NSMutableString string];

// Save original stdout and stderr
_originalStdout = dup(STDOUT_FILENO);
_originalStderr = dup(STDERR_FILENO);

// Create pipes
_stdoutPipe = [NSPipe pipe];
_stderrPipe = [NSPipe pipe];

// Redirect stdout
dup2(_stdoutPipe.fileHandleForWriting.fileDescriptor, STDOUT_FILENO);

// Redirect stderr
dup2(_stderrPipe.fileHandleForWriting.fileDescriptor, STDERR_FILENO);

// Start reading from stdout pipe
NSFileHandle *stdoutReadHandle = _stdoutPipe.fileHandleForReading;
_stdoutObserver = [[NSNotificationCenter defaultCenter]
addObserverForName:NSFileHandleReadCompletionNotification
object:stdoutReadHandle
queue:nil
usingBlock:^(NSNotification *note) {
NSData *data = note.userInfo[NSFileHandleNotificationDataItem];
if (data.length > 0) {
[self processOutputData:data isError:NO buffer:_stdoutBuffer];
[stdoutReadHandle readInBackgroundAndNotify];
}
}];
[stdoutReadHandle readInBackgroundAndNotify];

// Start reading from stderr pipe
NSFileHandle *stderrReadHandle = _stderrPipe.fileHandleForReading;
_stderrObserver = [[NSNotificationCenter defaultCenter]
addObserverForName:NSFileHandleReadCompletionNotification
object:stderrReadHandle
queue:nil
usingBlock:^(NSNotification *note) {
NSData *data = note.userInfo[NSFileHandleNotificationDataItem];
if (data.length > 0) {
[self processOutputData:data isError:YES buffer:_stderrBuffer];
[stderrReadHandle readInBackgroundAndNotify];
}
}];
[stderrReadHandle readInBackgroundAndNotify];
}
}

+ (void)disableSwiftPrintRedirection {
@synchronized(self) {
if (!_isRedirectionEnabled) {
return;
}

_isRedirectionEnabled = NO;

// Remove observers
if (_stdoutObserver) {
[[NSNotificationCenter defaultCenter] removeObserver:_stdoutObserver];
_stdoutObserver = nil;
}
if (_stderrObserver) {
[[NSNotificationCenter defaultCenter] removeObserver:_stderrObserver];
_stderrObserver = nil;
}

// Restore original stdout and stderr
if (_originalStdout != -1) {
dup2(_originalStdout, STDOUT_FILENO);
close(_originalStdout);
_originalStdout = -1;
}
if (_originalStderr != -1) {
dup2(_originalStderr, STDERR_FILENO);
close(_originalStderr);
_originalStderr = -1;
}

// Clean up pipes
_stdoutPipe = nil;
_stderrPipe = nil;
}
}

+ (BOOL)isRedirectionEnabled {
@synchronized(self) {
return _isRedirectionEnabled;
}
}

+ (void)setMessageHandler:(void(^)(FLEXSystemLogMessage *message))handler {
@synchronized(self) {
_messageHandler = [handler copy];
}
}

+ (void)processOutputData:(NSData *)data isError:(BOOL)isError buffer:(NSMutableString *)buffer {
// Write to original file descriptor first to preserve console output
int originalFd = isError ? _originalStderr : _originalStdout;
if (originalFd != -1) {
write(originalFd, data.bytes, data.length);
}

// Process on background queue to avoid blocking
dispatch_async(_logQueue, ^{
NSString *output = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
if (!output) {
output = [[NSString alloc] initWithData:data encoding:NSASCIIStringEncoding];
}

if (output && _messageHandler) {
// Append to buffer to handle partial lines
@synchronized(buffer) {
[buffer appendString:output];

// Process complete lines (ending with newline)
while (YES) {
NSRange newlineRange = [buffer rangeOfString:@"\n"];
if (newlineRange.location == NSNotFound) {
break;
}

// Extract complete line
NSString *line = [buffer substringToIndex:newlineRange.location];
[buffer deleteCharactersInRange:NSMakeRange(0, newlineRange.location + 1)];

NSString *trimmedLine = [line stringByTrimmingCharactersInSet:
[NSCharacterSet whitespaceCharacterSet]];
if (trimmedLine.length > 0) {
// Create FLEX message directly without going through NSLog/os_log
NSString *formattedMessage = [NSString stringWithFormat:@"[SwiftPrint] %@", trimmedLine];
FLEXSystemLogMessage *message = [FLEXSystemLogMessage
logMessageFromDate:[NSDate date]
text:formattedMessage];

dispatch_async(dispatch_get_main_queue(), ^{
_messageHandler(message);
});
}
}
}
}
});
}

@end
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
#import "UIBarButtonItem+FLEX.h"
#import "NSUserDefaults+FLEX.h"
#import "flex_fishhook.h"
#import "FLEXSwiftPrintRedirector.h"
#import <dlfcn.h>

@interface FLEXSystemLogViewController ()
Expand Down Expand Up @@ -111,6 +112,15 @@ - (void)viewDidLoad {
_logController = [FLEXASLLogController withUpdateHandler:logHandler];
}

// Enable Swift print redirection
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
[FLEXSwiftPrintRedirector setMessageHandler:^(FLEXSystemLogMessage *message) {
[self handleUpdateWithNewMessages:@[message]];
}];
[FLEXSwiftPrintRedirector enableSwiftPrintRedirection];
});

self.tableView.separatorStyle = UITableViewCellSeparatorStyleNone;
self.title = @"Waiting for Logs...";

Expand Down
8 changes: 7 additions & 1 deletion Example/FLEXample/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,13 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
self.repeatingLogExampleTimer = Timer.scheduledTimer(withTimeInterval: 3, repeats: true) { [weak self] (_) in
if let self = self {
NSLog("Example log \(self.exampleLogSent)")


// Test Swift print redirection
print("Swift print test \(self.exampleLogSent)")
print("Multiple line")
print("Swift print test")
debugPrint("Debug print test \(self.exampleLogSent)")

self.exampleLogSent += 1
if self.exampleLogSent > self.exampleLogLimit {
self.repeatingLogExampleTimer.invalidate()
Expand Down