8

I have an app that is making use of UITextChecker class provided by Apple. This class has a following bug: If the iOS device is offline (expected to be often for my app) each time I call some of UITextCheckers methods it logs following to console:

2016-03-08 23:48:02.119 HereIsMyAppName [4524:469877] UITextChecker sent string:isExemptFromTextCheckerWithCompletionHandler: to com.apple.TextInput.rdt but received error Error Domain=NSCocoaErrorDomain Code=4099 "The connection to service named com.apple.TextInput.rdt was invalidated."

I do not want to have logs spammed by this messages. Is there a way to disable logging from code? I would disable logging before call to any of UITextCheckers methods and reenable it afterwards. Or is there perhaps any way how to disable logging selectively per class (event if it is foundation class not mine)? Or any other solution?

JAL
  • 41,701
  • 23
  • 172
  • 300
Rasto
  • 17,204
  • 47
  • 154
  • 245
  • 2
    Deleted my answer. As far as I know it's impossible to short-circuit NSLog, but let's hope for your sake that someone will prove me wrong... – deadbeef Mar 09 '16 at 01:58
  • @deadbeef Thank you for your advice and for deleting "it is not possible" asnwer. I consider your reactions to be of exemplary SO user but I hope you are wrong in this case and someone will come up with solution/workaround. The oposit would be very bad news for me. – Rasto Mar 09 '16 at 02:07
  • 1
    Possible duplicate of [Hiding NSLog output coming from precompiled library](http://stackoverflow.com/questions/12608988/hiding-nslog-output-coming-from-precompiled-library). There is – as far as I know – no way of *disabling* NSLog in compiled code (such as in Apple's libraries), *redirecting* stderr seems to be the only workaround. – Martin R Mar 09 '16 at 03:54
  • @MartinR Redirecting stderr might be viable solution for me (just for calls to `UITextChecker` methods). I have 2 questions about it: 1. If I did redirect in production and the app crashes during the call for which stderr is redirected would the crash log from Apple containt the exception that casused the crash or not? 2. What is performance cost of the redirect? If I understand it correctly the code is opening files. In some cases I would need to do that redirect several times per second while also running animations etc. – Rasto Mar 09 '16 at 14:20
  • I agree with Martin R. As far as I know there is now way to conditionally turn off NSLog. Having said that, this seems like a bug in the apple's API, I would file a radar if I were you. – Danny Bravo Mar 16 '16 at 10:15
  • @deadbeef check out my answer and let me know what you think, I've managed to short-circuit `NSLog`. – JAL Mar 16 '16 at 19:14
  • @MartinR I'm curious if my answer would be considered redirecting stderr. I'm hooking into the function which calls `printf` for `NSLog`, which seems to come in one step before where the strings are logged. – JAL Mar 16 '16 at 19:15
  • drasto, do you have any other questions? Please let me know. – JAL Mar 29 '16 at 15:34

1 Answers1

11

Warning: This answer uses the private-yet-sort-of-documented Cocoa C functions _NSSetLogCStringFunction() and _NSLogCStringFunction().

_NSSetLogCStringFunction() is an interface created by Apple to handle the implementation function for NSLog. It was initially exposed for WebObjects to hook into NSLog statements on Windows machines, but still exists in the iOS and OS X APIs today. It is documented in this support article.

The function takes in a function pointer with the arguments const char* message, the string to log, unsigned length, the length of the message, and a BOOL withSysLogBanner which toggles the standard logging banner. If we create our own hook function for logging that doesn't actually do anything (an empty implementation rather than calling fprintf like NSLog does behind-the-scenes), we can effectively disable all logging for your application.

Objective-C Example (or Swift with bridging header):

extern void _NSSetLogCStringFunction(void(*)(const char*, unsigned, BOOL));

static void hookFunc(const char* message, unsigned length, BOOL withSysLogBanner) { /* Empty */ }

// Later in your application

_NSSetLogCStringFunction(hookFunc);

NSLog(@"Hello _NSSetLogCStringFunction!\n\n");  // observe this isn't logged

An example implementation of this can be found in YILogHook, which provides an interface to add an array of blocks to any NSLog statement (write to file, etc).

Pure Swift Example:

@asmname("_NSSetLogCStringFunction") // NOTE: As of Swift 2.2 @asmname has been renamed to @_silgen_name
func _NSSetLogCStringFunction(_: ((UnsafePointer<Int8>, UInt32, Bool) -> Void)) -> Void

func hookFunc(message: UnsafePointer<Int8>, _ length: UInt32, _ withSysLogBanner: Bool) -> Void { /* Empty */ }

_NSSetLogCStringFunction(hookFunc)

NSLog("Hello _NSSetLogCStringFunction!\n\n");  // observe this isn't logged

In Swift, you can also chose to ignore all of the block parameters without using hookFunc like so:

_NSSetLogCStringFunction { _,_,_ in }

To turn logging back on using Objective-C, just pass in NULL as the function pointer:

_NSSetLogCStringFunction(NULL);

With Swift things are a little different, since the compiler will complain about a type mismatch if we try to pass in nil or a nil pointer (NULL is unavailable in Swift). To solve this, we need to access another system function, _NSLogCStringFunction, to get a pointer to the default logging implementation, retain that reference while logging is disabled, and set the reference back when we want to turn logging back on.

I've cleaned up the Swift implementation of this by adding a NSLogCStringFunc typedef:

/// Represents the C function signature used under-the-hood by NSLog
typealias NSLogCStringFunc = (UnsafePointer<Int8>, UInt32, Bool) -> Void

/// Sets the C function used by NSLog
@_silgen_name("_NSSetLogCStringFunction") // NOTE: As of Swift 2.2 @asmname has been renamed to @_silgen_name
func _NSSetLogCStringFunction(_: NSLogCStringFunc) -> Void

/// Retrieves the current C function used by NSLog
@_silgen_name("_NSLogCStringFunction")
func _NSLogCStringFunction() -> NSLogCStringFunc

let logFunc = _NSLogCStringFunction() // get function pointer to the system log function before we override it

_NSSetLogCStringFunction { (_, _, _) in } // set our own log function to do nothing in an anonymous closure

NSLog("Observe this isn't logged.");

_NSSetLogCStringFunction(logFunc) // switch back to the system log function

NSLog("Observe this is logged.")
JAL
  • 41,701
  • 23
  • 172
  • 300
  • That's amazing !! Check out [this](https://github.com/cjwl/cocotron/blob/master/Foundation/NSObjCRuntime.m) too ! It's all there, so you can indeed disable it and re-enable it later. – deadbeef Mar 16 '16 at 19:48
  • @deadbeef Yep just found that as well. That shows you Apple's implementation of the logging. You can provide your own function pointer to change `NSLog` implementation. Check out `NSPlatformLogString` in [`NSPlatform`](https://github.com/cjwl/cocotron/blob/master/Foundation/platform_posix/NSPlatform_posix.m). – JAL Mar 16 '16 at 19:52
  • Interesting, I hadn't seen that before! – Martin R Mar 16 '16 at 19:53
  • 1
    You can use `_NSLogCStringFunction()` to get the old function first, replace it temporarily and then re-set it to it's original value. – deadbeef Mar 16 '16 at 19:57
  • Didn't know about this Cocotron project. Looks like there has been a lot of work there ! – deadbeef Mar 16 '16 at 20:05
  • @JAL Side-note not related to this question. Where do you find documentation about those low-level swift things like `@asmname` ? I can't find any official documentation that covers all those things. – deadbeef Mar 16 '16 at 20:09
  • Actually passing `NULL` to `_NSSetLogCStringFunction()` resets it to it's default value. Even better. – deadbeef Mar 16 '16 at 20:17
  • @deadbeef oh awesome, even easier. Just read through the Foundation and Swift source code: https://github.com/apple/swift-corelibs-foundation – JAL Mar 16 '16 at 20:17
  • @deadbeef also this question helped explain how to "extern" a C function in Swift: http://stackoverflow.com/q/35183818/2415822 – JAL Mar 16 '16 at 20:18
  • No real well-written exhaustive documentation (yet ?) then. I was afraid of that. But thanks ! – deadbeef Mar 16 '16 at 20:21
  • @deadbeef Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/106505/discussion-between-jal-and-deadbeef). – JAL Mar 16 '16 at 20:22
  • Nice research. How does this solution compare with redirecting stderr as suggested by @MartinR and described in [Hiding NSLog output coming from precompiled library](http://stackoverflow.com/questions/12608988/hiding-nslog-output-coming-from-precompiled-library)? In terms of performance, private APIs usage, etc. – Rasto Mar 16 '16 at 21:48
  • @drasto Well for one my solution is a level higher than redirecting logging. I intercept any strings to be logged before they even get `fprintf`'d. I'd also say that Apple "unofficially" endorses this hack, or else they wouldn't have written a support article showing this workaround. I'd say it a better solution than trying to redirect the output post-logging. Trace and identify the issue at its source. – JAL Mar 16 '16 at 21:53
  • @drasto Your milage may vary. Try both and figure out what works best for you. – JAL Mar 16 '16 at 21:53
  • Thank you. What do you mean by *Trace and identify the issue at its source.*? I can hardly trace issue in Apple's compiled libraries... – Rasto Mar 16 '16 at 21:59
  • @drasto Well for starters I looked into swizzling `NSLog` which lead me to [this](http://stackoverflow.com/q/4332041/2415822) question which mentioned `_NSSetLogCStringFunction()`. From there I found YILogHook which gave me an understanding of Apple's `NSLog` implementation and enough information to write a few lines of Objective-C and Swift which solved your problem. – JAL Mar 16 '16 at 22:08
  • @drasto people have been talking about it for as long as Objective-C has been around, and [this](http://openradar.appspot.com/radar?id=242) SDK enhancement has been open for almost 10 years – JAL Mar 16 '16 at 22:10
  • Could you please update you answer to include reenabling the logging as well? Thank you! – Rasto Mar 25 '16 at 02:48
  • @drasto Added a way to turn logging back on in Objective-C. Still working on a Swift solution. – JAL Mar 25 '16 at 14:24
  • @drasto added a Swift example of how to turn logging back on. Let me know if you have any other questions. – JAL Mar 25 '16 at 14:43
  • While disabling loging works fine reenabling it does not work for me in Swift. Line `_NSSetLogCStringFunction(logFunc)` causes crash `EXC_BAD_ACCESS (code=2, address=...)`. It always crashes. If `logFunc` is declared as `var logFunc: NSLogCStringFunc? = _NSLogCStringFunction()` and reenabling is done with `if let logFunc = logFunc { _NSSetLogCStringFunction(logFunc) }` it is a bit more reliable but crashes as well after few disable-enable cycles. – Rasto Mar 30 '16 at 01:29
  • @drasto which version of iOS are you targeting and which version of Xcode are you using? – JAL Mar 30 '16 at 14:50
  • Supported iOS versions: 8.3-9.3. Crash observed on iOS 9.1. Xcode 7.2. – Rasto Mar 30 '16 at 17:02
  • @drasto I'm still trying to reproduce your crash. Is it on any specific device? Or just a range. – JAL Apr 19 '16 at 18:55
  • Happens on iPhone 6S, iPhone 6, iPhone 6 Plus, .... Does not always happen by is very frequent. – Rasto Apr 20 '16 at 23:17
  • @drasto I think my typedef was wrong. Does this discussion in [this question](http://stackoverflow.com/q/36916772/2415822) solve your crash? I'll update my answer if it has. – JAL Apr 28 '16 at 14:25
  • @drasto specifically the `@convention(c)` part. – JAL Apr 28 '16 at 14:31
  • Sorry right now I cannot try it - I have much more serious problems with my app. I will get back to you when they are solved. Hope you understand. I really appreciate your determination to solve this problem! – Rasto May 02 '16 at 21:37
  • 1
    @drasto of course! I'm curious about this problem as well, that's why I followed up. No rush, get back to me when you can. Thanks and good luck! – JAL May 02 '16 at 21:38
  • Am trying to use this in Xcode 12.3, Swift 5.3. It's not working, however. I placed @_silgen_name("_NSSetLogCStringFunction") func _NSSetLogCStringFunction(_: NSLogCStringFunc) -> Void. into top of my AppDelegate.swift file, then in application:didFinishLaunchingWithOptions: I added: _NSSetLogCStringFunction { (_, _, _) in }. Then added a call to NSLog, which dumped my message to Xcode's console. So Apple must have changed the private method signature? – Smartcat Feb 02 '21 at 21:31