/*
 * Author: Landon Fuller <landonf@plausiblelabs.com>
 *
 * Copyright (c) 2008-2009 Plausible Labs Cooperative, Inc.
 * All rights reserved.
 *
 * Permission is hereby granted, free of charge, to any person
 * obtaining a copy of this software and associated documentation
 * files (the "Software"), to deal in the Software without
 * restriction, including without limitation the rights to use,
 * copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the
 * Software is furnished to do so, subject to the following
 * conditions:
 *
 * The above copyright notice and this permission notice shall be
 * included in all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
 * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
 * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
 * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
 * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
 * OTHER DEALINGS IN THE SOFTWARE.
 */

#if __has_include(<CrashReporter/CrashReporter.h>)
#import <CrashReporter/CrashReporter.h>
#import <CrashReporter/PLCrashReporter.h>
#else
#import "CrashReporter.h"
#import "PLCrashReporter.h"
#endif

#import "PLCrashFeatureConfig.h"

#import "PLCrashHostInfo.h"

#import "PLCrashSignalHandler.h"
#import "PLCrashMachExceptionServer.h"

#import "PLCrashFeatureConfig.h"

#import "PLCrashAsync.h"
#import "PLCrashLogWriter.h"
#import "PLCrashFrameWalker.h"

#import "PLCrashAsyncMachExceptionInfo.h"

#import "PLCrashReporterNSError.h"

#import <fcntl.h>
#import <dlfcn.h>
#import <mach-o/dyld.h>

#import <stdatomic.h>

/** @internal
 * CrashReporter cache directory name. */
static NSString *PLCRASH_CACHE_DIR = @"com.plausiblelabs.crashreporter.data";

/** @internal
 * Crash Report file name. */
static NSString *PLCRASH_LIVE_CRASHREPORT = @"live_report.plcrash";

/** @internal
 * Directory containing crash reports queued for sending. */
static NSString *PLCRASH_QUEUED_DIR = @"queued_reports";

/** @internal
 * Maximum number of bytes that will be written to the crash report.
 * Used as a safety measure in case of implementation malfunction.
 *
 * We provide for a generous 256k here. Most crash reports
 * are approximately 7k, however, we've seen 97k reports
 * generated by a managed runtime that loads a pathologically
 * large number (~800) of shared libraries.
 */
#define MAX_REPORT_BYTES (256 * 1024)

/**
 * @internal
 * Fatal signals to be monitored.
 */
static int monitored_signals[] = {
    SIGABRT,
    SIGBUS,
    SIGFPE,
    SIGILL,
    SIGSEGV,
    SIGTRAP
};

/** @internal
 * number of signals in the fatal signals list */
static int monitored_signals_count = (sizeof(monitored_signals) / sizeof(monitored_signals[0]));

/**
 * @internal
 * Signal handler context
 */
typedef struct signal_handler_ctx {
    /** PLCrashLogWriter instance */
    plcrash_log_writer_t writer;

    /** Path to the output file */
    const char *path;

#if PLCRASH_FEATURE_MACH_EXCEPTIONS
    /* Previously registered Mach exception ports, if any. Will be left uninitialized if PLCrashReporterSignalHandlerTypeMach
     * is not enabled. */
    plcrash_mach_exception_port_set_t port_set;
#endif /* PLCRASH_FEATURE_MACH_EXCEPTIONS */
} plcrashreporter_handler_ctx_t;

/**
 * @internal
 *
 * Shared dyld image list.
 */
static plcrash_async_image_list_t shared_image_list;


/**
 * @internal
 * 
 * Signal handler context (singleton)
 */
static plcrashreporter_handler_ctx_t signal_handler_context;


/**
 * @internal
 * 
 * The optional user-supplied callbacks invoked after the crash report has been written.
 */
static PLCrashReporterCallbacks crashCallbacks = {
    .version = 0,
    .context = NULL,
    .handleSignal = NULL
};

/**
 * Write a fatal crash report.
 *
 * @param sigctx Fatal handler context.
 * @param crashed_thread The crashed thread.
 * @param thread_state The crashed thread's state.
 * @param siginfo The signal information.
 *
 * @return Returns PLCRASH_ESUCCESS on success, or an appropriate error value if the report could not be written.
 */
static plcrash_error_t plcrash_write_report (plcrashreporter_handler_ctx_t *sigctx, thread_t crashed_thread, plcrash_async_thread_state_t *thread_state, plcrash_log_signal_info_t *siginfo) {
    plcrash_async_file_t file;
    plcrash_error_t err;

    /* Open the output file */
    int fd = open(sigctx->path, O_RDWR|O_CREAT|O_TRUNC, 0644);
    if (fd < 0) {
        PLCF_DEBUG("Could not open the crashlog output file: %s", strerror(errno));
        return PLCRASH_EINTERNAL;
    }
    
    /* Initialize the output context */
    plcrash_async_file_init(&file, fd, MAX_REPORT_BYTES);
    
    /* Write the crash log using the already-initialized writer */
    err = plcrash_log_writer_write(&sigctx->writer, crashed_thread, &shared_image_list, &file, siginfo, thread_state);

    /* Close the writer; this may also fail (but shouldn't) */
    if (plcrash_log_writer_close(&sigctx->writer) != PLCRASH_ESUCCESS) {
        PLCF_DEBUG("Failed to close the log writer");
        plcrash_async_file_close(&file);
        return PLCRASH_EINTERNAL;
    }
    
    /* Finished */
    if (!plcrash_async_file_flush(&file)) {
        PLCF_DEBUG("Failed to flush output file");
        plcrash_async_file_close(&file);
        return PLCRASH_EINTERNAL;
    }
    
    if (!plcrash_async_file_close(&file)) {
        PLCF_DEBUG("Failed to close output file");
        return PLCRASH_EINTERNAL;
    }

    return err;
}

/**
 * @internal
 *
 * Signal handler callback.
 */
static bool signal_handler_callback (int signal, siginfo_t *info, pl_ucontext_t *uap, void *context, PLCrashSignalHandlerCallback *next) {
    plcrashreporter_handler_ctx_t *sigctx = context;
    plcrash_async_thread_state_t thread_state;
    plcrash_log_signal_info_t signal_info;
    plcrash_log_bsd_signal_info_t bsd_signal_info;
    
    /* Remove all signal handlers -- if the crash reporting code fails, the default terminate
     * action will occur.
     *
     * NOTE: SA_RESETHAND breaks SA_SIGINFO on ARM, so we reset the handlers manually.
     * http://openradar.appspot.com/11839803
     *
     * TODO: When forwarding signals (eg, to Mono's runtime), resetting the signal handlers
     * could result in incorrect runtime behavior; we should revisit resetting the
     * signal handlers once we address double-fault handling.
     */
    for (int i = 0; i < monitored_signals_count; i++) {
        struct sigaction sa;
        
        memset(&sa, 0, sizeof(sa));
        sa.sa_handler = SIG_DFL;
        sigemptyset(&sa.sa_mask);
        
        sigaction(monitored_signals[i], &sa, NULL);
    }

    /* Extract the thread state */
    plcrash_async_thread_state_mcontext_init(&thread_state, uap->uc_mcontext);
    
    /* Set up the BSD signal info */
    bsd_signal_info.signo = info->si_signo;
    bsd_signal_info.code = info->si_code;
    bsd_signal_info.address = info->si_addr;
    
    signal_info.bsd_info = &bsd_signal_info;
    signal_info.mach_info = NULL;

    /* Write the report */
    if (plcrash_write_report(sigctx, pl_mach_thread_self(), &thread_state, &signal_info) != PLCRASH_ESUCCESS)
        return false;

    /* Call any post-crash callback */
    if (crashCallbacks.handleSignal != NULL)
        crashCallbacks.handleSignal(info, uap, crashCallbacks.context);
    
    return false;
}

#if PLCRASH_FEATURE_MACH_EXCEPTIONS
/* State and callback used to generate thread state for the calling mach thread. */
struct mach_exception_callback_live_cb_ctx {
    plcrashreporter_handler_ctx_t *sigctx;
    thread_t crashed_thread;
    plcrash_log_signal_info_t *siginfo;
};

static plcrash_error_t mach_exception_callback_live_cb (plcrash_async_thread_state_t *state, void *ctx) {
    struct mach_exception_callback_live_cb_ctx *plcr_ctx = ctx;
    return plcrash_write_report(plcr_ctx->sigctx, plcr_ctx->crashed_thread, state, plcr_ctx->siginfo);
}

static kern_return_t mach_exception_callback (task_t task, thread_t thread, exception_type_t exception_type, mach_exception_data_t code, mach_msg_type_number_t code_count, void *context) {
    plcrashreporter_handler_ctx_t *sigctx = context;
    plcrash_log_signal_info_t signal_info;
    plcrash_log_bsd_signal_info_t bsd_signal_info;
    plcrash_log_mach_signal_info_t mach_signal_info;
    plcrash_error_t err;

    /* Let any other registered server attempt to handle the exception */
    if (PLCrashMachExceptionForward(task, thread, exception_type, code, code_count, &sigctx->port_set) == KERN_SUCCESS)
        return KERN_SUCCESS;
    
    /* Set up the BSD signal info */
    siginfo_t si;
    if (!plcrash_async_mach_exception_get_siginfo(exception_type, code, code_count, CPU_TYPE_ANY, &si)) {
        PLCF_DEBUG("Unexpected error mapping Mach exception to a POSIX signal");
        return KERN_FAILURE;
    }

    bsd_signal_info.signo = si.si_signo;
    bsd_signal_info.code = si.si_code;
    bsd_signal_info.address = si.si_addr;

    signal_info.bsd_info = &bsd_signal_info;
    
    /* Set up the Mach signal info */
    mach_signal_info.type = exception_type;
    mach_signal_info.code = code;
    mach_signal_info.code_count = code_count;
    signal_info.mach_info = &mach_signal_info;
    
    /* Write the report */
    struct mach_exception_callback_live_cb_ctx live_ctx = {
        .sigctx = sigctx,
        .crashed_thread = thread,
        .siginfo = &signal_info
    };
    if ((err = plcrash_async_thread_state_current(mach_exception_callback_live_cb, &live_ctx)) != PLCRASH_ESUCCESS) {
        PLCF_DEBUG("Failed to write live report: %d", err);
        return KERN_FAILURE;
    }

    /* Call any post-crash callback */
    if (crashCallbacks.handleSignal != NULL) {
        /*
         * The legacy signal-based callback assumes the availability of a ucontext_t; we mock
         * an empty value here for the purpose of maintaining backwards compatibility. This behavior
         * is defined in the PLCrashReporterCallbacks API documentation.
         */
        ucontext_t uctx;
        _STRUCT_MCONTEXT mctx;
        
        /* Populate the mctx */
        plcrash_async_memset(&mctx, 0, sizeof(mctx));

        /* Configure the ucontext */
        plcrash_async_memset(&uctx, 0, sizeof(uctx));
        uctx.uc_mcsize = sizeof(mctx);
        uctx.uc_mcontext = &mctx;
    
        crashCallbacks.handleSignal(&si, &uctx, crashCallbacks.context);
    }

    return KERN_FAILURE;
}
#endif /* PLCRASH_FEATURE_MACH_EXCEPTIONS */

/**
 * @internal
 * dyld image add notification callback.
 */
static void image_add_callback (const struct mach_header *mh, intptr_t vmaddr_slide) {
    Dl_info info;
    
    /* Look up the image info */
    if (dladdr(mh, &info) == 0) {
        PLCR_LOG("%s: dladdr(%p, ...) failed", __FUNCTION__, mh);
        return;
    }

    /* Register the image */
    plcrash_nasync_image_list_append(&shared_image_list, (pl_vm_address_t) mh, info.dli_fname);
}

/**
 * @internal
 * dyld image remove notification callback.
 */
static void image_remove_callback (const struct mach_header *mh, intptr_t vmaddr_slide) {
    plcrash_nasync_image_list_remove(&shared_image_list, (uintptr_t) mh);
}


/**
 * @internal
 *
 * Uncaught exception handler. Sets the plcrash_log_writer_t's uncaught exception
 * field, and then triggers a SIGTRAP (synchronous exception) to cause a normal
 * exception dump.
 */
static void uncaught_exception_handler (NSException *exception) {
    /**
     * It is possible that another crash may occur between setting the uncaught
     * exception field, and triggering the signal handler.
     */
    static atomic_bool exception_is_handled = false;
    bool expected = false;
    if (!atomic_compare_exchange_strong(&exception_is_handled, &expected, true)) {
        return;
    }
    
    /* Set the uncaught exception */
    plcrash_log_writer_set_exception(&signal_handler_context.writer, exception);

    /* Synchronously trigger the crash handler */
    abort();
}


@interface PLCrashReporter (PrivateMethods)

- (id) initWithBundle: (NSBundle *) bundle configuration: (PLCrashReporterConfig *) configuration;
- (id) initWithApplicationIdentifier: (NSString *) applicationIdentifier appVersion: (NSString *) applicationVersion appMarketingVersion: (NSString *) applicationMarketingVersion configuration: (PLCrashReporterConfig *) configuration;

#if PLCRASH_FEATURE_MACH_EXCEPTIONS
- (PLCrashMachExceptionServer *) enableMachExceptionServerWithPreviousPortSet: (__strong PLCrashMachExceptionPortSet **) previousPortSet
                                                                     callback: (PLCrashMachExceptionHandlerCallback) callback
                                                                      context: (void *) context
                                                                        error: (NSError **) outError;
#endif
- (plcrash_async_symbol_strategy_t) mapToAsyncSymbolicationStrategy: (PLCrashReporterSymbolicationStrategy) strategy;

- (BOOL) populateCrashReportDirectoryAndReturnError: (NSError **) outError;
- (NSString *) crashReportDirectory;
- (NSString *) queuedCrashReportDirectory;

@end


/**
 * Crash Reporter.
 *
 * A PLCrashReporter instance manages process-wide handling of crashes.
 */
@implementation PLCrashReporter

+ (void) initialize {
    if (![[self class] isEqual: [PLCrashReporter class]])
        return;

    /* Enable dyld image monitoring */
    plcrash_nasync_image_list_init(&shared_image_list, mach_task_self());
    _dyld_register_func_for_add_image(image_add_callback);
    _dyld_register_func_for_remove_image(image_remove_callback);
}


/* (Deprecated) Crash reporter singleton. */
static PLCrashReporter *sharedReporter = nil;

/**
 * Return the default crash reporter instance. The returned instance will be configured
 * appropriately for release deployment.
 *
 * @deprecated As of PLCrashReporter 1.2, the default reporter instance has been deprecated, and API
 * clients should initialize a crash reporter instance directly.
 */
+ (PLCrashReporter *) sharedReporter {
    static dispatch_once_t onceLock;
    dispatch_once(&onceLock, ^{
        if (sharedReporter == nil)
            sharedReporter = [[PLCrashReporter alloc] initWithBundle: [NSBundle mainBundle] configuration: [PLCrashReporterConfig defaultConfiguration]];
    });
    return sharedReporter;
}

/**
 * Initialize a new PLCrashReporter instance with a default configuration appropraite
 * for release deployment.
 */
- (instancetype) init {
    return [self initWithConfiguration: [PLCrashReporterConfig defaultConfiguration]];
}

/**
 * Initialize a new PLCrashReporter instance with the given configuration.
 *
 * @param configuration The configuration to be used by this reporter instance.
 */
- (instancetype) initWithConfiguration: (PLCrashReporterConfig *) configuration {
    return [self initWithBundle: [NSBundle mainBundle] configuration: configuration];
}

/**
 * Returns YES if the application has previously crashed and
 * an pending crash report is available.
 */
- (BOOL) hasPendingCrashReport {
    /* Check for a live crash report file */
    return [[NSFileManager defaultManager] fileExistsAtPath: [self crashReportPath]];
}


/**
 * If an application has a pending crash report, this method returns the crash
 * report data.
 *
 * You may use this to submit the report to your own HTTP server, over e-mail, or even parse and
 * introspect the report locally using the PLCrashReport API.
 *
 * @return Returns nil if the crash report data could not be loaded.
 */
- (NSData *) loadPendingCrashReportData {
    return [self loadPendingCrashReportDataAndReturnError: NULL];
}


/**
 * If an application has a pending crash report, this method returns the crash
 * report data.
 *
 * You may use this to submit the report to your own HTTP server, over e-mail, or even parse and
 * introspect the report locally using the PLCrashReport API.
 
 * @param outError A pointer to an NSError object variable. If an error occurs, this pointer
 * will contain an error object indicating why the pending crash report could not be
 * loaded. If no error occurs, this parameter will be left unmodified. You may specify
 * nil for this parameter, and no error information will be provided.
 *
 * @return Returns nil if the crash report data could not be loaded.
 */
- (NSData *) loadPendingCrashReportDataAndReturnError: (NSError **) outError {
    /* Load the (memory mapped) data */
    return [NSData dataWithContentsOfFile: [self crashReportPath] options: NSMappedRead error: outError];
}


/**
 * Purge a pending crash report.
 *
 * @return Returns YES on success, or NO on error.
 */
- (BOOL) purgePendingCrashReport {
    return [self purgePendingCrashReportAndReturnError: NULL];
}


/**
 * Purge a pending crash report.
 *
 * @return Returns YES on success, or NO on error.
 */
- (BOOL) purgePendingCrashReportAndReturnError: (NSError **) outError {
    return [[NSFileManager defaultManager] removeItemAtPath: [self crashReportPath] error: outError];
}


/**
 * Enable the crash reporter. Once called, all application crashes will
 * result in a crash report being written prior to application exit.
 *
 * @return Returns YES on success, or NO if the crash reporter could
 * not be enabled.
 *
 * @par Registering Multiple Reporters
 *
 * Only one PLCrashReporter instance may be enabled in a process; attempting to enable an additional instance
 * will return NO, and the reporter will not be enabled. This restriction may be removed in a future release.
 */
- (BOOL) enableCrashReporter {
    return [self enableCrashReporterAndReturnError: nil];
}



/**
 * Enable the crash reporter. Once called, all application crashes will
 * result in a crash report being written prior to application exit.
 *
 * This method must only be invoked once. Further invocations will throw
 * a PLCrashReporterException.
 *
 * @param outError A pointer to an NSError object variable. If an error occurs, this pointer
 * will contain an error in the PLCrashReporterErrorDomain indicating why the Crash Reporter
 * could not be enabled. If no error occurs, this parameter will be left unmodified. You may
 * specify nil for this parameter, and no error information will be provided.
 *
 * @return Returns YES on success, or NO if the crash reporter could
 * not be enabled.
 *
 * @par Registering Multiple Reporters
 *
 * Only one PLCrashReporter instance may be enabled in a process; attempting to enable an additional instance
 * will return NO and a PLCrashReporterErrorResourceBusy error, and the reporter will not be enabled.
 * This restriction may be removed in a future release.
 */
- (BOOL) enableCrashReporterAndReturnError: (NSError **) outError {
    /* Prevent enabling more than one crash reporter, process wide. We can not support multiple chained reporters
     * due to the use of NSUncaughtExceptionHandler (it doesn't support chaining or assocation of context with the callbacks), as
     * well as our legacy approach of deregistering any signal handlers upon the first signal. Once PLCrashUncaughtExceptionHandler is
     * implemented, and we support double-fault handling without resetting the signal handlers, we can support chaining of multiple
     * crash reporters. */
    {
        static BOOL enforceOne = NO;
        pthread_mutex_t enforceOneLock = PTHREAD_MUTEX_INITIALIZER;
        pthread_mutex_lock(&enforceOneLock); {
            if (enforceOne) {
                pthread_mutex_unlock(&enforceOneLock);
                plcrash_populate_error(outError, PLCrashReporterErrorResourceBusy, @"A PLCrashReporter instance has already been enabled", nil);
                return NO;
            }
            enforceOne = YES;
        } pthread_mutex_unlock(&enforceOneLock);
    }

    /* Check for programmer error */
    if (_enabled)
        [NSException raise: PLCrashReporterException format: @"The crash reporter has already been enabled"];

    /* Create the directory tree */
    if (![self populateCrashReportDirectoryAndReturnError: outError])
        return NO;

    /* Set up the signal handler context */
    signal_handler_context.path = strdup([[self crashReportPath] UTF8String]); // NOTE: would leak if this were not a singleton struct
    assert(_applicationIdentifier != nil);
    assert(_applicationVersion != nil);
    plcrash_log_writer_init(&signal_handler_context.writer, _applicationIdentifier, _applicationVersion, _applicationMarketingVersion, [self mapToAsyncSymbolicationStrategy: _config.symbolicationStrategy], false);

    /* Set custom data, if already set before enabling */
    if (self.customData != nil) {
        plcrash_log_writer_set_custom_data(&signal_handler_context.writer, self.customData);
    }

    /* Enable the signal handler */
    switch (_config.signalHandlerType) {
        case PLCrashReporterSignalHandlerTypeBSD:
            for (size_t i = 0; i < monitored_signals_count; i++) {
                if (![[PLCrashSignalHandler sharedHandler] registerHandlerForSignal: monitored_signals[i] callback: &signal_handler_callback context: &signal_handler_context error: outError])
                    return NO;
            }
            break;

#if PLCRASH_FEATURE_MACH_EXCEPTIONS
        case PLCrashReporterSignalHandlerTypeMach: {
            /* We still need to use signal handlers to catch SIGABRT in-process. The kernel sends an EXC_CRASH mach exception
             * to denote SIGABRT termination. In that case, catching the Mach exception in-process leads to process deadlock
             * in an uninterruptable wait. Thus, we fall back on BSD signal handlers for SIGABRT, and do not register for
             * EXC_CRASH. */
            if (![[PLCrashSignalHandler sharedHandler] registerHandlerForSignal: SIGABRT callback: &signal_handler_callback context: &signal_handler_context error: outError])
                return NO;
            
            /* Enable the server. */
            _machServer = [self enableMachExceptionServerWithPreviousPortSet:  &_previousMachPorts
                                                                    callback: &mach_exception_callback
                                                                     context: &signal_handler_context
                                                                       error: outError];
            if (_machServer == nil)
                return NO;
            
            /*
             * MEMORY WARNING: To ensure that our instance survives for the lifetime of the callback registration,
             * we keep a reference on self. This is necessary to ensure that the Mach exception server instance and previous port set
             * survive for the lifetime of the callback. Since there's currently no support for *deregistering* a crash reporter,
             * this simply results in the reporter living forever.
             */
            CFBridgingRetain(self);
            
            /*
             * Save the previous ports. There's a race condition here, in that an exception that is delivered before (or during)
             * setting the previous port values will see a fully and/or partially configured port set. This could be an issue
             * when interoperating with managed runtimes, where NULL dereferences may trigger exception handling
             * in a common runtime case.
             *
             * TODO: Investigate use of (async-safe) locking to close the window in which an exception would not be safely forwarded.
             * This issue also exists (and is noted with a TODO) in PLCrashSignalHandler.
             */
            signal_handler_context.port_set = [_previousMachPorts asyncSafeRepresentation];
            break;
        }
#endif /* PLCRASH_FEATURE_MACH_EXCEPTIONS */
    }

    /* Set the uncaught exception handler */
    if(_config.shouldRegisterUncaughtExceptionHandler) {
      NSSetUncaughtExceptionHandler(&uncaught_exception_handler);
    }
  
    /* Success */
    _enabled = YES;
    return YES;
}

/**
 * Generate a live crash report for a given @a thread, without triggering an actual crash condition.
 * This may be used to log current process state without actually crashing. The crash report data will be
 * returned on success.
 *
 * @param thread The thread which will be marked as the failing thread in the generated report.
 *
 * @return Returns nil if the crash report data could not be generated.
 *
 * @sa PLCrashReporter::generateLiveReportWithMachThread:error:
 */
- (NSData *) generateLiveReportWithThread: (thread_t) thread {
    return [self generateLiveReportWithThread: thread error: NULL];
}


/* State and callback used by -generateLiveReportWithThread */
struct plcr_live_report_context {
    plcrash_log_writer_t *writer;
    plcrash_async_file_t *file;
    plcrash_log_signal_info_t *info;
};
static plcrash_error_t plcr_live_report_callback (plcrash_async_thread_state_t *state, void *ctx) {
    struct plcr_live_report_context *plcr_ctx = ctx;
    return plcrash_log_writer_write(plcr_ctx->writer, pl_mach_thread_self(), &shared_image_list, plcr_ctx->file, plcr_ctx->info, state);
}


/**
 * Generate a live crash report for a given @a thread, without triggering an actual crash condition.
 * This may be used to log current process state without actually crashing. The crash report data will be
 * returned on success.
 *
 * @param thread The thread which will be marked as the failing thread in the generated report.
 * @param outError A pointer to an NSError object variable. If an error occurs, this pointer
 * will contain an error object indicating why the crash report could not be generated or loaded. If no
 * error occurs, this parameter will be left unmodified. You may specify nil for this parameter, and no
 * error information will be provided.
 *
 * @return Returns nil if the crash report data could not be loaded.
 *
 * @todo Implement in-memory, rather than requiring writing of the report to disk.
 */
- (NSData *) generateLiveReportWithThread: (thread_t) thread error: (NSError **) outError {
    plcrash_log_writer_t writer;
    plcrash_async_file_t file;
    plcrash_error_t err;

    /* Open the output file */
    NSString *templateStr = [NSTemporaryDirectory() stringByAppendingPathComponent: @"live_crash_report.XXXXXX"];
    char *path = strdup([templateStr fileSystemRepresentation]);
    
    int fd = mkstemp(path);
    if (fd < 0) {
        plcrash_populate_posix_error(outError, errno, NSLocalizedString(@"Failed to create temporary path", @"Error opening temporary output path"));
        free(path);

        return nil;
    }

    /* Initialize the output context */
    plcrash_log_writer_init(&writer, _applicationIdentifier, _applicationVersion, _applicationMarketingVersion, [self mapToAsyncSymbolicationStrategy: _config.symbolicationStrategy], true);
    plcrash_async_file_init(&file, fd, MAX_REPORT_BYTES);

    /* Set custom data, if already set before enabling */
    if (self.customData != nil) {
        plcrash_log_writer_set_custom_data(&writer, self.customData);
    }
    
    /* Mock up a SIGTRAP-based signal info */
    plcrash_log_bsd_signal_info_t bsd_signal_info;
    plcrash_log_signal_info_t signal_info;
    bsd_signal_info.signo = SIGTRAP;
    bsd_signal_info.code = TRAP_TRACE;
    bsd_signal_info.address = __builtin_return_address(0);

    signal_info.bsd_info = &bsd_signal_info;
    signal_info.mach_info = NULL;
    
    /* Write the crash log using the already-initialized writer */
    if (thread == pl_mach_thread_self()) {
        struct plcr_live_report_context ctx = {
            .writer = &writer,
            .file = &file,
            .info = &signal_info
        };
        err = plcrash_async_thread_state_current(plcr_live_report_callback, &ctx);
    } else {
        err = plcrash_log_writer_write(&writer, thread, &shared_image_list, &file, &signal_info, NULL);
    }
    plcrash_log_writer_close(&writer);

    /* Flush the data */
    plcrash_async_file_flush(&file);
    plcrash_async_file_close(&file);

    /* Check for write failure */
    NSData *data;
    if (err != PLCRASH_ESUCCESS) {
        PLCR_LOG("Write failed with error %s", plcrash_async_strerror(err));
        plcrash_populate_error(outError, PLCrashReporterErrorUnknown, @"Failed to write the crash report to disk", nil);
        data = nil;
        goto cleanup;
    }

    data = [NSData dataWithContentsOfFile:[NSString stringWithUTF8String: path]
                                  options:NSDataReadingMappedAlways | NSDataReadingUncached
                                    error:outError];
    if (data == nil) {
        /* This should only happen if our data is deleted out from under us */
        plcrash_populate_error(outError, PLCrashReporterErrorUnknown, NSLocalizedString(@"Unable to open live crash report for reading", nil), nil);
        goto cleanup;
    }

cleanup:
    /* Finished -- clean up. */
    plcrash_log_writer_free(&writer);

    if (unlink(path) != 0) {
        /* This shouldn't fail, but if it does, there's no use in returning nil */
        PLCR_LOG("Failure occured deleting live crash report: %s", strerror(errno));
    }

    free(path);
    return data;
}


/**
 * Generate a live crash report, without triggering an actual crash condition. This may be used to log
 * current process state without actually crashing. The crash report data will be returned on
 * success.
 * 
 * @return Returns nil if the crash report data could not be loaded.
 */
- (NSData *) generateLiveReport {
    return [self generateLiveReportAndReturnError: NULL];
}


/**
 * Generate a live crash report for the current thread, without triggering an actual crash condition.
 * This may be used to log current process state without actually crashing. The crash report data will be
 * returned on success.
 *
 * @param outError A pointer to an NSError object variable. If an error occurs, this pointer
 * will contain an error object indicating why the pending crash report could not be
 * generated or loaded. If no error occurs, this parameter will be left unmodified. You may specify
 * nil for this parameter, and no error information will be provided.
 * 
 * @return Returns nil if the crash report data could not be loaded.
 */
- (NSData *) generateLiveReportAndReturnError: (NSError **) outError {
    return [self generateLiveReportWithThread: pl_mach_thread_self() error: outError];
}


/**
 * Set the callbacks that will be executed by the receiver after a crash has occured and been recorded by PLCrashReporter.
 *
 * @param callbacks A pointer to an initialized PLCrashReporterCallbacks structure.
 *
 * @note This method must be called prior to PLCrashReporter::enableCrashReporter or
 * PLCrashReporter::enableCrashReporterAndReturnError:
 *
 * @sa The @ref async_safety documentation.
 */
- (void) setCrashCallbacks: (PLCrashReporterCallbacks *) callbacks {
    /* Check for programmer error; this should not be called after the signal handler is enabled as to ensure that
     * the signal handler can never fire with a partially initialized callback structure. */
    if (_enabled)
        [NSException raise: PLCrashReporterException format: @"The crash reporter has already been enabled"];

    assert(callbacks->version == 0);

    /* Re-initialize our internal callback structure */
    crashCallbacks.version = 0;

    /* Re-configure the saved callbacks */
    crashCallbacks.context = callbacks->context;
    crashCallbacks.handleSignal = callbacks->handleSignal;
}

/**
 * Set the custom data that will be saved in the crash report along the rest of information,
 * It deletes any previous custom data configured.
 *
 * @param customData A string with the custom data to save.
 */
- (void) setCustomData: (NSData *) customData {
    _customData = customData;
    plcrash_log_writer_set_custom_data(&signal_handler_context.writer, customData);
}

- (NSString *) crashReportPath {
    return [[self crashReportDirectory] stringByAppendingPathComponent: PLCRASH_LIVE_CRASHREPORT];
}

@end

/**
 * @internal
 *
 * Private Methods
 */
@implementation PLCrashReporter (PrivateMethods)

/**
 * @internal
 *
 * This is the designated initializer, but it is not intended
 * to be called externally.
 *
 * @param applicationIdentifier The application identifier to be included in crash reports.
 * @param applicationVersion The application version number to be included in crash reports.
 * @param applicationMarketingVersion The application marketing version number to be included in crash reports.
 * @param configuration The PLCrashReporter configuration.
 *
 * @todo The appId and version values should be fetched from the PLCrashReporterConfig, once the API
 * has been extended to allow supplying these values.
 */
- (id) initWithApplicationIdentifier: (NSString *) applicationIdentifier appVersion: (NSString *) applicationVersion appMarketingVersion: (NSString *) applicationMarketingVersion configuration: (PLCrashReporterConfig *) configuration {
    /* Initialize our superclass */
    if ((self = [super init]) == nil)
        return nil;

    /* Save the configuration */
    _config = configuration;
    _applicationIdentifier = applicationIdentifier;
    _applicationVersion = applicationVersion;
    _applicationMarketingVersion = applicationMarketingVersion;
    
    /* No occurances of '/' should ever be in a bundle ID, but just to be safe, we escape them */
    NSString *appIdPath = [applicationIdentifier stringByReplacingOccurrencesOfString: @"/" withString: @"_"];
    
    NSString *basePath = _config.basePath;
    if (basePath == nil) {
        NSArray *paths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES);
        basePath = [paths objectAtIndex: 0];
    }
    _crashReportDirectory = [[basePath stringByAppendingPathComponent: PLCRASH_CACHE_DIR] stringByAppendingPathComponent: appIdPath];
    return self;
}


/**
 * @internal
 *
 * Derive the bundle identifier and version from @a bundle.
 *
 * @param bundle The application's main bundle.
 * @param configuration The PLCrashReporter configuration to use for this instance.
 */
- (id) initWithBundle: (NSBundle *) bundle configuration: (PLCrashReporterConfig *) configuration {
    NSString *bundleIdentifier = [bundle bundleIdentifier];
    NSString *bundleVersion = [[bundle infoDictionary] objectForKey: (NSString *) kCFBundleVersionKey];
    NSString *bundleMarketingVersion = [[bundle infoDictionary] objectForKey: @"CFBundleShortVersionString"];
    
    /* Verify that the identifier is available */
    if (bundleIdentifier == nil) {
        const char *progname = getprogname();
        if (progname == NULL) {
            [NSException raise: PLCrashReporterException format: @"Can not determine process identifier or process name"];
            return nil;
        }

        PLCR_LOG("Warning -- bundle identifier, using process name %s", progname);
        bundleIdentifier = [NSString stringWithUTF8String: progname];
    }

    /* Verify that the version is available */
    if (bundleVersion == nil) {
        PLCR_LOG("Warning -- bundle version unavailable");
        bundleVersion = @"";
    }

    return [self initWithApplicationIdentifier: bundleIdentifier appVersion: bundleVersion appMarketingVersion:bundleMarketingVersion configuration: configuration];
}

#if PLCRASH_FEATURE_MACH_EXCEPTIONS

/**
 * Create, register, and return a Mach exception server.
 *
 * @param[out] previousPortSet The previously registered Mach exception ports.
 * @param context The context to be provided to the callback.
 * @param outError A pointer to an NSError object variable. If an error occurs, this pointer
 * will contain an error in the PLCrashReporterErrorDomain indicating why the Crash Reporter
 * could not be enabled. If no error occurs, this parameter will be left unmodified. You may
 * specify nil for this parameter, and no error information will be provided.
 */
- (PLCrashMachExceptionServer *) enableMachExceptionServerWithPreviousPortSet: (__strong PLCrashMachExceptionPortSet **) previousPortSet
                                                                     callback: (PLCrashMachExceptionHandlerCallback) callback
                                                                      context: (void *) context
                                                                        error: (NSError **) outError
{
    /* Determine the target exception type mask. Note that unlike some other Mach exception-based
     * crash reporting implementations, we do not monitor EXC_RESOURCE:
     *
     * EXC_RESOURCE wasn't added until iOS 5.1 and Mac OS X 10.8, and is used for
     * kernel-based thread resource constraints on a per-thread/per-task basis. XNU
     * supports either pausing threads that exceed the defined constraints (via the private
     * ledger kernel APIs), or issueing a Mach exception that can be used to monitor the
     * constraints.
     *
     * The EXC_RESOURCE resouce exception is used, for example, to implement the
     * private posix_spawnattr_setcpumonitor() API, which allows for monitoring CPU utilization
     * by observing issued EXC_RESOURCE exceptions. This appears to be used by launchd.
     *
     * Either way, we're uninterested in EXC_RESOURCE; the xnu ux_exception() handler should not deliver
     * a signal for the exception and should return KERN_SUCCESS, letting exception_triage()
     * consider it as handled.
     */
    exception_mask_t exc_mask = EXC_MASK_BAD_ACCESS |       /* Memory access fail */
                                EXC_MASK_BAD_INSTRUCTION |  /* Illegal instruction */
                                EXC_MASK_ARITHMETIC |       /* Arithmetic exception (eg, divide by zero) */
                                EXC_MASK_SOFTWARE |         /* Software exception (eg, as triggered by x86's bound instruction) */
                                EXC_MASK_BREAKPOINT;        /* Trace or breakpoint */
    
    /* EXC_GUARD was added in xnu 13.x (iOS 6.0, Mac OS X 10.9) */
#ifdef EXC_MASK_GUARD
    PLCrashHostInfo *hinfo = [PLCrashHostInfo currentHostInfo];
    
    if (hinfo != nil && hinfo.darwinVersion.major >= 13)
        exc_mask |= EXC_MASK_GUARD; /* Process accessed a guarded file descriptor. See also: https://devforums.apple.com/message/713907#713907 */
#endif
    
    /* Create the server */
    NSError *osError;
    PLCrashMachExceptionServer *server = [[PLCrashMachExceptionServer alloc] initWithCallBack: callback context: context error: &osError];
    if (server == nil) {
        plcrash_populate_error(outError, PLCrashReporterErrorOperatingSystem, @"Failed to instantiate the Mach exception server.", osError);
        return nil;
    }
    
    /* Allocate the port */
    PLCrashMachExceptionPort *port = [server exceptionPortWithMask: exc_mask error: &osError];
    if (port == nil) {
        plcrash_populate_error(outError, PLCrashReporterErrorOperatingSystem, @"Failed to instantiate the Mach exception port.", osError);
        return nil;
    }
    
    /* Register for the task */
    if (![port registerForTask: mach_task_self() previousPortSet: previousPortSet error: &osError]) {
        plcrash_populate_error(outError, PLCrashReporterErrorOperatingSystem, @"Failed to set the target task's mach exception ports.", osError);
        return nil;
    }

    return server;
}

#endif /* PLCRASH_FEATURE_MACH_EXCEPTIONS */


/**
 * Map the configuration defined @a strategy to the backing plcrash_async_symbol_strategy_t representation.
 *
 * @param strategy The strategy value to map.
 */
- (plcrash_async_symbol_strategy_t) mapToAsyncSymbolicationStrategy: (PLCrashReporterSymbolicationStrategy) strategy {
    plcrash_async_symbol_strategy_t result = PLCRASH_ASYNC_SYMBOL_STRATEGY_NONE;
    
    if (strategy == PLCrashReporterSymbolicationStrategyNone)
        return PLCRASH_ASYNC_SYMBOL_STRATEGY_NONE;
    
    if (strategy & PLCrashReporterSymbolicationStrategySymbolTable)
        result |= PLCRASH_ASYNC_SYMBOL_STRATEGY_SYMBOL_TABLE;
    
    if (strategy & PLCrashReporterSymbolicationStrategyObjC)
        result |= PLCRASH_ASYNC_SYMBOL_STRATEGY_OBJC;
    
    return result;
}

/**
 * Validate (and create if necessary) the crash reporter directory structure.
 */
- (BOOL) populateCrashReportDirectoryAndReturnError: (NSError **) outError {
    NSFileManager *fm = [NSFileManager defaultManager];
    
    /* Set up reasonable directory attributes */
    NSDictionary *attributes = [NSDictionary dictionaryWithObject: [NSNumber numberWithUnsignedLong: 0755] forKey: NSFilePosixPermissions];
    
    /* Create the top-level path */
    if (![fm fileExistsAtPath: [self crashReportDirectory]] &&
        ![fm createDirectoryAtPath: [self crashReportDirectory] withIntermediateDirectories: YES attributes: attributes error: outError])
    {
        return NO;
    }

    /* Create the queued crash report directory */
    if (![fm fileExistsAtPath: [self queuedCrashReportDirectory]] &&
        ![fm createDirectoryAtPath: [self queuedCrashReportDirectory] withIntermediateDirectories: YES attributes: attributes error: outError])
    {
        return NO;
    }

    return YES;
}

/**
 * Return the path to the crash reporter data directory.
 */
- (NSString *) crashReportDirectory {
    return _crashReportDirectory;
}


/**
 * Return the path to to-be-sent crash reports.
 */
- (NSString *) queuedCrashReportDirectory {
    return [[self crashReportDirectory] stringByAppendingPathComponent: PLCRASH_QUEUED_DIR];
}

@end
