class Proxy: ''' Contains code to control the target indirectly. ''' def __init__(self, dsn: str): self.database = DB(dsn) self.session_id = None def _run_cmd(self, cmd: str, args: List) -> List: args = ','.join([str(arg) for arg in args]) return self.database.run_sql(f'SELECT * FROM {cmd}({args})', fetch_result=True) def attach(self, port: int) -> int: ''' Attach to an opened debugger port. ''' result = self._run_cmd('pldbg_attach_to_port', [port]) self.session_id = result[0][0] def cont(self): ''' Continue execution until the next breakpoint. ''' result = self._run_cmd('pldbg_continue', [self.session_id]) logger.debug(f'Continue result: {result}') def abort(self): ''' Abort waiting for the target. ''' result = self._run_cmd('pldbg_abort_target', [self.session_id]) logger.debug(f'Abort result: {result}') def get_variables(self) -> List[Variable]: ''' Get variables of the currently active frame in the active session. ''' result = self._run_cmd('pldbg_get_variables', [self.session_id]) return [Variable(*row) for row in result] def step_over(self) -> Breakpoint: ''' Step over a call until next blocking statement. ''' result = self._run_cmd('pldbg_step_over', [self.session_id]) return Breakpoint(*result[0]) def step_into(self) -> Breakpoint: ''' Step into a call, stop at next blocking statement. ''' result = self._run_cmd('pldbg_step_into', [self.session_id]) return Breakpoint(*result[0]) def get_source(self, oid) -> str: ''' Get source of the provided OID. ''' result = self._run_cmd('pldbg_get_source', [self.session_id, oid]) return result[0][0] def get_stack(self) -> List[Frame]: ''' Get current stack of the active session. ''' result = self._run_cmd('pldbg_get_stack', [self.session_id]) return [Frame(*row) for row in result] def get_breakpoints(self) -> List[Breakpoint]: ''' Get all breakpoints of the current session. ''' result = self._run_cmd('pldbg_get_breakpoints', [self.session_id]) return [Breakpoint(*row) for row in result] def set_breakpoint(self, oid, line_number): ''' Set a breakpoint for the provided OID at given line number. ''' result = self._run_cmd('pldbg_set_breakpoint', [self.session_id, oid, line_number]) logger.debug(f'Set breakpoint result: {result}')
class Target: ''' This is the target. It controls/contains the code to be debugged. ''' def __init__(self, dsn: str): self.database = DB(dsn, is_async=True) self.notice_queue = Queue() self.oid = None self.executor = None self.port = None def cleanup(self): ''' Cleanup routine for the target. ''' self.database.cleanup() def get_notices(self) -> List[str]: ''' Get all notices the target might have. Reads from an internal queue, does not use the DB itself since it is likely blocked. ''' notices = [] while not self.notice_queue.empty(): notices.append(self.notice_queue.get()) logger.debug(f'Target notices: {notices}') return notices @classmethod def _parse_port(cls, port_raw: str) -> int: return int(port_raw.split(':')[-1]) def wait_for_shutdown(self): ''' Wait until the target completed fully. ''' self.executor.join() def _run_executor_thread(self, func_call, func_oid): self.executor = Thread(target=self._run, args=(func_call, func_oid)) self.executor.daemon = True self.executor.start() @classmethod def assert_valid_function_call(cls, func_call: str) -> bool: return re.match(r'[_a-zA-Z0-9]+\([^\)]*\)(\.[^\)]*\))?', func_call) is not None def start(self, func_call: str) -> bool: ''' Start target debugging. Resolve the function to be debugged, find its OID and eventually start a thread calling it. ''' print(func_call) if not Target.assert_valid_function_call(func_call): logger.error(f'Function call seems incomplete: {func_call}') return False func_name, func_args = Target._parse_func_call(func_call) func_oid = get_func_oid_by_name(self.database, func_name) if not func_oid: logger.error('Function OID not found. Either function is not ' 'defined or there are multiple with the same name ' 'which is currently not support') return False logger.debug(f'Function OID is: {func_oid}') self._run_executor_thread(func_call, func_oid) # Wait here until the executor started logger.debug('Waiting for port') self.port = Target._parse_port(self.notice_queue.get()) logger.debug(f'Port is: {self.port}') return True def _run(self, func_call: str, func_oid: int): ''' Start a debugging session. Consumes the function call to debug with its arguments and returns the session ID once the debugger started. ''' self.oid = func_oid self.database.run_sql(f'SELECT * FROM pldbg_oid_debug({func_oid})') while True: logger.debug('Starting target function') try: result = self.database.run_sql(f'SELECT * FROM {func_call}', fetch_result=True, notice_queue=self.notice_queue) # This will now wait here until the function finishes. It will # eventually restart immediately. Otherwise, the proxy process # hangs until it hits a timeout. logger.debug(f'Target result: {result}') except QueryCanceled: logger.info('Stopped target query') break @classmethod def _parse_func_call(cls, func_call: str) -> Tuple[str, List[str]]: ''' Take a function call and return the function name and its arguments. ''' func_call = func_call.replace(' ', '').replace('(', ',').replace(')', '') func_call = func_call.split(',') return func_call[0], func_call[1:]