Log synchronization in IOS to obtain nslog redirection and other details

Time:2020-12-18

preface

For those engineers who do back-end development, it should be a matter of course to watch logs and solve bugs. However, many of the mobile application development engineers I have contacted do not have this awareness. When checking bugs, they always try to reproduce and debug, especially for some bugs that are not easy to reproduce.

When testing a real machine, we often find that it is difficult to view the real-time log of nslog type of the real machine. At this time, we need to locate the log of the time by the RD replication problem, so as to find the problem conveniently. This problem is very common in testing, and it is also a reason why functional testing takes a long time.

Let’s talk about several ways to view logs in real time.

Where does nslog output?

In IOS development, we often use nslog debugging, but we don’t know much about it. In nslog, it is a C function in essence. Its function declaration is as follows:
FOUNDATION_EXPORT void NSLog(NSString *format, ...)

Log an error message to the apple system log facility. It is used to output information to the standard error console. In fact, it uses the API of Apple system log. In the debugging phase, the log will be output to Xcode, while on IOS, it will be output to the system’s / var / log / syslog file.

In IOS, the handle to output log to file is defined in unistd. H file


#define STDIN_FILENO 0 /* standard input file descriptor */
#define STDOUT_FILENO 1 /* standard output file descriptor */
#define STDERR_FILENO 2 /* standard error file descriptor */

The output of nslog is to stderr_ On fileno, we can use fprintf of C language output file in IOS to verify that:


NSLog(@"iOS NSLog");
fprintf (stderr, "%s\n", "fprintf log");

As fprintf does not call ASL interface internally like nslog, it only outputs information, and does not add date, process name, process ID, and does not automatically wrap lines.

ASL read log

The first thing we can think of is that since the logs are written to the syslog of the system, we can read these logs directly. The core code for reading logs from ASL is as follows:

#import <asl.h>
//Get the data we need from the log object aslmsg
+(instancetype)logMessageFromASLMessage:(aslmsg)aslMessage{
 SystemLogMessage *logMessage = [[SystemLogMessage alloc] init];
 const char *timestamp = asl_get(aslMessage, ASL_KEY_TIME);
 if (timestamp) {
  NSTimeInterval timeInterval = [@(timestamp) integerValue];
  const char *nanoseconds = asl_get(aslMessage, ASL_KEY_TIME_NSEC);
  if (nanoseconds) {
   timeInterval += [@(nanoseconds) doubleValue] / NSEC_PER_SEC;
  }
  logMessage.timeInterval = timeInterval;
  logMessage.date = [NSDate dateWithTimeIntervalSince1970:timeInterval];
 }
 const char *sender = asl_get(aslMessage, ASL_KEY_SENDER);
 if (sender) {
  logMessage.sender = @(sender);
 }
 const char *messageText = asl_get(aslMessage, ASL_KEY_MSG);
 if (messageText) {
  logMessage.messageText  [email protected] (message text); // the text content written by nslog
 }
 const char *messageID = asl_get(aslMessage, ASL_KEY_MSG_ID);
 if (messageID) {
  logMessage.messageID = [@(messageID) longLongValue];
 }
 return logMessage;
}
+ (NSMutableArray<SystemLogMessage *> *)allLogMessagesForCurrentProcess{
 asl_object_t query = asl_new(ASL_TYPE_QUERY);
 // Filter for messages from the current process. Note that this appears to happen by default on device, but is required in the simulator.
 NSString *pidString = [NSString stringWithFormat:@"%d", [[NSProcessInfo processInfo] processIdentifier]];
 asl_set_query(query, ASL_KEY_PID, [pidString UTF8String], ASL_QUERY_OP_EQUAL);
 aslresponse response = asl_search(NULL, query);
 aslmsg aslMessage = NULL;
 NSMutableArray *logMessages = [NSMutableArray array];
 while ((aslMessage = asl_next(response))) {
  [logMessages addObject:[SystemLogMessage logMessageFromASLMessage:aslMessage]];
 }
 asl_release(response);
 return logMessages;
}

The advantage of using the above method is that the output of Xcode console will not be affected, and the log can be read in a non intrusive way.

Nslog redirection

Another way is to redirect nslog so that nslog will not be written to the syslog of the system.

Dup2 redirection

Through redirection, you can directly intercept stdout, stderr and other standard output information, and then save it in the location you want to store, upload it to the server or display it to the view.

After that, the input side of the NSDU should be redirected to the pipe end, and then the pipe that needs to be written through the pipe should be redirected. Then monitor the read end of the pipe through nsfilehandle, and finally process the read-out information.

After that, if data is written through printf or nslog, it will be written to the write end of the pipe. At the same time, the pipe will directly transmit the data to the reader. Finally, the data will be extracted through the monitoring function of nsfilehandle.

The core code is as follows:

- (void)redirectStandardOutput{
 //Record standard output and error stream original file descriptors
 self.outFd = dup(STDOUT_FILENO);
 self.errFd = dup(STDERR_FILENO);
#if BETA_BUILD
 stdout->_flags = 10;
 NSPipe *outPipe = [NSPipe pipe];
 NSFileHandle *pipeOutHandle = [outPipe fileHandleForReading];
 dup2([[outPipe fileHandleForWriting] fileDescriptor], STDOUT_FILENO);
 [pipeOutHandle readInBackgroundAndNotify];
 stderr->_flags = 10;
 NSPipe *errPipe = [NSPipe pipe];
 NSFileHandle *pipeErrHandle = [errPipe fileHandleForReading];
 dup2([[errPipe fileHandleForWriting] fileDescriptor], STDERR_FILENO);
 [pipeErrHandle readInBackgroundAndNotify];
 [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(redirectOutNotificationHandle:) name:NSFileHandleReadCompletionNotification object:pipeOutHandle];
 [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(redirectErrNotificationHandle:) name:NSFileHandleReadCompletionNotification object:pipeErrHandle];
#endif
}
-(void)recoverStandardOutput{
#if BETA_BUILD
 dup2(self.outFd, STDOUT_FILENO);
 dup2(self.errFd, STDERR_FILENO);
 [[NSNotificationCenter defaultCenter] removeObserver:self];
#endif
}
//Nslog output after redirection
- (void)redirectOutNotificationHandle:(NSNotification *)nf{
#if BETA_BUILD
 NSData *data = [[nf userInfo] objectForKey:NSFileHandleNotificationDataItem];
 NSString *str = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
 //Your code here... Save the log and upload or display it
#endif
 [[nf object] readInBackgroundAndNotify];
}
//Error output after redirection
- (void)redirectErrNotificationHandle:(NSNotification *)nf{
#if BETA_BUILD
 NSData *data = [[nf userInfo] objectForKey:NSFileHandleNotificationDataItem];
 NSString *str = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
 //Your code here... Save the log and upload or display it
#endif
 [[nf object] readInBackgroundAndNotify];
}

Folder Redirection

Another way of redirection is to use the freepen function of C language to redirect the content written to stderr to the file we made. Once the above code is executed, the nslog after this will not be displayed on the console, but will be directly output in the specified file.

In the simulator, we can use the tail command (tail – F) of the terminal xxx.log )To view this file in real time, just as we see in the output window of Xcode, you can also use the grep command for real-time filtering and viewing, which is very convenient to quickly locate the log information we want in a large number of log information.


FILE * freopen ( const char * filename, const char * mode, FILE * stream );

The specific codes are as follows:


NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentsPath = [paths objectAtIndex:0];
NSString *loggingPath = [documentsPath stringByAppendingPathComponent:@"/xxx.log"];
//redirect NSLog
freopen([loggingPath cStringUsingEncoding:NSASCIIStringEncoding], "a+", stderr);

In this way, we can send the available log files to the server or share them through iTunes. However, due to the strict sandbox mechanism of IOS, we can’t know the original file path of stderr, and we can’t directly use the files outside the sandbox. Therefore, freeopen cannot be redirected back. We can only use DUP and dup2 mentioned in point 1.

//Redirection
int origin1 = dup(STDERR_FILENO);
FILE * myFile = freopen([loggingPath cStringUsingEncoding:NSASCIIStringEncoding], "a+", stderr);
//Resume redirection
dup2(origin1, STDERR_FILENO);

Dispatch source redirection using GCD

The specific codes are as follows:


- (dispatch_source_t)_startCapturingWritingToFD:(int)fd {
 int fildes[2];
 pipe(fildes); // [0] is read end of pipe while [1] is write end
 dup2(fildes[1], fd); // Duplicate write end of pipe "onto" fd (this closes fd)
 close(fildes[1]); // Close original write end of pipe
 fd = fildes[0]; // We can now monitor the read end of the pipe
 char* buffer = malloc(1024);
 NSMutableData* data = [[NSMutableData alloc] init];
 fcntl(fd, F_SETFL, O_NONBLOCK);
 dispatch_source_t source = dispatch_source_create(DISPATCH_SOURCE_TYPE_READ, fd, 0, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0));
 dispatch_source_set_cancel_handler(source, ^{
  free(buffer);
 });
 dispatch_source_set_event_handler(source, ^{
  @autoreleasepool {
   while (1) {
    ssize_t size = read(fd, buffer, 1024);
    if (size <= 0) {
     break;
    }
    [data appendBytes:buffer length:size];
    if (size < 1024) {
     break;
    }
   }
   NSString *aString = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
   //printf("aString = %s",[aString UTF8String]);
   //NSLog(@"aString = %@",aString);
   // Do something
  }
 });
 dispatch_resume(source);
 return source;
}

Log synchronization / upload

The redirected or stored data can be transmitted to the server or synchronized to the web page through the server, which makes it more convenient to see the data.

If you want to view the log in real time on the web, you can build a small HTTP web server in the app. Gcdwebserver is an open source project on GitHub. You can use this tool to open webserver service in app and use it in the same LAN http://localhost : 8080 to request the latest log.

The part of the upload server is very simple. It is OK to implement a simple network request. There is no description here.

In addition, in the actual project, you can set a switch to turn this redirection on or off. During debugging and testing, you can turn on the switch to view the current log of the program.

Through the above processing, the log can be easily obtained and viewed in the real machine test, which can save a lot of manpower and time cost.

summary

The above is the whole content of this article, I hope that the content of this article has a certain reference value for your study or work. If you have any questions, you can leave a message and exchange, thank you for your support to developeppaer.

Reference documents

  • IOS IO redirection
  • IOS log