#!/usr/bin/env python
# coding=utf-8

# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements.  See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership.  The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License.  You may obtain a copy of the License at
#
#   http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied.  See the License for the
# specific language governing permissions and limitations
# under the License.

"""
Bthread Stack Print Tool

this only for running process, core dump is not supported.

Get Started:
    1. gdb attach <pid>
    2. source gdb_bthread_stack.py
    3. bthread_begin
    4. bthread_list
    5. bthread_frame 0
    6. bt / up / down
    7. bthread_end

Commands:
    1. bthread_num: print all bthread nums
    2. bthread_begin <num>: enter bthread debug mode, `num` is max scanned bthreads, default will scan all
    3. bthread_list: list all bthreads
    4. bthread_frame <id>: switch stack to bthread, id will displayed in bthread_list
    5. bthread_meta <id>: print bthread meta
    6. bthread_reg_restore: bthread_frame will modify registers, reg_restore will restore them
    7. bthread_end: exit bthread debug mode
    8. bthread_regs <id>: print bthread registers
    9. bthread_all: print all bthread frames

when call bthread_frame, registers will be modified,
remember to call bthread_end after debug, or process will be corrupted

after call bthread_frame, you can call `bt`/`up`/`down`, or other gdb command
"""

import gdb

bthreads = []
status = False

def get_bthread_num():
    root_agent = gdb.parse_and_eval("&(((*(((*bthread::g_task_control)._nbthreads)._combiner._M_ptr))._agents).root_)")
    global_res = int(gdb.parse_and_eval("(*(((*bthread::g_task_control)._nbthreads)._combiner._M_ptr))._global_result"))
    get_agent = "(*(('bvar::detail::AgentCombiner<long, long, bvar::detail::AddTo<long> >::Agent' *){}))"
    last_node = root_agent
    long_type = gdb.lookup_type("long")
    while True:
        agent = gdb.parse_and_eval(get_agent.format(last_node))
        if last_node != root_agent:
            val = int(agent["element"]["_value"].cast(long_type))
            gdb.parse_and_eval(get_agent.format(last_node))
            global_res += val
        if agent["next_"] == root_agent:
            return global_res
        last_node = agent["next_"]

def get_all_bthreads(total):
    global bthreads
    bthreads = []
    count = 0
    groups = int(gdb.parse_and_eval("(size_t)'butil::ResourcePool<bthread::TaskMeta>::_ngroup'"))
    for group in range(groups):
        blocks = int(gdb.parse_and_eval("(unsigned long)(*((*((('butil::static_atomic<butil::ResourcePool<bthread::TaskMeta>::BlockGroup*>' *)('butil::ResourcePool<bthread::TaskMeta>::_block_groups')) + {})).val)).nblock".format(group)))
        for block in range(blocks):
            items = int(gdb.parse_and_eval("(*(*(('butil::ResourcePool<bthread::TaskMeta>::Block' **)((*((*((('butil::static_atomic<butil::ResourcePool<bthread::TaskMeta>::BlockGroup*>' *)('butil::ResourcePool<bthread::TaskMeta>::_block_groups'))+ {})).val)).blocks) + {}))).nitem".format(group, block)))
            for item in range(items):
                task_meta = gdb.parse_and_eval("*(('bthread::TaskMeta' *)((*(*(('butil::ResourcePool<bthread::TaskMeta>::Block' **)((*((*((('butil::static_atomic<butil::ResourcePool<bthread::TaskMeta>::BlockGroup*>' *)('butil::ResourcePool<bthread::TaskMeta>::_block_groups')) + {})).val)).blocks) + {}))).items) + {})".format(group, block, item))
                version_tid = (int(task_meta["tid"]) >> 32)
                version_butex = gdb.parse_and_eval(
                    "*(uint32_t *){}".format(task_meta["version_butex"]))
                if version_tid == int(version_butex) and int(task_meta["attr"]["stack_type"]) != 0:
                    bthreads.append(task_meta)
                    count += 1
                    if count >= total:
                        return

class BthreadListCmd(gdb.Command):
    """list all bthreads, print format is 'id\ttid\tfunction\thas stack'"""
    def __init__(self):
        gdb.Command.__init__(self, "bthread_list", gdb.COMMAND_STACK, gdb.COMPLETE_NONE)

    def invoke(self, arg, tty):
        global status
        global bthreads
        if not status:
            print("Not in bthread debug mode")
            return
        print("id\t\ttid\t\tfunction\t\thas stack\t\t\ttotal:{}".format(len(bthreads)))
        for i, t in enumerate(bthreads):
            print("#{}\t\t{}\t\t{}\t\t{}".format(i, t["tid"], t["fn"], "no" if str(t["stack"]) == "0x0" else "yes"))

class BthreadNumCmd(gdb.Command):
    """list active bthreads num"""
    def __init__(self):
        gdb.Command.__init__(self, "bthread_num", gdb.COMMAND_STACK, gdb.COMPLETE_NONE)

    def invoke(self, arg, tty):
        res = get_bthread_num()
        print(res)

class BthreadFrameCmd(gdb.Command):
    """bthread_frame <id>, select bthread frame by id"""
    def __init__(self):
        gdb.Command.__init__(self, "bthread_frame", gdb.COMMAND_STACK, gdb.COMPLETE_NONE)

    def invoke(self, arg, tty):
        global status
        global bthreads
        if not status:
            print("Not in bthread debug mode")
            return
        if not arg:
            print("bthread_frame <id>, see 'bthread_list'")
            return
        bthread_id = int(arg)
        if bthread_id >= len(bthreads):
            print("id {} exceeds max bthread nums {}".format(bthread_id, len(bthreads)))
            return
        stack = bthreads[bthread_id]["stack"]
        if str(stack) == "0x0":
            print("this bthread has no stack")
            return
        context = gdb.parse_and_eval("(*(('bthread::ContextualStack' *){})).context".format(stack))
        rip = gdb.parse_and_eval("*(uint64_t*)({}+7*8)".format(context))
        rbp = gdb.parse_and_eval("*(uint64_t*)({}+6*8)".format(context))
        rsp = gdb.parse_and_eval("{}+8*8".format(context))
        gdb.parse_and_eval("$rip = {}".format(rip))
        gdb.parse_and_eval("$rsp = {}".format(rsp))
        gdb.parse_and_eval("$rbp = {}".format(rbp))

class BthreadAllCmd(gdb.Command):
    """print all bthread frames"""
    def __init__(self):
        gdb.Command.__init__(self, "bthread_all", gdb.COMMAND_STACK, gdb.COMPLETE_NONE)

    def invoke(self, arg, tty):
        global status
        global bthreads
        if not status:
            print("Not in bthread debug mode")
            return
        for bthread_id in range(len(bthreads)):
            stack = bthreads[bthread_id]["stack"]
            if str(stack) == "0x0":
                print("this bthread has no stack")
                continue
            try:
                context = gdb.parse_and_eval("(*(('bthread::ContextualStack' *){})).context".format(stack))
                rip = gdb.parse_and_eval("*(uint64_t*)({}+7*8)".format(context))
                rbp = gdb.parse_and_eval("*(uint64_t*)({}+6*8)".format(context))
                rsp = gdb.parse_and_eval("{}+8*8".format(context))
                gdb.parse_and_eval("$rip = {}".format(rip))
                gdb.parse_and_eval("$rsp = {}".format(rsp))
                gdb.parse_and_eval("$rbp = {}".format(rbp))
                print("Bthread {}:".format(bthread_id))
                gdb.execute('bt')
            except Exception as e:
                pass

class BthreadRegsCmd(gdb.Command):
    """bthread_regs <id>, print bthread registers"""
    def __init__(self):
        gdb.Command.__init__(self, "bthread_regs", gdb.COMMAND_STACK, gdb.COMPLETE_NONE)

    def invoke(self, arg, tty):
        global status
        global bthreads
        if not status:
            print("Not in bthread debug mode")
            return
        if not arg:
            print("bthread_regs <id>, see 'bthread_list'")
            return
        bthread_id = int(arg)
        if bthread_id >= len(bthreads):
            print("id {} exceeds max bthread nums {}".format(bthread_id, len(bthreads)))
            return
        stack = bthreads[bthread_id]["stack"]
        if str(stack) == "0x0":
            print("this bthread has no stack")
            return
        context = gdb.parse_and_eval("(*(('bthread::ContextualStack' *){})).context".format(stack))
        rip = int(gdb.parse_and_eval("*(uint64_t*)({}+7*8)".format(context)))
        rbp = int(gdb.parse_and_eval("*(uint64_t*)({}+6*8)".format(context)))
        rbx = int(gdb.parse_and_eval("*(uint64_t*)({}+5*8)".format(context)))
        r15 = int(gdb.parse_and_eval("*(uint64_t*)({}+4*8)".format(context)))
        r14 = int(gdb.parse_and_eval("*(uint64_t*)({}+3*8)".format(context)))
        r13 = int(gdb.parse_and_eval("*(uint64_t*)({}+2*8)".format(context)))
        r12 = int(gdb.parse_and_eval("*(uint64_t*)({}+1*8)".format(context)))
        rsp = int(gdb.parse_and_eval("{}+8*8".format(context)))
        print("rip: 0x{:x}\nrsp: 0x{:x}\nrbp: 0x{:x}\nrbx: 0x{:x}\nr15: 0x{:x}\nr14: 0x{:x}\nr13: 0x{:x}\nr12: 0x{:x}".format(rip, rsp, rbp, rbx, r15, r14, r13, r12))

class BthreadMetaCmd(gdb.Command):
    """bthread_meta <id>, print task meta by id"""
    def __init__(self):
        gdb.Command.__init__(self, "bthread_meta", gdb.COMMAND_STACK, gdb.COMPLETE_NONE)

    def invoke(self, arg, tty):
        global status
        global bthreads
        if not status:
            print("Not in bthread debug mode")
            return
        if not arg:
            print("bthread_meta <id>, see 'bthread_list'")
            return
        bthread_id = int(arg)
        if bthread_id >= len(bthreads):
            print("id {} exceeds max bthread nums {}".format(bthread_id, len(bthreads)))
            return
        print(bthreads[bthread_id])

class BthreadBeginCmd(gdb.Command):
    """enter bthread debug mode"""
    def __init__(self):
        gdb.Command.__init__(self, "bthread_begin", gdb.COMMAND_STACK, gdb.COMPLETE_NONE)

    def invoke(self, arg, tty):
        global status
        if status:
            print("Already in bthread debug mode, do not switch thread before exec 'bthread_end' !!!")
            return
        active_bthreads = get_bthread_num()
        scanned_bthreads = active_bthreads
        if arg:
            num_arg = int(arg)
            if num_arg < active_bthreads:
                scanned_bthreads = num_arg
            else:
                print("requested bthreads {} more than actived, will display {} bthreads".format(num_arg, scanned_bthreads))
        print("Active bthreads: {}, will display {} bthreads".format(active_bthreads, scanned_bthreads))
        get_all_bthreads(scanned_bthreads)
        gdb.parse_and_eval("$saved_rip = $rip")
        gdb.parse_and_eval("$saved_rsp = $rsp")
        gdb.parse_and_eval("$saved_rbp = $rbp")
        status = True
        print("Enter bthread debug mode, do not switch thread before exec 'bthread_end' !!!")

class BthreadRegRestoreCmd(gdb.Command):
    """restore registers"""
    def __init__(self):
        gdb.Command.__init__(self, "bthread_reg_restore", gdb.COMMAND_STACK, gdb.COMPLETE_NONE)

    def invoke(self, arg, tty):
        global status
        if not status:
            print("Not in bthread debug mode")
            return
        gdb.parse_and_eval("$rip = $saved_rip")
        gdb.parse_and_eval("$rsp = $saved_rsp")
        gdb.parse_and_eval("$rbp = $saved_rbp")
        print("OK")

class BthreadEndCmd(gdb.Command):
    """exit bthread debug mode"""
    def __init__(self):
        gdb.Command.__init__(self, "bthread_end", gdb.COMMAND_STACK, gdb.COMPLETE_NONE)

    def invoke(self, arg, tty):
        global status
        if not status:
            print("Not in bthread debug mode")
            return
        gdb.parse_and_eval("$rip = $saved_rip")
        gdb.parse_and_eval("$rsp = $saved_rsp")
        gdb.parse_and_eval("$rbp = $saved_rbp")
        status = False
        print("Exit bthread debug mode")

BthreadListCmd()
BthreadNumCmd()
BthreadBeginCmd()
BthreadEndCmd()
BthreadFrameCmd()
BthreadAllCmd()
BthreadMetaCmd()
BthreadRegRestoreCmd()
BthreadRegsCmd()
