background

Sentry is a platform for real-time event logging and aggregation. It focuses on error monitoring and extracting all the information it needs to deal with afterwards without relying on cumbersome user feedback. In China, such as Bugtags, Bugly and other APP crash acquisition platforms. The advantage of Sentry is that it supports server, Android, iOS, and the Web. And most importantly, it’s open source! It’s open source! It’s open source! (Three important things to say) All platform SDK and server side code is open source! Since our company APP wants to access this platform recently (actually, I want to use this platform for myself), I want to study their SDK to better enable customers to use our products.

Access to the use of

According to the official documentation, iOS access requires only two steps:

  1. Add the dependent
source 'https://github.com/CocoaPods/Specs.git'
platform :ios.'8.0'
use_frameworks!

target 'YourApp' do
    pod 'Sentry'.:git= >'https://github.com/getsentry/sentry-cocoa.git'.:tag= >'this'
end
Copy the code
  1. indidFinishLaunchingWithOptionsMethod
NSError *error = nil;
SentryClient *client = [[SentryClient alloc] initWithDsn:@"https://[email protected]/xxxx" didFailWithError:&error];
SentryClient.sharedClient = client;
[SentryClient.sharedClient startCrashHandlerWithError:&error];
if (nil! = error) {NSLog(@ "% @", error);
}
Copy the code

This way you can catch exceptions and report them to the server.

The source code parsing

Initialize the

We see that there is one key code in the initialization.

[SentryClient.sharedClient startCrashHandlerWithError:&error];
Copy the code

We startCrashHandlerWithError inside a function body

- (BOOL)startCrashHandlerWithError:(NSError *_Nullable *_Nullable)error {
    [SentryLog logWithMessage:@"SentryCrashHandler started" andLevel:kSentryLogLevelDebug];
    static dispatch_once_t onceToken = 0;
    dispatch_once(&onceToken, ^{
        installation = [[SentryInstallation alloc] init];
        [installation install];
        [installation sendAllReports];
    });
    return YES;
}
Copy the code

We see a dispatch_once that prevents multiple initializations. From here we open the door to a new world. The key method calls can be seen in the sequence diagram below

We can see that the final set of Monitors is initialized with a total of nine, defined by enumeration in the code, as follows:

typedef enum
{
    /* Captures and reports Mach exceptions. */
    SentryCrashMonitorTypeMachException      = 0x01./* Captures and reports POSIX signals. */
    SentryCrashMonitorTypeSignal             = 0x02./* Captures and reports C++ exceptions. * Note: This will slightly slow down exception processing. */
    SentryCrashMonitorTypeCPPException       = 0x04./* Captures and reports NSExceptions. */
    SentryCrashMonitorTypeNSException        = 0x08./* Detects and reports a deadlock in the main thread. */
    SentryCrashMonitorTypeMainThreadDeadlock = 0x10./* Accepts and reports user-generated exceptions. */
    SentryCrashMonitorTypeUserReported       = 0x20./* Keeps track of and injects system information. */
    SentryCrashMonitorTypeSystem             = 0x40./* Keeps track of and injects application state. */
    SentryCrashMonitorTypeApplicationState   = 0x80./* Keeps track of zombies, and injects the last zombie NSException. */
    SentryCrashMonitorTypeZombie             = 0x100,
} SentryCrashMonitorType;
Copy the code

According to my understanding, these monitors can be divided into two categories, as shown in the following figure (I present the categories in the form of file names to facilitate locating codes).



Among themCrashMonitorsA monitor that can capture a CrashContextMonitorsThis refers to monitoring for logging context injection into the log. Note, however, that not all of these Monitors will be initialized; Sentry will initialize different Monitors depending on the situation. Such as:

void sentrycrashcm_setActiveMonitors(SentryCrashMonitorType monitorTypes)
{
    if(sentrycrashdebug_isBeingTraced() && (monitorTypes & SentryCrashMonitorTypeDebuggerUnsafe))
    {
        static bool hasWarned = false;
        if(! hasWarned) { hasWarned =true;
            SentryCrashLOGBASIC_WARN(" ************************ Crash Handler Notice ************************");
            SentryCrashLOGBASIC_WARN(" * App is running in a debugger. Masking out unsafe monitors. *");
            SentryCrashLOGBASIC_WARN(" * This means that most crashes WILL NOT BE RECORDED while debugging! *");
            SentryCrashLOGBASIC_WARN("* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *"); } monitorTypes &= SentryCrashMonitorTypeDebuggerSafe; }... }Copy the code

Which will start in the case of the debug SentryCrashMonitorTypeDebuggerSafe series of monitoring, including SentryCrashMonitorTypeDebuggerSafe macros are defined as follows:

/** Monitors that are safe to enable in a debugger. */
#define SentryCrashMonitorTypeDebuggerSafe (SentryCrashMonitorTypeAll & (~SentryCrashMonitorTypeDebuggerUnsafe))
Copy the code

Monitors

Let’s take a look at how these Monitors catch exceptions and log information, one by one, in the order defined by Sentry’s enumeration. Tips: The following code may cause discomfort, if you do not have the patience to read multiple times.

1. SentryCrashMonitorTypeMachException

This is the Monitor that catches kernel exceptions, and the core code is as follows

static bool installExceptionHandler(a)
{
    SentryCrashLOG_DEBUG("Installing mach exception handler.");

    bool attributes_created = false;
    pthread_attr_t attr;

    kern_return_t kr;
    int error;

    const task_t thisTask = mach_task_self();
    exception_mask_t mask = EXC_MASK_BAD_ACCESS |
    EXC_MASK_BAD_INSTRUCTION |
    EXC_MASK_ARITHMETIC |
    EXC_MASK_SOFTWARE |
    EXC_MASK_BREAKPOINT;

    // Back up the existing exception receive port
    SentryCrashLOG_DEBUG("Backing up original exception ports.");
    kr = task_get_exception_ports(thisTask,
                                  mask,
                                  g_previousExceptionPorts.masks,
                                  &g_previousExceptionPorts.count,
                                  g_previousExceptionPorts.ports,
                                  g_previousExceptionPorts.behaviors,
                                  g_previousExceptionPorts.flavors);
    if(kr ! = KERN_SUCCESS) { SentryCrashLOG_ERROR("task_get_exception_ports: %s", mach_error_string(kr));
        goto failed;
    }

    if(g_exceptionPort == MACH_PORT_NULL)
    {
        // Assign a new port and receive permission
        SentryCrashLOG_DEBUG("Allocating new port with receive rights.");
        kr = mach_port_allocate(thisTask,
                                MACH_PORT_RIGHT_RECEIVE,
                                &g_exceptionPort);
        if(kr ! = KERN_SUCCESS) { SentryCrashLOG_ERROR("mach_port_allocate: %s", mach_error_string(kr));
            goto failed;
        }

        SentryCrashLOG_DEBUG("Adding send rights to port.");
        kr = mach_port_insert_right(thisTask,
                                    g_exceptionPort,
                                    g_exceptionPort,
                                    MACH_MSG_TYPE_MAKE_SEND);
        if(kr ! = KERN_SUCCESS) { SentryCrashLOG_ERROR("mach_port_insert_right: %s", mach_error_string(kr));
            gotofailed; }}// Set the new port to accept exceptions
    SentryCrashLOG_DEBUG("Installing port as exception handler.");
    kr = task_set_exception_ports(thisTask,
                                  mask,
                                  g_exceptionPort,
                                  EXCEPTION_DEFAULT,
                                  THREAD_STATE_NONE);
    if(kr ! = KERN_SUCCESS) { SentryCrashLOG_ERROR("task_set_exception_ports: %s", mach_error_string(kr));
        goto failed;
    }

    // Create a secondary exception thread
    SentryCrashLOG_DEBUG("Creating secondary exception thread (suspended).");
    pthread_attr_init(&attr);
    attributes_created = true;
    pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
    error = pthread_create(&g_secondaryPThread,
                           &attr,
                           &handleExceptions,
                           kThreadSecondary);
    if(error ! =0)
    {
        SentryCrashLOG_ERROR("pthread_create_suspended_np: %s", strerror(error));
        goto failed;
    }
    g_secondaryMachThread = pthread_mach_thread_np(g_secondaryPThread);
    sentrycrashmc_addReservedThread(g_secondaryMachThread);

    // Create the main exception thread
    SentryCrashLOG_DEBUG("Creating primary exception thread.");
    error = pthread_create(&g_primaryPThread,
                           &attr,
                           &handleExceptions,
                           kThreadPrimary);
    if(error ! =0)
    {
        SentryCrashLOG_ERROR("pthread_create: %s", strerror(error));
        goto failed;
    }
    pthread_attr_destroy(&attr);
    g_primaryMachThread = pthread_mach_thread_np(g_primaryPThread);
    sentrycrashmc_addReservedThread(g_primaryMachThread);

    SentryCrashLOG_DEBUG("Mach exception handler installed.");
    return true;

failed:
    SentryCrashLOG_DEBUG("Failed to install mach exception handler.");
    if(attributes_created)
    {
        pthread_attr_destroy(&attr);
    }
    uninstallExceptionHandler();
    return false;
}
Copy the code

Task_get_exception_ports, mach_port_allocate, task_set_EXCEPtion_ports and other kernel functions are unfamiliar to us. In fact, I have never seen them before. However, we looked at the official development documentation for Kernel Functions and found that these Functions were not documented. But don’t panic! I went back to Darwin XNU, the open source repository for XNU, and found descriptions of these functions. Also can refer to another website, is my Google out, convenient to read web.mit.edu/darwin/src/… If you’re interested, you can explore the functions of these kernels, and we’ll stop there.

2. SentryCrashMonitorTypeSignal

static bool installSignalHandler(a)
{
    SentryCrashLOG_DEBUG("Installing signal handler.");

#if SentryCrashCRASH_HAS_SIGNAL_STACK

    if(g_signalStack.ss_size == 0)
    {
        // Allocate memory for the new signal handler stack
        SentryCrashLOG_DEBUG("Allocating signal stack area.");
        g_signalStack.ss_size = SIGSTKSZ;
        g_signalStack.ss_sp = malloc(g_signalStack.ss_size);
    }

    // Replace the signal handler stack
    SentryCrashLOG_DEBUG("Setting signal stack area.");
    if(sigaltstack(&g_signalStack, NULL) != 0)
    {
        SentryCrashLOG_ERROR("signalstack: %s", strerror(errno));
        goto failed;
    }
#endif

    const int* fatalSignals = sentrycrashsignal_fatalSignals();
    int fatalSignalsCount = sentrycrashsignal_numFatalSignals();

    if(g_previousSignalHandlers == NULL)
    {
        // Allocate memory space to hold previous signal processing functions
        SentryCrashLOG_DEBUG("Allocating memory to store previous signal handlers.");
        g_previousSignalHandlers = malloc(sizeof(*g_previousSignalHandlers)
                                          * (unsigned)fatalSignalsCount);
    }

    struct sigaction action = {{0}};
    action.sa_flags = SA_SIGINFO | SA_ONSTACK;
#if SentryCrashCRASH_HOST_APPLE && defined(__LP64__)
    action.sa_flags |= SA_64REGSET;
#endif
    // Initialize the signal set to null
    sigemptyset(&action.sa_mask);
    // Set the signal exception handler
    action.sa_sigaction = &handleSignal;

    // Set the handlers for different exception signals one by one
    for(int i = 0; i < fatalSignalsCount; i++)
    {
        SentryCrashLOG_DEBUG("Assigning handler for signal %d", fatalSignals[i]);
        // If the setup fails, restore the previous processor field
        if(sigaction(fatalSignals[i], &action, &g_previousSignalHandlers[i]) ! =0)
        {
            char sigNameBuff[30];
            const char* sigName = sentrycrashsignal_signalName(fatalSignals[i]);
            if(sigName == NULL)
            {
                snprintf(sigNameBuff, sizeof(sigNameBuff), "%d", fatalSignals[i]);
                sigName = sigNameBuff;
            }
            SentryCrashLOG_ERROR("sigaction (%s): %s", sigName, strerror(errno));
            // Try to reverse the damage
            for(i--; i >=0; i--)
            {
                sigaction(fatalSignals[i], &g_previousSignalHandlers[i], NULL);
            }
            goto failed;
        }
    }
    SentryCrashLOG_DEBUG("Signal handlers installed.");
    return true;

failed:
    SentryCrashLOG_DEBUG("Failed to install signal handlers.");
    return false;
}
Copy the code

One of the core API functions is sigAction, here I also prepared a gnu official document for you www.gnu.org/software/li…

3. SentryCrashMonitorTypeCPPException

Monitor to catch c++ exceptions, with the core code below

static void setEnabled(bool isEnabled)
{
    if(isEnabled ! = g_isEnabled) { g_isEnabled = isEnabled;if(isEnabled)
        {
            initialize();

            sentrycrashid_generate(g_eventID);
            // Replace the exception handler with CPPExceptionTerminate
            g_originalTerminateHandler = std::set_terminate(CPPExceptionTerminate);
        }
        else
        {
            std::set_terminate(g_originalTerminateHandler); } g_captureNextStackTrace = isEnabled; }}Copy the code

STD: : set_terminate function description can see the c + + reference manual zh.cppreference.com/w/cpp/error…

4. SentryCrashMonitorTypeNSException

This we are more familiar with, used to capture the APP layer exception, the core code is as follows

static void setEnabled(bool isEnabled)
{
    if(isEnabled ! = g_isEnabled) { g_isEnabled = isEnabled;if(isEnabled)
        {
            // Back up existing exception handlers
            SentryCrashLOG_DEBUG(@"Backing up original handler.");
            g_previousUncaughtExceptionHandler = NSGetUncaughtExceptionHandler(a);// Set the new exception handler
            SentryCrashLOG_DEBUG(@"Setting new handler.");
            NSSetUncaughtExceptionHandler(&handleUncaughtException); SentryCrash.sharedInstance.uncaughtExceptionHandler = &handleUncaughtException; SentryCrash.sharedInstance.currentSnapshotUserReportedExceptionHandler = &handleCurrentSnapshotUserReportedException; }... }}Copy the code

Including the use of NSSetUncaughtExceptionHandler developer.apple.com/documentati can refer to an apple official document…

5. SentryCrashMonitorTypeMainThreadDeadlock

The main catch main thread blocking exception, the core code is as follows

- (void) watchdogPulse
{
    __block id blockSelf = self;
    self.awaitingResponse = YES;
    dispatch_async(dispatch_get_main_queue(), ^
                   {
                       [blockSelf watchdogAnswer];
                   });
}

- (void) watchdogAnswer
{
    self.awaitingResponse = NO;
}

- (void) runMonitor
{
    BOOL cancelled = NO;
    do
    {
        // Only do a watchdog check if the watchdog interval is > 0.
        // If the interval is <= 0, just idle until the user changes it.
        @autoreleasepool {
            NSTimeInterval sleepInterval = g_watchdogInterval;
            BOOL runWatchdogCheck = sleepInterval > 0;
            if(! runWatchdogCheck) { sleepInterval = kIdleInterval; } [NSThread sleepForTimeInterval:sleepInterval];
            cancelled = self.monitorThread.isCancelled;
            if(! cancelled && runWatchdogCheck) {if(self.awaitingResponse)
                {
                    [self handleDeadlock];
                }
                else{[selfwatchdogPulse]; }}}}while(! cancelled); }Copy the code

The main thread is considered blocked if the interval for executing blocks exceeds a set threshold.

6. SentryCrashMonitorTypeUserReported

Exceptions reported by users by calling the following API functions

[SentryClient.sharedClient reportUserException:<#(NSString *)name#> 
                                        reason:<#(NSString *)reason#> 
                                      language:<#(NSString *)language#> 
                                    lineOfCode:<#(NSString *)lineOfCode#> 
                                    stackTrace:<#(NSArray *)stackTrace#> 
                                 logAllThreads:<#(BOOL)logAllThreads#> 
                              terminateProgram:<#(BOOL)terminateProgram#>];
Copy the code

7. SentryCrashMonitorTypeSystem

The main record and injection system status, such as system version number, kernel version, whether emulator, etc., part of the code is as follows

static void initialize()
{
    static bool isInitialized = false;
    if(! isInitialized) { isInitialized =true; . g_systemData.kernelVersion = stringSysctl("kern.version");
        g_systemData.osVersion = stringSysctl("kern.osversion");
        g_systemData.isJailbroken = isJailbroken();
        g_systemData.bootTime = dateSysctl("kern.boottime");
        g_systemData.appStartTime = dateString(time(NULL));
        g_systemData.executablePath = cString(getExecutablePath());
        g_systemData.executableName = cString(infoDict[@"CFBundleExecutable"]);
        g_systemData.bundleID = cString(infoDict[@"CFBundleIdentifier"]);
        g_systemData.bundleName = cString(infoDict[@"CFBundleName"]);
        g_systemData.bundleVersion = cString(infoDict[@"CFBundleVersion"]);
        g_systemData.bundleShortVersion = cString(infoDict[@"CFBundleShortVersionString"]);
        g_systemData.appID = getAppUUID();
        g_systemData.cpuArchitecture = getCurrentCPUArch();
        g_systemData.cpuType = sentrycrashsysctl_int32ForName("hw.cputype");
        g_systemData.cpuSubType = sentrycrashsysctl_int32ForName("hw.cpusubtype");
        g_systemData.binaryCPUType = header->cputype;
        g_systemData.binaryCPUSubType = header->cpusubtype;
        g_systemData.timezone = cString([NSTimeZone localTimeZone].abbreviation);
        g_systemData.processName = cString([NSProcessInfo processInfo].processName);
        g_systemData.processID = [NSProcessInfo processInfo].processIdentifier;
        g_systemData.parentProcessID = getppid();
        g_systemData.deviceAppHash = getDeviceAndAppHash();
        g_systemData.buildType = getBuildType();
        g_systemData.storageSize = getStorageSize();
        g_systemData.memorySize = sentrycrashsysctl_uint64ForName("hw.memsize"); }}Copy the code

8. SentryCrashMonitorTypeApplicationState

Mainly record and inject the state of APP, such as startup time, whether in the foreground, etc., relatively simple, here will not be overstated.

9. SentryCrashMonitorTypeZombie

Trace and inject zombie object information, the core code is as follows

#define CREATE_ZOMBIE_HANDLER_INSTALLER(CLASS) \
static IMP g_originalDealloc_ ## CLASS; \
static void handleDealloc_ ## CLASS(id self, SEL _cmd) \
{ \
    handleDealloc(self); \
    typedef void (*fn)(id,SEL); \
    fn f = (fn)g_originalDealloc_ ## CLASS; \
    f(self, _cmd); \} \static void installDealloc_ ## CLASS() \
{ \
    Method method = class_getInstanceMethod(objc_getClass(#CLASS), sel_registerName("dealloc")); \
    g_originalDealloc_ ## CLASS = method_getImplementation(method); \
    method_setImplementation(method, (IMP)handleDealloc_ ## CLASS); \
}

CREATE_ZOMBIE_HANDLER_INSTALLER(NSObject)
CREATE_ZOMBIE_HANDLER_INSTALLER(NSProxy)

static void install()
{
    // Allocate Zombie cache space
    unsigned cacheSize = CACHE_SIZE;
    g_zombieHashMask = cacheSize - 1;
    g_zombieCache = calloc(cacheSize, sizeof(*g_zombieCache));
    if(g_zombieCache == NULL)
    {
        SentryCrashLOG_ERROR("Error: Could not allocate %u bytes of memory. SentryCrashZombie NOT installed!",
              cacheSize * sizeof(*g_zombieCache));
        return;
    }

    g_lastDeallocedException.class = objc_getClass("NSException");
    g_lastDeallocedException.address = NULL;
    g_lastDeallocedException.name[0] = 0;
    g_lastDeallocedException.reason[0] = 0;

    // Hook dealloc function
    installDealloc_NSObject();
    installDealloc_NSProxy();
}
Copy the code

The dealloc function of NSObject and NSProxy is Hook by Method Swizzling. I also prepared a popular science article about the principle of Method Swizzling iOS Black magic -Method Swizzling

conclusion

Now that you have an idea of how Sentry catches various Crash exception events, we’ll see how Sentry logs and sends exception events later.