From b7bb93c1740d68ea541ac8eb6a047f718d276cc1 Mon Sep 17 00:00:00 2001 From: Neal Beeken Date: Sun, 30 Sep 2018 18:43:35 -0400 Subject: [PATCH] Stateless debugging server, Clean cli --- .gitignore | 1 + dashmips/__main__.py | 18 ++++-- dashmips/debugger.py | 62 +++++++------------ dashmips/debugserver.py | 92 +++++++++++++++------------- dashmips/hardware.py | 17 ++--- dashmips/mips.py | 9 +++ dashmips/preprocessor.py | 35 +++-------- dashmips/run.py | 46 +++++++------- setup.py | 14 +++++ {test => tests}/debugserver_tests.py | 0 10 files changed, 150 insertions(+), 144 deletions(-) create mode 100644 setup.py rename {test => tests}/debugserver_tests.py (100%) diff --git a/.gitignore b/.gitignore index 288567b..3aba4f4 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ __pycache__ .vs/ test.mips.json .idea/ +*.egg-info diff --git a/dashmips/__main__.py b/dashmips/__main__.py index a9732c7..96d00a6 100644 --- a/dashmips/__main__.py +++ b/dashmips/__main__.py @@ -1,15 +1,18 @@ """dashmips program.""" +import json from dashmips.preprocessor import preprocess, MipsProgram from dashmips.debugserver import debug_mips + def main_compile(args): """Compile/Exec mips code.""" program = preprocess(args.FILE) if args.out: - program.dump(args.out) + json.dump(dict(program), args.out) if args.json: - print(program.dumps(), end='\r\n\r\n') # Ending matches socket comm + # Ending matches socket communication + print(json.dumps(dict(program)), end='\r\n\r\n') if args.run: from dashmips.run import run @@ -23,11 +26,14 @@ def main_debug(args): debug_mips(host=args.host, port=args.port) -if __name__ == "__main__": +def main(): + """Entry function for Dashmips.""" import argparse import sys parser = argparse.ArgumentParser('dashmips') + parser.add_argument('-v', '--version', action='version', version='0.0.1') + sbp = parser.add_subparsers(title='commands', dest='command', required=True) compileparse = sbp.add_parser('compile', aliases=['c']) debugparse = sbp.add_parser('debug', aliases=['d']) @@ -52,9 +58,13 @@ def main_debug(args): '-p', '--port', type=int, default=9999, help='run debugger on port' ) debugparse.add_argument( - '-i', '--host', default='localhost', help='run debugger on host' + '-i', '--host', default='0.0.0.0', help='run debugger on host' ) debugparse.set_defaults(func=main_debug) prog_args = parser.parse_args() sys.exit(prog_args.func(prog_args)) + + +if __name__ == "__main__": + main() diff --git a/dashmips/debugger.py b/dashmips/debugger.py index 50f2a57..b3aa2e3 100644 --- a/dashmips/debugger.py +++ b/dashmips/debugger.py @@ -3,7 +3,8 @@ from typing import Dict, Callable, Optional from dashmips.debugserver import DebugMessage -from dashmips.instructions import Instructions +from dashmips.run import next_instruction, run +from dashmips.mips import MipsException def debug_start(msg: DebugMessage) -> Optional[DebugMessage]: @@ -14,57 +15,40 @@ def debug_start(msg: DebugMessage) -> Optional[DebugMessage]: def debug_step(msg: DebugMessage) -> DebugMessage: """Debug step.""" - current_pc = msg.program.registers['pc'] - if len(msg.program.source) < current_pc: - # We jumped or executed beyond available text - msg.message = 'pc is greater than len(source)' + try: + next_instruction(msg.program) + # TODO: Should be doing something with breakpoints here + except MipsException as exc: msg.error = True - return msg - - lineofcode = msg.program.source[current_pc].line # Current line of execution - instruction = lineofcode.split(' ')[0] # Grab the instruction name - - instruction_fn = Instructions[instruction] # relevant Instruction() - - match = re.match(instruction_fn.regex, lineofcode) - if match: - # Instruction has the correct format - args = instruction_fn.parser(match) - instruction_fn(msg.program, args) - else: - # Bad arguments to instruction - msg.message = f"{lineofcode} is malformed for {instruction}" - msg.error = True - return msg + msg.message = exc.message return msg def debug_continue(msg: DebugMessage) -> DebugMessage: """Debug continue.""" - msg.message = 'Not Implemented' - msg.error = True - return msg - - -def debug_stepreverse(msg: DebugMessage) -> DebugMessage: - """Debug stepreverse.""" - msg.message = 'Not Implemented' - msg.error = True - return msg - + starting_pc = msg.program.registers['pc'] + + def breaking_condition(program): + nonlocal starting_pc + if program.registers['pc'] == starting_pc: + # current instruction will execute even if on breakpoint + # b/c we would have broken on it last time. + return True + if program.registers['pc'] in msg.breakpoints: + return False + return True + try: + run(msg.program, breaking_condition) + except MipsException as exc: + msg.error = True + msg.message = exc.message -def debug_restart(msg: DebugMessage) -> DebugMessage: - """Debug restart.""" - msg.message = 'Not Implemented' - msg.error = True return msg Commands: Dict[str, Callable] = { 'start': debug_start, - 'restart': debug_restart, 'step': debug_step, - 'stepreverse': debug_stepreverse, 'continue': debug_continue, } diff --git a/dashmips/debugserver.py b/dashmips/debugserver.py index e4da870..2016fba 100644 --- a/dashmips/debugserver.py +++ b/dashmips/debugserver.py @@ -9,19 +9,6 @@ from dashmips.preprocessor import MipsProgram -BAD_MSG = json.dumps({ - 'command': 'stop', - 'message': 'malformed message', - 'program': None, - 'error': True -}) -ERR_MSG = json.dumps({ - 'command': 'stop', - 'message': '500 internal error', - 'program': None, - 'error': True, -}) - @dataclass class DebugMessage: @@ -33,55 +20,67 @@ class DebugMessage: message: str = '' error: bool = False - def dumps(self): - """Dump Json formatted Debug message""" - msg = asdict(self) - return json.dumps(msg) + def __post_init__(self): + # set to remove duplicates and sort + self.breakpoints = sorted(set(self.breakpoints)) + + def __iter__(self): + return iter(asdict(self).items()) @staticmethod - def loads(string): + def from_dict(payload: dict): """Deserialize from json to DebugMessage.""" from dashmips.debugger import Commands - try: - payload = json.loads(string) - - if ('command' not in payload and - payload['command'] not in Commands): - # Json doesn't contain a valid command nor program - return None - - if 'program' in payload: - payload['program'] = MipsProgram( - **MipsProgram.from_dict(payload.get('program', {})) - ) - else: - payload['program'] = None # command stop doesn't need a program - return DebugMessage(**payload) - except json.JSONDecodeError: + + if ('command' not in payload and + payload['command'] not in Commands): + # Json doesn't contain a valid command nor program return None + if 'program' in payload: + payload['program'] = MipsProgram.from_dict( + payload.get('program', {}) + ) + else: + payload['program'] = None + return DebugMessage(**payload) + class MipsDebugRequestHandler(StreamRequestHandler): """Mips Debug Client Request Handler.""" def respond(self, msg: DebugMessage): """Send response.""" - msg_to_send = msg.dumps().encode('utf8') + msg_to_send = json.dumps(dict(msg)).encode('utf8') self.wfile.write(msg_to_send + b'\r\n\r\n') self.wfile.flush() + print(f"{self.client_address}: Respond {msg}") def receive(self) -> Optional[DebugMessage]: """Receive Client Command.""" - return DebugMessage.loads(self.rfile.readline().strip()) + try: + msg = DebugMessage.from_dict( + json.loads(self.rfile.readline().strip()) + ) + print(f"{self.client_address}: Receive {msg}") + return msg + except json.JSONDecodeError: + return None def handle(self): """Handle Client Req.""" from dashmips.debugger import Commands try: - + print(f"{self.client_address}: Connected") msg = self.receive() if msg is None: - self.respond(BAD_MSG) + self.respond(DebugMessage(**{ + 'command': 'stop', + 'message': 'malformed message', + 'program': None, + 'breakpoints': [], + 'error': True, + })) return # End this party now self.respond(Commands[msg.command](msg)) @@ -97,26 +96,31 @@ def handle(self): 'program': None, 'error': True, }) - self.respond(err_msg) - # Arbitrarily large bad exit code to signal it was serious - self.server.shutdown() - self.server.server_close() - exit(24) + print(f'{fname}:{exc_tb.tb_lineno} {exc_type} {ex}') + try: + self.respond(err_msg) + finally: + # Arbitrarily large bad exit code to signal it was serious + self.server.shutdown() + self.server.server_close() + exit(24) class MipsDebugServer(TCPServer): """Mips Debug Server.""" def __init__(self, - server_address=('localhost', 9999), + server_address=('0.0.0.0', 9999), RequestHandlerClass=MipsDebugRequestHandler, bind_and_activate=True) -> None: """Create Mips Debug Server.""" + self.allow_reuse_address = True super().__init__( server_address, RequestHandlerClass, bind_and_activate ) + print(f"Server is listening on {self.socket.getsockname()}") def server_bind(self): """Set reusable address opt.""" diff --git a/dashmips/hardware.py b/dashmips/hardware.py index b3d4824..91bc418 100644 --- a/dashmips/hardware.py +++ b/dashmips/hardware.py @@ -89,9 +89,9 @@ class Memory(list): def __init__(self, listish=None): """Create 2KB of MIPS RAM.""" self._freespace = 0x4 - if isinstance(listish, bytes) or isinstance(listish, str): - listish = list(a85decode(listish, foldspaces=True)) - elif listish is None: + # if isinstance(listish, bytes) or isinstance(listish, str): + # listish = list(a85decode(listish, foldspaces=True)) + if listish is None: listish = [] else: listish = list(listish) @@ -105,7 +105,8 @@ def __init__(self, listish=None): def __setitem__(self, key, value): """Bounds checking on access.""" if 0x0 == key <= 0x3: - raise Exception('NULL-ish pointer') + from dashmips.mips import MipsException + raise MipsException('NULL-ish pointer') try: for idx, val in enumerate(value): # val &= 0xFF @@ -116,7 +117,7 @@ def __setitem__(self, key, value): return super().__setitem__(key, value) def __repr__(self): - """Compated Memory string.""" + """Compacted Memory string.""" s = '[' zero_ct = 0 for v in self: @@ -138,6 +139,6 @@ def malloc(self, size: int) -> int: self._freespace += size return old_freespace - def encoded_str(self): - """Base85 encoding of memory.""" - return a85encode(bytes(self), foldspaces=True).decode('utf8') + # def encoded_str(self): + # """Base85 encoding of memory.""" + # return a85encode(bytes(self), foldspaces=True).decode('utf8') diff --git a/dashmips/mips.py b/dashmips/mips.py index 566a365..d797617 100644 --- a/dashmips/mips.py +++ b/dashmips/mips.py @@ -5,6 +5,15 @@ import dashmips.directives as directives from dashmips.hardware import Memory + +class MipsException(Exception): + """Mips related errors""" + + def __init__(self, message): + super().__init__(message) + self.message = message + + Directives: Dict[str, Callable[[str, Any, Memory], int]] = { directive: fn for directive, fn in diff --git a/dashmips/preprocessor.py b/dashmips/preprocessor.py index c81992c..e855449 100644 --- a/dashmips/preprocessor.py +++ b/dashmips/preprocessor.py @@ -4,6 +4,7 @@ from dataclasses import dataclass, field, asdict from typing import List, Dict, Any, Tuple, Optional, TextIO +from dashmips.mips import MipsException import dashmips.mips as mips from dashmips.hardware import Memory, Registers @@ -36,40 +37,18 @@ class MipsProgram: memory: Memory = field(default_factory=Memory) registers: Registers = field(default_factory=Registers) - def dump(self, fp, **kwargs): - json.dump(self, fp, cls=MipsProgram.Encoder, **kwargs) - - def dumps(self, **kwargs): - return json.dumps(self, cls=MipsProgram.Encoder, **kwargs) - @staticmethod - def from_dict(prg) -> dict: + def from_dict(prg) -> 'MipsProgram': + """From Basic dictionary to MipsProgram.""" prg['memory'] = Memory(prg['memory']) prg['registers'] = Registers(prg['registers']) prg['labels'] = {ln: Label(**l) for ln, l in prg['labels'].items()} prg['source'] = [SourceLine(**m) for m in prg['source']] - return prg - - @staticmethod - def load(fp): - prg = MipsProgram.from_dict(json.load(fp)) return MipsProgram(**prg) - @staticmethod - def loads(string): - prg = MipsProgram.from_dict(json.loads(string)) - return MipsProgram(**prg) - - class Encoder(json.JSONEncoder): - """JSONEncoder for MipsProgram.""" - - def default(self, obj): - """Object encoder.""" - if isinstance(obj, MipsProgram): - p = asdict(obj) - p['memory'] = p['memory'].encoded_str() - return p - return json.JSONEncoder.default(self, obj) + def __iter__(self): + """Two item iterable for dictionary making.""" + return iter(asdict(self).items()) def preprocess(file: TextIO) -> MipsProgram: @@ -137,7 +116,7 @@ def split_to_sections(code: List[Tuple[int, str]]) -> sectionsType: section = code[0][1] if section is None: - raise Exception("first line must be .text/.data") + raise MipsException("first line must be .text/.data") sections: Dict[str, Any] = {mips.RE.DATA_SEC: [], mips.RE.TEXT_SEC: []} for lineno, line in code: diff --git a/dashmips/run.py b/dashmips/run.py index 9f34ca8..6f243fa 100644 --- a/dashmips/run.py +++ b/dashmips/run.py @@ -1,29 +1,33 @@ """MIPS Runner.""" import re +from dashmips.mips import MipsException from dashmips.instructions import Instructions from dashmips.preprocessor import MipsProgram -def run(program: MipsProgram): +def run(program: MipsProgram, runnable=lambda _: True): """Execute Preprocessed Mips.""" - program.registers['pc'] = program.labels['main'].value - while True: - current_pc = program.registers['pc'] - if len(program.source) < current_pc: - # We jumped or executed beyond available text - raise Exception(f'Bad pc value {current_pc}') - - lineofcode = program.source[current_pc].line # line to execute - instruction = lineofcode.split(' ')[0] # Grab the instruction name - - instruction_fn = Instructions[instruction] # relevant Instruction() - - match = re.match(instruction_fn.regex, lineofcode) - if match: - # Instruction has the correct format - args = instruction_fn.parser(match) - instruction_fn(program, args) - else: - # Bad arguments to instruction - raise Exception(f"{lineofcode} is malformed for {instruction}") + while runnable(program): + next_instruction(program) + + +def next_instruction(program): + current_pc = program.registers['pc'] + if len(program.source) < current_pc: + # We jumped or executed beyond available text + raise MipsException(f'Bad pc value {current_pc}') + + line = program.source[current_pc].line # line to execute + instruction = line.split(' ')[0] # Grab the instruction name + + instruction_fn = Instructions[instruction] # relevant Instruction() + + match = re.match(instruction_fn.regex, line) + if match: + # Instruction has the correct format + args = instruction_fn.parser(match) + instruction_fn(program, args) + else: + # Bad arguments to instruction + raise MipsException(f"{line} is malformed for {instruction}") diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..e0d66d5 --- /dev/null +++ b/setup.py @@ -0,0 +1,14 @@ +"""Setup for Dashmips.""" +from setuptools import setup + +setup( + name='dashmips', + version='0.0.1', + packages=['dashmips'], + long_description=open('README.md').read(), + entry_points={ + 'console_scripts': [ + 'dashmips = dashmips.__main__:main', + ], + } +) diff --git a/test/debugserver_tests.py b/tests/debugserver_tests.py similarity index 100% rename from test/debugserver_tests.py rename to tests/debugserver_tests.py