Skip to content

Instantly share code, notes, and snippets.

@joncardasis
Last active July 25, 2024 18:49
Show Gist options
  • Save joncardasis/5612f19575766cbbd1672437427cba88 to your computer and use it in GitHub Desktop.
Save joncardasis/5612f19575766cbbd1672437427cba88 to your computer and use it in GitHub Desktop.
Reroute print and NSLogs to BOTH Xcode console and a file url [Swift + ObjC Solutions]
//
// DebugLogger.swift
//
// Created by Jonathan Cardasis. on 1/1/18.
// Copyright ©2018 Jonathan Cardasis. All rights reserved.
//
#if DEBUG
import Foundation
class DebugLogger {
static let shared: DebugLogger = {
let logger = DebugLogger()
logger.openConsolePipe()
return logger
}()
static private var inputPipe = Pipe()
static private var outputPipe = Pipe()
static private var fileRedirectURL: URL?
private init() { }
func redirectOutputToFile(_ fileURL: URL) {
DebugLogger.fileRedirectURL = fileURL
}
private func openConsolePipe() {
// Route everything that comes in throught the outputPipe back to xcode console
dup2(fileno(stdout), DebugLogger.outputPipe.fileHandleForWriting.fileDescriptor)
// Route printing functions that go to sys pipes (stdout & stderr) into local inputPipe
dup2(DebugLogger.inputPipe.fileHandleForWriting.fileDescriptor, fileno(stdout))
dup2(DebugLogger.inputPipe.fileHandleForWriting.fileDescriptor, fileno(stderr))
// Begin notification listener
let inputReadHandler = DebugLogger.inputPipe.fileHandleForReading
NotificationCenter.default.addObserver(self, selector: #selector(handlePipeNotification(notification:)), name: FileHandle.readCompletionNotification, object: inputReadHandler)
inputReadHandler.readInBackgroundAndNotify()
}
@objc private func handlePipeNotification(notification: Notification) {
// Listen for next input
DebugLogger.inputPipe.fileHandleForReading.readInBackgroundAndNotify()
if let data = notification.userInfo?[NSFileHandleNotificationDataItem] as? Data {
// Write data back to output pipe for xcode to display
DebugLogger.outputPipe.fileHandleForWriting.write(data)
// Write to file if applicable
if let fileURL = DebugLogger.fileRedirectURL {
if !FileManager.default.fileExists(atPath: fileURL.path) {
FileManager.default.createFile(atPath: fileURL.path, contents: nil, attributes: nil)
}
do {
let fileHandle = try FileHandle(forWritingTo: fileURL)
fileHandle.seekToEndOfFile()
fileHandle.write(data)
fileHandle.closeFile()
} catch { /* pass */ }
}
}
}
}
#endif
//
// DebugLogger.h
//
// Created by Jonathan Cardasis. on 3/1/18.
//
#ifdef DEBUG
#import <Foundation/Foundation.h>
@interface DebugLogger : NSObject
+ (id)sharedInstance;
- (void)redirectOutputToFile:(NSURL*) fileURL;
@end
#endif
//----------------------------------------------//
//
// DebugLogger.m
//
// Created by Jonathan Cardasis. on 3/1/18.
//
#ifdef DEBUG
#import "DebugLogger.h"
@implementation DebugLogger
static NSPipe *inputPipe;
static NSPipe *outputPipe;
static NSURL *fileRedirectURL;
+ (id)sharedInstance {
static dispatch_once_t s = 0;
__strong static id _sharedObject = nil;
dispatch_once(&s, ^{
_sharedObject = [[self alloc] init];
inputPipe = [[NSPipe alloc] init];
outputPipe = [[NSPipe alloc] init];
[_sharedObject openConsolePipe];
});
return _sharedObject;
}
- (void)redirectOutputToFile:(NSURL*) fileURL {
fileRedirectURL = fileURL;
}
// MARK: - Private
- (void)openConsolePipe {
// Route everything that comes in throught the outputPipe back to xcode console
dup2(fileno(stdout), outputPipe.fileHandleForWriting.fileDescriptor);
// Route printing functions that go to sys pipes (stdout & stderr) into local inputPipe
dup2(inputPipe.fileHandleForWriting.fileDescriptor, fileno(stdout));
dup2(inputPipe.fileHandleForWriting.fileDescriptor, fileno(stderr));
// Begin notification listener
NSFileHandle *inputReadHandler = inputPipe.fileHandleForReading;
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handlePipeNotification:) name:NSFileHandleReadCompletionNotification object:inputReadHandler];
[inputReadHandler readInBackgroundAndNotify];
}
- (void)handlePipeNotification:(NSNotification*) notification {
// Listen for next input
[inputPipe.fileHandleForReading readInBackgroundAndNotify];
NSDictionary *userInfo = notification.userInfo;
if (userInfo == nil) {
return;
}
NSData *data = (NSData*)userInfo[NSFileHandleNotificationDataItem];
if (data == nil) {
return;
}
[outputPipe.fileHandleForWriting writeData:data];
NSURL *fileURL = fileRedirectURL;
if (fileURL != nil) {
if (![NSFileManager.defaultManager fileExistsAtPath:fileURL.path]) {
[NSFileManager.defaultManager createFileAtPath:fileURL.path contents:nil attributes:nil];
}
NSError *err;
NSFileHandle *fileHandle = [NSFileHandle fileHandleForWritingToURL:fileURL error:&err];
if (err == nil) {
[fileHandle seekToEndOfFile];
[fileHandle writeData:data];
[fileHandle closeFile];
}
}
}
@end
#endif
func main() {
#if DEBUG
let path = URL(fileURLWithPath: NSHomeDirectory()).appendingPathComponent("Documents/MyApp.log")
DebugLogger.shared.redirectOutputToFile(path)
#endif
print("Swiftly logging")
NSLog("Objectively saying something")
}
// ...
#ifdef DEBUG
NSURL *debuggingLogURL = [[NSURL fileURLWithPath:NSHomeDirectory()] URLByAppendingPathComponent:@"Documents/MyApp.log"];
[DebugLogger.sharedInstance redirectOutputToFile:debuggingLogURL];
#endif
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment