// Copyright (c) 2019-present, iQIYI, 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.
//

// Created by caikelun on 2019-03-07.

#include <inttypes.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <fcntl.h>
#include <time.h>
#include <ucontext.h>
#include <dirent.h>
#include <string.h>
#include <regex.h>
#include <ctype.h>
#include <sys/mman.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/ptrace.h>
#include <sys/wait.h>
#include <linux/elf.h>
#include "queue.h"
#include "xcc_errno.h"
#include "xcc_util.h"
#include "xcc_b64.h"
#include "xcc_meminfo.h"
#include "xcd_log.h"
#include "xcd_process.h"
#include "xcd_thread.h"
#include "xcd_maps.h"
#include "xcd_regs.h"
#include "xcd_util.h"
#include "xcd_sys.h"

typedef struct xcd_thread_info
{
    xcd_thread_t t;
    TAILQ_ENTRY(xcd_thread_info,) link;
} xcd_thread_info_t;
typedef TAILQ_HEAD(xcd_thread_info_queue, xcd_thread_info,) xcd_thread_info_queue_t;

#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wpadded"
struct xcd_process
{
    pid_t                    pid;
    char                    *pname;
    pid_t                    crash_tid;
    ucontext_t              *uc;
    siginfo_t               *si;
    xcd_thread_info_queue_t  thds;
    size_t                   nthds;
    xcd_maps_t              *maps;
};
#pragma clang diagnostic pop

static int xcd_process_load_threads(xcd_process_t *self)
{
    char               buf[128];
    DIR               *dir;
    struct dirent     *ent;
    pid_t              tid;
    xcd_thread_info_t *thd;

    snprintf(buf, sizeof(buf), "/proc/%d/task", self->pid);
    if(NULL == (dir = opendir(buf))) return XCC_ERRNO_SYS;
    while(NULL != (ent = readdir(dir)))
    {
        if(0 == strcmp(ent->d_name, ".")) continue;
        if(0 == strcmp(ent->d_name, "..")) continue;
        if(0 != xcc_util_atoi(ent->d_name, &tid)) continue;
        
        if(NULL == (thd = malloc(sizeof(xcd_thread_info_t)))) return XCC_ERRNO_NOMEM;
        xcd_thread_init(&(thd->t), self->pid, tid);
        
        TAILQ_INSERT_TAIL(&(self->thds), thd, link);
        self->nthds++;
    }
    closedir(dir);

    return 0;
}

int xcd_process_create(xcd_process_t **self, pid_t pid, pid_t crash_tid, siginfo_t *si, ucontext_t *uc)
{
    int                r;
    xcd_thread_info_t *thd;
    
    if(NULL == (*self = malloc(sizeof(xcd_process_t)))) return XCC_ERRNO_NOMEM;
    (*self)->pid       = pid;
    (*self)->pname     = NULL;
    (*self)->crash_tid = crash_tid;
    (*self)->si        = si;
    (*self)->uc        = uc;
    (*self)->nthds     = 0;
    TAILQ_INIT(&((*self)->thds));

    if(0 != (r = xcd_process_load_threads(*self)))
    {
        XCD_LOG_ERROR("PROCESS: load threads failed, errno=%d", r);
        return r;
    }

    //check if crashed thread existed
    TAILQ_FOREACH(thd, &((*self)->thds), link)
    {
        if(thd->t.tid == (*self)->crash_tid)
            return 0; //OK
    }

    XCD_LOG_ERROR("PROCESS: crashed thread NOT found");
    return XCC_ERRNO_NOTFND;
}

size_t xcd_process_get_number_of_threads(xcd_process_t *self)
{
    return self->nthds;
}

void xcd_process_suspend_threads(xcd_process_t *self)
{
    xcd_thread_info_t *thd;
    TAILQ_FOREACH(thd, &(self->thds), link)
        xcd_thread_suspend(&(thd->t));
}

void xcd_process_resume_threads(xcd_process_t *self)
{
    xcd_thread_info_t *thd;
    TAILQ_FOREACH(thd, &(self->thds), link)
        xcd_thread_resume(&(thd->t));
}

int xcd_process_load_info(xcd_process_t *self)
{
    int                r;
    xcd_thread_info_t *thd;
    char               buf[256];
    
    xcc_util_get_process_name(self->pid, buf, sizeof(buf));
    if(NULL == (self->pname = strdup(buf))) self->pname = "unknown";

    TAILQ_FOREACH(thd, &(self->thds), link)
    {
        //load thread info
        xcd_thread_load_info(&(thd->t));
        
        //load thread regs
        if(thd->t.tid != self->crash_tid)
            xcd_thread_load_regs(&(thd->t));
        else
            xcd_thread_load_regs_from_ucontext(&(thd->t), self->uc);
    }

    //load maps
    if(0 != (r = xcd_maps_create(&(self->maps), self->pid)))
        XCD_LOG_ERROR("PROCESS: create maps failed, errno=%d", r);

    return 0;
}

static int xcd_process_record_signal_info(xcd_process_t *self, int log_fd)
{
    //fault addr
    char addr_desc[64];
    if(xcc_util_signal_has_si_addr(self->si))
    {
        void *addr = self->si->si_addr;
        if(self->si->si_signo == SIGILL)
        {
            uint32_t instruction = 0;
            xcd_util_ptrace_read(self->pid, (uintptr_t)addr, &instruction, sizeof(instruction));
            snprintf(addr_desc, sizeof(addr_desc), "%p (*pc=%#08x)", addr, instruction);
        }
        else
        {
            snprintf(addr_desc, sizeof(addr_desc), "%p", addr);
        }
    }
    else
    {
        snprintf(addr_desc, sizeof(addr_desc), "--------");
    }

    //from
    char sender_desc[64] = "";
    if(xcc_util_signal_has_sender(self->si, self->pid))
    {
        snprintf(sender_desc, sizeof(sender_desc), " from pid %d, uid %d", self->si->si_pid, self->si->si_uid);
    }

    return xcc_util_write_format(log_fd, "signal %d (%s), code %d (%s%s), fault addr %s\n",
                                 self->si->si_signo, xcc_util_get_signame(self->si),
                                 self->si->si_code, xcc_util_get_sigcodename(self->si),
                                 sender_desc, addr_desc);
}

static int xcd_process_get_abort_message_29(xcd_process_t *self, char *buf, size_t buf_len)
{
    //
    // struct abort_msg_t {
    //     size_t size;
    //     char msg[0];
    // };
    //
    // struct magic_abort_msg_t {
    //     uint64_t magic1;
    //     uint64_t magic2;
    //     abort_msg_t msg;
    // };
    //
    // ...
    // size_t size = sizeof(magic_abort_msg_t) + strlen(msg) + 1;
    // ...
    //

    int r;

    //get abort_msg_t *p
    uintptr_t p = xcd_maps_find_abort_msg(self->maps);
    if(0 == p) return XCC_ERRNO_NOTFND;
    p += (sizeof(uint64_t) * 2);

    //get size
    size_t size = 0;
    if(0 != (r = xcd_util_ptrace_read_fully(self->pid, p, &size, sizeof(size_t)))) return r;
    if(size < (sizeof(uint64_t) * 2 + sizeof(size_t) + 1 + 1)) return XCC_ERRNO_NOTFND;
    XCD_LOG_DEBUG("PROCESS: abort_msg, size = %zu", size);

    //get strlen(msg)
    size -= (sizeof(uint64_t) * 2 + sizeof(size_t) + 1);

    //get p->msg
    if(size > buf_len) size = buf_len;
    if(0 != (r = xcd_util_ptrace_read_fully(self->pid, p + sizeof(size_t), buf, size))) return r;

    return 0;
}

static int xcd_process_get_abort_message_14(xcd_process_t *self, char *buf, size_t buf_len)
{
    //
    // struct abort_msg_t {
    //     size_t size;
    //     char msg[0];
    // };
    //
    // abort_msg_t** __abort_message_ptr;
    //
    // ......
    // size_t size = sizeof(abort_msg_t) + strlen(msg) + 1;
    // ......
    //

    int r;

    //get abort_msg_t ***ppp (&__abort_message_ptr)
    uintptr_t ppp = 0;
    ppp = xcd_maps_find_pc(self->maps, XCC_UTIL_LIBC, XCC_UTIL_LIBC_ABORT_MSG_PTR);
    if(0 == ppp) return XCC_ERRNO_NOTFND;
    XCD_LOG_DEBUG("PROCESS: abort_msg, ppp = %"PRIxPTR, ppp);

    //get abort_msg_t **pp (__abort_message_ptr)
    uintptr_t pp = 0;
    if(0 != (r = xcd_util_ptrace_read_fully(self->pid, ppp, &pp, sizeof(uintptr_t)))) return r;
    if(0 == pp) return XCC_ERRNO_NOTFND;
    XCD_LOG_DEBUG("PROCESS: abort_msg, pp = %"PRIxPTR, pp);

    //get abort_msg_t *p (*__abort_message_ptr)
    uintptr_t p = 0;
    if(0 != (r = xcd_util_ptrace_read_fully(self->pid, pp, &p, sizeof(uintptr_t)))) return r;
    if(0 == p) return XCC_ERRNO_NOTFND;
    XCD_LOG_DEBUG("PROCESS: abort_msg, p = %"PRIxPTR, p);

    //get p->size
    size_t size = 0;
    if(0 != (r = xcd_util_ptrace_read_fully(self->pid, p, &size, sizeof(size_t)))) return r;
    if(size < (sizeof(size_t) + 1 + 1)) return XCC_ERRNO_NOTFND;
    XCD_LOG_DEBUG("PROCESS: abort_msg, size = %zu", size);

    //get strlen(msg)
    size -= (sizeof(size_t) + 1);

    //get p->msg
    if(size > buf_len) size = buf_len;
    if(0 != (r = xcd_util_ptrace_read_fully(self->pid, p + sizeof(size_t), buf, size))) return r;

    return 0;
}

static int xcd_process_record_abort_message(xcd_process_t *self, int log_fd, int api_level)
{
    char msg[256 + 1];
    memset(msg, 0, sizeof(msg));

    if(api_level >= 29)
    {
        if(0 != xcd_process_get_abort_message_29(self, msg, sizeof(msg) - 1)) return 0;
    }
    else
    {
        if(0 != xcd_process_get_abort_message_14(self, msg, sizeof(msg) - 1)) return 0;
    }

    //format
    size_t i;
    for(i = 0; i < strlen(msg); i++)
    {
        if(isspace(msg[i]) && ' ' != msg[i])
            msg[i] = ' ';
    }

    //write
    return xcc_util_write_format(log_fd, "Abort message: '%s'\n", msg);
}

static regex_t *xcd_process_build_whitelist_regex(char *dump_all_threads_whitelist, size_t *re_cnt)
{
    if(NULL == dump_all_threads_whitelist || 0 == strlen(dump_all_threads_whitelist)) return NULL;

    char *p = dump_all_threads_whitelist;
    size_t cnt = 0;
    while(*p)
    {
        if(*p == '|') cnt++;
        p++;
    }
    cnt += 1;

    regex_t *re = NULL;
    if(NULL == (re = calloc(cnt, sizeof(regex_t)))) return NULL;

    char *tmp;
    char *regex_str = strtok_r(dump_all_threads_whitelist, "|", &tmp);
    char *regex_str_decoded;
    size_t i = 0;
    while(regex_str)
    {
        regex_str_decoded = (char *)xcc_b64_decode(regex_str, strlen(regex_str), NULL);
        if(0 == regcomp(&(re[i]), regex_str_decoded, REG_EXTENDED | REG_NOSUB))
        {
            XCD_LOG_DEBUG("PROCESS: compile regex OK: %s", regex_str_decoded);
            i++;
        }
        regex_str = strtok_r(NULL, "|", &tmp);
    }

    if(0 == i)
    {
        free(re);
        return NULL;
    }

    XCD_LOG_DEBUG("PROCESS: got %zu regex", i);
    *re_cnt = i;
    return re;
}

static int xcd_process_if_need_dump(char *tname, regex_t *re, size_t re_cnt)
{
    if(NULL == re || 0 == re_cnt) return 1;

    size_t i;
    for(i = 0; i < re_cnt; i++)
        if(0 == regexec(&(re[i]), tname, 0, NULL, 0)) return 1;

    return 0;
}

int xcd_process_record(xcd_process_t *self,
                       int log_fd,
                       unsigned int logcat_system_lines,
                       unsigned int logcat_events_lines,
                       unsigned int logcat_main_lines,
                       int dump_elf_hash,
                       int dump_map,
                       int dump_fds,
                       int dump_network_info,
                       int dump_all_threads,
                       unsigned int dump_all_threads_count_max,
                       char *dump_all_threads_whitelist,
                       int api_level)
{
    int                r = 0;
    xcd_thread_info_t *thd;
    regex_t           *re = NULL;
    size_t             re_cnt = 0;
    unsigned int       thd_dumped = 0;
    int                thd_matched_regex = 0;
    int                thd_ignored_by_limit = 0;
    
    TAILQ_FOREACH(thd, &(self->thds), link)
    {
        if(thd->t.tid == self->crash_tid)
        {
            if(0 != (r = xcd_thread_record_info(&(thd->t), log_fd, self->pname))) return r;
            if(0 != (r = xcd_process_record_signal_info(self, log_fd))) return r;
            if(0 != (r = xcd_process_record_abort_message(self, log_fd, api_level))) return r;
            if(0 != (r = xcd_thread_record_regs(&(thd->t), log_fd))) return r;
            if(0 == xcd_thread_load_frames(&(thd->t), self->maps))
            {
                if(0 != (r = xcd_thread_record_backtrace(&(thd->t), log_fd))) return r;
                if(0 != (r = xcd_thread_record_buildid(&(thd->t), log_fd, dump_elf_hash, xcc_util_signal_has_si_addr(self->si) ? (uintptr_t)self->si->si_addr : 0))) return r;
                if(0 != (r = xcd_thread_record_stack(&(thd->t), log_fd))) return r;
                if(0 != (r = xcd_thread_record_memory(&(thd->t), log_fd))) return r;
            }
            if(dump_map) if(0 != (r = xcd_maps_record(self->maps, log_fd))) return r;
            if(0 != (r = xcc_util_record_logcat(log_fd, self->pid, api_level, logcat_system_lines, logcat_events_lines, logcat_main_lines))) return r;
            if(dump_fds) if(0 != (r = xcc_util_record_fds(log_fd, self->pid))) return r;
            if(dump_network_info) if(0 != (r = xcc_util_record_network_info(log_fd, self->pid, api_level))) return r;
            if(0 != (r = xcc_meminfo_record(log_fd, self->pid))) return r;

            break;
        }
    }
    if(!dump_all_threads) return 0;

    //parse thread name whitelist regex
    re = xcd_process_build_whitelist_regex(dump_all_threads_whitelist, &re_cnt);

    TAILQ_FOREACH(thd, &(self->thds), link)
    {
        if(thd->t.tid != self->crash_tid)
        {
            //check regex for thread name
            if(NULL != re && re_cnt > 0 && !xcd_process_if_need_dump(thd->t.tname, re, re_cnt))
            {
                continue;
            }
            thd_matched_regex++;

            //check dump count limit
            if(dump_all_threads_count_max > 0 && thd_dumped >= dump_all_threads_count_max)
            {
                thd_ignored_by_limit++;
                continue;
            }

            if(0 != (r = xcc_util_write_str(log_fd, XCC_UTIL_THREAD_SEP))) goto end;
            if(0 != (r = xcd_thread_record_info(&(thd->t), log_fd, self->pname))) goto end;
            if(0 != (r = xcd_thread_record_regs(&(thd->t), log_fd))) goto end;
            if(0 == xcd_thread_load_frames(&(thd->t), self->maps))
            {
                if(0 != (r = xcd_thread_record_backtrace(&(thd->t), log_fd))) goto end;
                if(0 != (r = xcd_thread_record_stack(&(thd->t), log_fd))) goto end;
            }
            thd_dumped++;
        }
    }

 end:
    if(self->nthds > 1)
    {
        if(0 == thd_dumped)
            if(0 != (r = xcc_util_write_str(log_fd, XCC_UTIL_THREAD_SEP))) goto ret;

        if(0 != (r = xcc_util_write_format(log_fd, "total threads (exclude the crashed thread): %zu\n", self->nthds - 1))) goto ret;
        if(NULL != re && re_cnt > 0)
            if(0 != (r = xcc_util_write_format(log_fd, "threads matched whitelist: %d\n", thd_matched_regex))) goto ret;
        if(dump_all_threads_count_max > 0)
            if(0 != (r = xcc_util_write_format(log_fd, "threads ignored by max count limit: %d\n", thd_ignored_by_limit))) goto ret;
        if(0 != (r = xcc_util_write_format(log_fd, "dumped threads: %u\n", thd_dumped))) goto ret;
        
        if(0 != (r = xcc_util_write_str(log_fd, XCC_UTIL_THREAD_END))) goto ret;
    }
    
 ret:
    return r;
}
