Skip to content

Commit 51fa52e

Browse files
committed
Init new debugger
1 parent 3e4bcea commit 51fa52e

15 files changed

+1064
-0
lines changed

debugger2/.gitignore

Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
# Byte-compiled / optimized / DLL files
2+
__pycache__/
3+
*.py[cod]
4+
*$py.class
5+
6+
# C extensions
7+
*.so
8+
9+
# Distribution / packaging
10+
.Python
11+
build/
12+
develop-eggs/
13+
dist/
14+
downloads/
15+
eggs/
16+
.eggs/
17+
lib/
18+
lib64/
19+
parts/
20+
sdist/
21+
var/
22+
wheels/
23+
share/python-wheels/
24+
*.egg-info/
25+
.installed.cfg
26+
*.egg
27+
MANIFEST
28+
29+
# PyInstaller
30+
# Usually these files are written by a python script from a template
31+
# before PyInstaller builds the exe, so as to inject date/other infos into it.
32+
*.manifest
33+
*.spec
34+
35+
# Installer logs
36+
pip-log.txt
37+
pip-delete-this-directory.txt
38+
39+
# Unit test / coverage reports
40+
htmlcov/
41+
.tox/
42+
.nox/
43+
.coverage
44+
.coverage.*
45+
.cache
46+
nosetests.xml
47+
coverage.xml
48+
*.cover
49+
*.py,cover
50+
.hypothesis/
51+
.pytest_cache/
52+
cover/
53+
54+
# Translations
55+
*.mo
56+
*.pot
57+
58+
# Django stuff:
59+
*.log
60+
local_settings.py
61+
db.sqlite3
62+
db.sqlite3-journal
63+
64+
# Flask stuff:
65+
instance/
66+
.webassets-cache
67+
68+
# Scrapy stuff:
69+
.scrapy
70+
71+
# Sphinx documentation
72+
docs/_build/
73+
74+
# PyBuilder
75+
.pybuilder/
76+
target/
77+
78+
# Jupyter Notebook
79+
.ipynb_checkpoints
80+
81+
# IPython
82+
profile_default/
83+
ipython_config.py
84+
85+
# pyenv
86+
# For a library or package, you might want to ignore these files since the code is
87+
# intended to run in multiple environments; otherwise, check them in:
88+
# .python-version
89+
90+
# pipenv
91+
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
92+
# However, in case of collaboration, if having platform-specific dependencies or dependencies
93+
# having no cross-platform support, pipenv may install dependencies that don't work, or not
94+
# install all needed dependencies.
95+
#Pipfile.lock
96+
97+
# UV
98+
# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
99+
# This is especially recommended for binary packages to ensure reproducibility, and is more
100+
# commonly ignored for libraries.
101+
#uv.lock
102+
103+
# poetry
104+
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
105+
# This is especially recommended for binary packages to ensure reproducibility, and is more
106+
# commonly ignored for libraries.
107+
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
108+
#poetry.lock
109+
110+
# pdm
111+
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
112+
#pdm.lock
113+
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
114+
# in version control.
115+
# https://pdm.fming.dev/latest/usage/project/#working-with-version-control
116+
.pdm.toml
117+
.pdm-python
118+
.pdm-build/
119+
120+
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
121+
__pypackages__/
122+
123+
# Celery stuff
124+
celerybeat-schedule
125+
celerybeat.pid
126+
127+
# SageMath parsed files
128+
*.sage.py
129+
130+
# Environments
131+
.env
132+
.venv
133+
env/
134+
venv/
135+
ENV/
136+
env.bak/
137+
venv.bak/
138+
139+
# Spyder project settings
140+
.spyderproject
141+
.spyproject
142+
143+
# Rope project settings
144+
.ropeproject
145+
146+
# mkdocs documentation
147+
/site
148+
149+
# mypy
150+
.mypy_cache/
151+
.dmypy.json
152+
dmypy.json
153+
154+
# Pyre type checker
155+
.pyre/
156+
157+
# pytype static type analyzer
158+
.pytype/
159+
160+
# Cython debug symbols
161+
cython_debug/
162+
163+
# PyCharm
164+
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
165+
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
166+
# and can be added to the global gitignore or merged into this file. For a more nuclear
167+
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
168+
#.idea/

debugger2/pytest.ini

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
[pytest]
2+
asyncio_mode = auto
3+
asyncio_default_fixture_loop_scope = "function"
4+
addopts = --doctest-modules -s -v

debugger2/src/debugger/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
from .debugger import Debugger, Frame
2+
from .compile import compile
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
from asyncio import create_subprocess_exec
2+
from asyncio import create_task
3+
from asyncio import Queue
4+
from asyncio import to_thread
5+
from asyncio import Event
6+
from asyncio import iscoroutinefunction
7+
from asyncio.subprocess import PIPE
8+
from contextlib import suppress
9+
from pathlib import Path
10+
import os
11+
12+
from . import mion
13+
14+
15+
def panic(*args, **kwargs):
16+
raise AssertionError(*args, **kwargs)
17+
18+
19+
class BaseDebugger:
20+
def __init__(self) -> None:
21+
do_nothing = lambda *args, **kwargs: None
22+
self.oob_handler = do_nothing
23+
self.inferior_handler = do_nothing
24+
self._inferior_dispatch_done = Event()
25+
self._inferior_dispatch_done.set()
26+
self._did_init = False
27+
28+
async def init(self, executable_path: str | Path) -> None:
29+
self.fd_master, self.fd_slave = os.openpty()
30+
self.process = await create_subprocess_exec(
31+
"gdb",
32+
"--interpreter=mi4",
33+
"--quiet",
34+
"-nx",
35+
"-nh",
36+
"--tty",
37+
os.ttyname(self.fd_slave),
38+
"--args",
39+
str(executable_path),
40+
stdin=PIPE,
41+
stdout=PIPE,
42+
)
43+
44+
self.result_queue = Queue[tuple[str, dict]]()
45+
create_task(self._stdout_dispatch())
46+
create_task(self._inferior_dispatch())
47+
self._did_init = True
48+
return self
49+
50+
async def deinit(self) -> None:
51+
if not self._did_init:
52+
return
53+
54+
self.process.stdin.write_eof() # or write `-gdb-exit`
55+
await self.process.stdin.drain()
56+
await self.process.wait()
57+
58+
self.result_queue.shutdown()
59+
await self.result_queue.join()
60+
61+
os.close(self.fd_master)
62+
os.close(self.fd_slave)
63+
await self._inferior_dispatch_done.wait()
64+
65+
async def run_command(self, command: str):
66+
self.process.stdin.write(f"{command}\n".encode())
67+
await self.process.stdin.drain()
68+
69+
subkind, result = await self.result_queue.get()
70+
self.result_queue.task_done()
71+
assert (
72+
subkind in mion.RESULT_CLASS
73+
), f"Command '{command}' returned unexpected status {subkind}"
74+
return result
75+
76+
def on_oob[F](self, func: F) -> F:
77+
"""oob = out of band"""
78+
self.oob_handler = func
79+
return func
80+
81+
def on_inferior[F](self, func: F) -> F:
82+
"""inferior = inferior output"""
83+
self.inferior_handler = func
84+
return func
85+
86+
async def _stdout_dispatch(self) -> None:
87+
while line := await self.process.stdout.readline():
88+
line = line.strip().decode()
89+
if line == "(gdb)":
90+
continue
91+
92+
kind, message = line[:1], line[1:]
93+
match kind:
94+
case mion.RESULT:
95+
subkind, message = _split_subkind(message)
96+
await self.result_queue.put((subkind, mion.loads(message)))
97+
case _ if kind in mion.ASYNC:
98+
subkind, message = _split_subkind(message)
99+
if iscoroutinefunction(self.oob_handler):
100+
await self.oob_handler((subkind, mion.loads(message)))
101+
else:
102+
self.oob_handler((subkind, mion.loads(message)))
103+
case _ if kind in mion.STREAM:
104+
if iscoroutinefunction(self.oob_handler):
105+
await self.oob_handler(message)
106+
else:
107+
self.oob_handler(message)
108+
case _:
109+
panic(f"Received unknown message from GDB: {kind}")
110+
111+
async def _inferior_dispatch(self) -> None:
112+
self._inferior_dispatch_done.clear()
113+
with suppress(OSError):
114+
while output := await to_thread(os.read, self.fd_master, 512):
115+
if iscoroutinefunction(self.inferior_handler):
116+
await self.inferior_handler(output.decode())
117+
else:
118+
self.inferior_handler(output.decode())
119+
self._inferior_dispatch_done.set()
120+
121+
122+
def _split_subkind(message: str) -> tuple[str, str]:
123+
"""
124+
>>> _split_subkind('abc')
125+
('abc', '')
126+
>>> _split_subkind('abc,def')
127+
('abc', 'def')
128+
>>> _split_subkind('abc,def,ghi')
129+
('abc', 'def,ghi')
130+
"""
131+
132+
if "," in message:
133+
return tuple(message.split(",", 1))
134+
return message, ""

debugger2/src/debugger/compile.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
from asyncio import create_subprocess_exec
2+
from asyncio.subprocess import PIPE
3+
from pathlib import Path
4+
5+
6+
async def compile(source_path: str | Path, output_path: str | Path) -> None:
7+
clang = await create_subprocess_exec(
8+
"clang",
9+
str(source_path),
10+
"-o",
11+
str(output_path),
12+
"-g",
13+
"-O0",
14+
# "-Wall",
15+
# "-Wextra",
16+
# "-Werror",
17+
stdin=PIPE,
18+
stdout=PIPE,
19+
stderr=PIPE,
20+
)
21+
stdout, stderr = await clang.communicate()
22+
exit_code = await clang.wait()
23+
assert exit_code == 0, ("Failed to compile source code", stderr)

0 commit comments

Comments
 (0)