├── src └── idd │ ├── py.typed │ ├── __init__.py │ ├── ui │ ├── __init__.py │ ├── figlet_text.py │ ├── scroll_window.py │ ├── scrollable_area.py │ ├── diff_area.py │ ├── header.py │ └── footer.py │ ├── debuggers │ ├── __init__.py │ ├── gdb │ │ ├── __init__.py │ │ ├── utils.py │ │ ├── idd_gdb_controller.py │ │ ├── gdb_commands.py │ │ ├── gdb_driver.py │ │ └── gdb_mi_driver.py │ └── lldb │ │ ├── __init__.py │ │ ├── lldb_commands.py │ │ ├── lldb_controller.py │ │ ├── lldb_new_driver.py │ │ ├── lldb_extensions.py │ │ └── lldb_driver.py │ ├── __main__.py │ ├── driver.py │ ├── layout.tcss │ ├── diff_driver.py │ ├── differ.py │ └── cli.py ├── test ├── pointer_offset │ └── program.cxx ├── vectors │ └── program.cxx ├── simple │ └── program.cxx ├── recursive_factorial │ └── program.cxx ├── local_vars_switched │ └── program.cxx ├── strace_sample │ └── program.cxx ├── recursive_fib │ └── program.cxx ├── local_vars_address │ └── program.cxx ├── dynamic_vars_address │ └── program.cxx ├── df_dynamic_vars_address │ └── program.cxx ├── compile_tests.py ├── pointer_simple_class │ └── program.cxx ├── recursive_str │ └── program.cxx └── simple_functions_chain │ └── program.cxx ├── pyproject.toml ├── .github └── workflows │ └── release.yml ├── images └── discord.svg ├── README.md ├── .gitignore ├── uv.lock └── LICENSE /src/idd/py.typed: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/idd/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/idd/ui/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/idd/debuggers/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/idd/debuggers/gdb/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/idd/debuggers/lldb/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/idd/__main__.py: -------------------------------------------------------------------------------- 1 | from idd.cli import main 2 | 3 | if __name__ == "__main__": 4 | main() 5 | -------------------------------------------------------------------------------- /test/pointer_offset/program.cxx: -------------------------------------------------------------------------------- 1 | // g++ -DV1 -o a/program.out -xc++ -g program.cxx 2 | // g++ -DV2 -o b/program.out -xc++ -g program.cxx 3 | 4 | #include 5 | 6 | int a3(int i = 3) { 7 | int * j = new int(2); 8 | int * k = new int(7); 9 | std::cout << "Ptr:" << j << std::endl; 10 | 11 | return 0; 12 | } 13 | 14 | 15 | int main() 16 | { 17 | return a3(); 18 | } 19 | -------------------------------------------------------------------------------- /test/vectors/program.cxx: -------------------------------------------------------------------------------- 1 | // g++ -DV1 -o a/program.out -xc++ -g program.cxx 2 | // g++ -DV2 -o b/program.out -xc++ -g program.cxx 3 | 4 | static char array[1000][1000]; 5 | 6 | int main (void) 7 | { 8 | int i, j; 9 | 10 | for (i = 0; i < 1000; i++) 11 | for (j = 0; j < 1000; j++) 12 | #if V1 13 | array[i][j]++; 14 | #else 15 | array[j][i]++; 16 | #endif 17 | return 0; 18 | } -------------------------------------------------------------------------------- /test/simple/program.cxx: -------------------------------------------------------------------------------- 1 | // g++ -DV1 -o a/program.out -xc++ -g program.cxx 2 | // g++ -DV2 -o b/program.out -xc++ -g program.cxx 3 | 4 | #include 5 | 6 | int f(int arg1, int arg2) { 7 | int var1 = 1; 8 | #if V1 9 | int var2 = 2; 10 | #else 11 | int var2 = 3; 12 | #endif 13 | return arg1 + arg2 + var1 + var2; 14 | } 15 | 16 | int main() 17 | { 18 | std::cout << "Result:" << f(1, 2) << std::endl; 19 | return 0; 20 | } 21 | -------------------------------------------------------------------------------- /test/recursive_factorial/program.cxx: -------------------------------------------------------------------------------- 1 | // g++ -DV1 -o a/program.out -xc++ -g program.cxx 2 | // g++ -DV2 -o b/program.out -xc++ -g program.cxx 3 | 4 | #include 5 | 6 | int f(int arg1) { 7 | if (arg1 == 0) 8 | #if V1 9 | return 1; 10 | #else 11 | return 0; 12 | #endif 13 | else 14 | return arg1 * f(arg1 - 1); 15 | } 16 | 17 | int main() 18 | { 19 | std::cout << "Result:" << f(5) << std::endl; 20 | return 0; 21 | } 22 | -------------------------------------------------------------------------------- /test/local_vars_switched/program.cxx: -------------------------------------------------------------------------------- 1 | //g++ -DV1 -o a/program.out -xc++ -g program.cxx 2 | //g++ -DV2 -o b/program.out -xc++ -g program.cxx 3 | 4 | #include 5 | 6 | int a(int arg1, int arg2) { 7 | #if V1 8 | int a_var1 = 1; 9 | int a_var2 = 2; 10 | #else 11 | int a_var2 = 2; 12 | int a_var1 = 1; 13 | #endif 14 | return a_var1 + a_var2 + arg1 + arg2; 15 | } 16 | 17 | int main() 18 | { 19 | std::cout << "Result:" << a(1, 2) << std::endl; 20 | return a(1, 2); 21 | } 22 | -------------------------------------------------------------------------------- /test/strace_sample/program.cxx: -------------------------------------------------------------------------------- 1 | // g++ -DV1 -o a/program.out -xc++ -g program.cxx 2 | // g++ -DV2 -o b/program.out -xc++ -g program.cxx 3 | 4 | #include 5 | #include 6 | 7 | void file_open() 8 | { 9 | char str[30]; 10 | #if V1 11 | FILE *fp = fopen("/tmp/strace_sample", "r"); 12 | #else 13 | FILE *fp = fopen("strace_sample", "r"); 14 | #endif 15 | fgets(str, 5, fp); 16 | fclose(fp); 17 | } 18 | 19 | int main() { 20 | file_open(); 21 | return 0; 22 | } 23 | -------------------------------------------------------------------------------- /src/idd/ui/figlet_text.py: -------------------------------------------------------------------------------- 1 | from rich.console import Console, ConsoleOptions, RenderResult 2 | 3 | class FigletText: 4 | """A renderable to generate figlet text that adapts to fit the container.""" 5 | 6 | def __init__(self, text: str) -> None: 7 | self.text = text 8 | 9 | def __rich_console__( 10 | self, console: Console, options: ConsoleOptions 11 | ) -> RenderResult: 12 | """Build a Rich renderable to render the Figlet text.""" 13 | return self.text 14 | -------------------------------------------------------------------------------- /test/recursive_fib/program.cxx: -------------------------------------------------------------------------------- 1 | // g++ -DV1 -o a/program.out -xc++ -g program.cxx 2 | // g++ -DV2 -o b/program.out -xc++ -g program.cxx 3 | 4 | #include 5 | 6 | int fib(int x) { 7 | #if V1 8 | if (x < 2) { 9 | return x; 10 | } 11 | #else 12 | if (x <= 1) { 13 | return 1; 14 | } 15 | #endif 16 | return (fib(x - 1) + fib(x - 2)); 17 | } 18 | 19 | int main() 20 | { 21 | for (int i = 0; i <= 5; i++) 22 | std::cout << "Result fib(" << i << "):" << fib(i) << std::endl; 23 | return 0; 24 | } 25 | -------------------------------------------------------------------------------- /test/local_vars_address/program.cxx: -------------------------------------------------------------------------------- 1 | // g++ -DV1 -o a/program.out -xc++ -g program.cxx 2 | // g++ -DV2 -o b/program.out -xc++ -g program.cxx 3 | 4 | #include 5 | 6 | int f(int arg1, int arg2) { 7 | #if V1 8 | int var1 = 1; 9 | int var2 = 2; 10 | #else 11 | int var2 = 2; 12 | int var1 = 1; 13 | #endif 14 | std::cout << "var1_ptr:" << &var1 << ", var2_ptr:" << &var2 << std::endl; 15 | return var1 + var2 + arg1 + arg2; 16 | } 17 | 18 | int main() 19 | { 20 | std::cout << "Result:" << f(1, 2) << std::endl; 21 | return 0; 22 | } 23 | -------------------------------------------------------------------------------- /src/idd/driver.py: -------------------------------------------------------------------------------- 1 | import abc 2 | from abc import abstractmethod 3 | 4 | class Driver(metaclass=abc.ABCMeta): 5 | @abstractmethod 6 | def get_state(self, target): raise NotImplementedError 7 | 8 | @abstractmethod 9 | def run_single_command(self, command, target): raise NotImplementedError 10 | 11 | @abstractmethod 12 | def run_parallel_command(self, command): raise NotImplementedError 13 | 14 | @abstractmethod 15 | def terminate(self): raise NotImplementedError 16 | 17 | 18 | class IDDParallelTerminate: 19 | pass 20 | -------------------------------------------------------------------------------- /test/dynamic_vars_address/program.cxx: -------------------------------------------------------------------------------- 1 | // g++ -DV1 -o a/program.out -xc++ -g program.cxx 2 | // g++ -DV2 -o b/program.out -xc++ -g program.cxx 3 | 4 | #include 5 | 6 | int f(int arg1, int arg2) { 7 | #if V1 8 | int *var1 = new int(1); 9 | int *var2 = new int(2); 10 | #else 11 | int *var2 = new int(2); 12 | int *var1 = new int(1); 13 | #endif 14 | std::cout << "var1_ptr:" << var1 << ", var2_ptr:" << var2 << std::endl; 15 | return *var1 + *var2 + arg1 + arg2; 16 | } 17 | 18 | int main() 19 | { 20 | std::cout << "Result:" << f(1, 2) << std::endl; 21 | return 0; 22 | } 23 | -------------------------------------------------------------------------------- /test/df_dynamic_vars_address/program.cxx: -------------------------------------------------------------------------------- 1 | // g++ -DV1 -o a/program.out -xc++ -g program.cxx 2 | // g++ -DV2 -o b/program.out -xc++ -g program.cxx 3 | 4 | #include 5 | 6 | int f(int arg1, int arg2) { 7 | #if V2 8 | int i; 9 | #endif 10 | 11 | int var1 = arg1 + 1; 12 | int var2 = arg2 + 2; 13 | 14 | #if V1 15 | return var1 + var2; 16 | #else 17 | int result; 18 | for (i = 0; i < 5; i++) { 19 | result = var1 + var2; 20 | } 21 | 22 | return result; 23 | #endif 24 | 25 | } 26 | 27 | int main() 28 | { 29 | std::cout << "Result:" << f(1, 2) << std::endl; 30 | return 0; 31 | } 32 | -------------------------------------------------------------------------------- /src/idd/layout.tcss: -------------------------------------------------------------------------------- 1 | Screen { 2 | layout: vertical; 3 | height: 100%; 4 | } 5 | 6 | TextScrollView { 7 | border: solid green; 8 | } 9 | 10 | .row1 { 11 | height: 20%; 12 | } 13 | 14 | .row2 { 15 | height: 30%; 16 | } 17 | 18 | .diff-asm-reg { 19 | height: 10%; 20 | } 21 | 22 | .row3, .row5 { 23 | height: 5%; 24 | } 25 | 26 | .row4 { 27 | height: 40%; 28 | } 29 | 30 | .base_only_row1 { 31 | height: 20% 32 | } 33 | 34 | .base_only_row2 { 35 | height: 30% 36 | } 37 | 38 | .base_only_row3 { 39 | height: 44% 40 | } 41 | 42 | .base_only_row4 { 43 | height: 6% 44 | } 45 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["hatchling"] 3 | build-backend = "hatchling.build" 4 | 5 | [project] 6 | name = "idd" 7 | version = "1.1.0" 8 | description = "A tool for performing interactive dynamic differential debugging capable to identify functional and performance regressions." 9 | readme = "README.md" 10 | authors = [ 11 | { name = "Martin Vassilev" }, 12 | { name = "Vassil Vassilev" }, 13 | { name = "Alexander Penev" }, 14 | ] 15 | requires-python = ">=3.9" 16 | dependencies = [ 17 | "pygdbmi", 18 | "rich", 19 | "textual", 20 | ] 21 | 22 | 23 | [project.scripts] 24 | idd = "idd.cli:main" 25 | -------------------------------------------------------------------------------- /src/idd/ui/scroll_window.py: -------------------------------------------------------------------------------- 1 | from textual.widgets import ScrollView 2 | 3 | class ScrollWindow(ScrollView): 4 | prev = "" 5 | 6 | async def set_text(self, text_input, should_append = True): 7 | pre_y = self.y 8 | if should_append: 9 | prev = "\n===============================\n" 10 | prev += "\n".join(e for e in text_input) 11 | self.prev += prev 12 | else: 13 | self.prev = "\n".join(e for e in text_input) 14 | 15 | await self.update(self.prev) 16 | self.y = pre_y 17 | self.animate("y", self.window.virtual_size.height, duration=1, easing="linear") 18 | -------------------------------------------------------------------------------- /test/compile_tests.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import subprocess 4 | from pathlib import Path 5 | 6 | root_tests_dir = Path(__file__).parent.resolve() 7 | prog = "g++" 8 | 9 | for test_dir in root_tests_dir.iterdir(): 10 | if test_dir.is_dir(): 11 | print(test_dir) 12 | a_dir = test_dir / "a" 13 | b_dir = test_dir / "b" 14 | 15 | a_dir.mkdir(exist_ok=True) 16 | b_dir.mkdir(exist_ok=True) 17 | 18 | test_files = list(test_dir.glob('*.c??')) 19 | subprocess.run([prog, "-DV1", "-o", a_dir/"program.out", "-g", *test_files], check=True) 20 | subprocess.run([prog, "-DV2", "-o", b_dir/"program.out", "-g", *test_files], check=True) 21 | -------------------------------------------------------------------------------- /src/idd/diff_driver.py: -------------------------------------------------------------------------------- 1 | from idd.differ import Differ 2 | 3 | class DiffDriver: 4 | def get_diff(self, a, b, typ): 5 | d = Differ() 6 | diffs = [] 7 | if typ == "base": 8 | diffs = [x for x in d.compare(a, b) if x[0] in ('-', '?', ' ')] 9 | elif typ == "regressed": 10 | diffs = [x for x in d.compare(b, a) if x[0] in ('+', '?', ' ')] 11 | 12 | # for i in range(0, len(diffs)): 13 | # if diffs[i][0] == "-": 14 | # diffs[i] = "
" + diffs[i] + "
" 15 | # elif diffs[i][0] == "+": 16 | # diffs[i] = "
" + diffs[i] + "
" 17 | # elif diffs[i][0] == " ": 18 | # diffs[i] = "
" + diffs[i] + "
" 19 | return diffs 20 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: PyPI Upload 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*.*.*' 7 | 8 | jobs: 9 | dist: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v4 13 | - uses: hynek/build-and-inspect-python-package@v2 14 | 15 | publish: 16 | needs: [dist] 17 | environment: pypi 18 | permissions: 19 | id-token: write 20 | attestations: write 21 | runs-on: ubuntu-latest 22 | 23 | steps: 24 | - uses: actions/download-artifact@v4 25 | with: 26 | name: Packages 27 | path: dist 28 | 29 | - name: Generate artifact attestation for sdist and wheel 30 | uses: actions/attest-build-provenance@v2 31 | with: 32 | subject-path: "dist/*" 33 | 34 | - uses: pypa/gh-action-pypi-publish@release/v1 35 | -------------------------------------------------------------------------------- /src/idd/ui/scrollable_area.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | from textual.widgets import RichLog 3 | 4 | 5 | class TextScrollView(RichLog): 6 | COMPONENT_CLASSES = { 7 | "box", # Class for the main text container 8 | "rich-text--line", # Class for each line of text 9 | } 10 | DEFAULT_CSS = """ 11 | TextScrollView { 12 | height: 100%; 13 | scrollbar-size: 1 1; 14 | } 15 | """ 16 | 17 | def __init__(self, title: str = "", component_id: str = None) -> None: 18 | super().__init__(name=title, auto_scroll=True, markup=True) 19 | self.border_title = title 20 | if component_id: 21 | self.id = component_id 22 | 23 | def append(self, lines: list[str]): 24 | self.write("\n".join(lines)) 25 | 26 | def text(self, lines: list[str]): 27 | self.clear() 28 | self.append(lines) 29 | -------------------------------------------------------------------------------- /src/idd/ui/diff_area.py: -------------------------------------------------------------------------------- 1 | from rich.align import Align 2 | from rich.console import RenderableType 3 | from rich.panel import Panel 4 | 5 | from textual.reactive import Reactive 6 | from textual.widget import Widget 7 | 8 | from ui.figlet_text import FigletText 9 | 10 | class DiffArea(Widget): 11 | """The general widget for displaying diff data.""" 12 | 13 | value = Reactive("0") 14 | widget_title = "title" 15 | 16 | def __init__(self, title ="title", value = "val", component_id: str = None): 17 | super().__init__() 18 | self.widget_title = title 19 | self.value = value 20 | 21 | if component_id: 22 | self.id = component_id 23 | 24 | def render(self) -> RenderableType: 25 | return Panel( 26 | Align.left(FigletText(self.value), vertical="top"), 27 | title = self.widget_title, 28 | style="white on rgb(51,51,51)" 29 | ) 30 | -------------------------------------------------------------------------------- /src/idd/ui/header.py: -------------------------------------------------------------------------------- 1 | from rich.style import Style 2 | from rich.table import Table 3 | 4 | from textual.widgets import Header 5 | from textual import events 6 | 7 | class Header(Header): 8 | """Override the default Header for Styling""" 9 | 10 | def __init__(self) -> None: 11 | super().__init__() 12 | self.tall = False 13 | self.style = Style(color="white", bgcolor="rgb(98,98,98)") 14 | 15 | def render(self) -> Table: 16 | header_table = Table.grid(padding=(0, 1), expand=True) 17 | header_table.add_column(justify="left", ratio=0, width=8) 18 | header_table.add_column("title", justify="center", ratio=1) 19 | header_table.add_column("clock", justify="right", width=8) 20 | header_table.add_row( 21 | "IDD" 22 | ) 23 | return header_table 24 | 25 | async def on_click(self, event: events.Click) -> None: 26 | return await super().on_click(event) 27 | -------------------------------------------------------------------------------- /test/pointer_simple_class/program.cxx: -------------------------------------------------------------------------------- 1 | // g++ -DV1 -o a/program.out -xc++ -g program.cxx 2 | // g++ -DV2 -o b/program.out -xc++ -g program.cxx 3 | 4 | #include 5 | 6 | class Task { 7 | public: 8 | int id; 9 | Task *next; 10 | Task(int i, Task *n): 11 | id(i), 12 | next(n) 13 | {} 14 | }; 15 | 16 | int a3(int i = 3) { 17 | int * j = new int(2); 18 | int * k = new int(7); 19 | std::cout << "Ptr:" << j << std::endl; 20 | 21 | return 0; 22 | } 23 | 24 | 25 | int main() 26 | { 27 | Task *task_head = new Task(-1, NULL); 28 | Task *task1 = new Task(1, NULL); 29 | //Task *task2 = new Task(2, NULL); 30 | //Task *task3 = new Task(3, NULL); 31 | //Task *task4 = new Task(4, NULL); 32 | //Task *task5 = new Task(5, NULL); 33 | 34 | task_head->next = task1; 35 | //task1->next = task2; 36 | //task2->next = task3; 37 | //task3->next = task4; 38 | //task4->next = task5; 39 | 40 | return 0; 41 | } 42 | -------------------------------------------------------------------------------- /test/recursive_str/program.cxx: -------------------------------------------------------------------------------- 1 | // g++ -DV1 -o a/program.out -xc++ -g program.cxx 2 | // g++ -DV2 -o b/program.out -xc++ -g program.cxx 3 | 4 | #include 5 | #include 6 | 7 | std::string dup(int cnt, std::string x) { 8 | #if V1 9 | if (cnt <= 0) { 10 | return ""; 11 | } else { 12 | return x + dup(cnt - 1, x); 13 | } 14 | #else 15 | if (cnt <= 0) { 16 | return ""; 17 | } else { 18 | std::string s = dup(cnt/2, x); 19 | return (cnt%1 == 0) ? s + s : s + s + x; // cnt%2 == 0 20 | } 21 | #endif 22 | } 23 | 24 | int main() 25 | { 26 | std::cout << "Result(0):" << dup(0, "*") << std::endl; 27 | std::cout << "Result(1):" << dup(1, "*") << std::endl; 28 | std::cout << "Result(2):" << dup(2, "*") << std::endl; 29 | std::cout << "Result(3):" << dup(3, "*") << std::endl; 30 | std::cout << "Result(4):" << dup(4, "*") << std::endl; 31 | std::cout << "Result(5):" << dup(5, "*") << std::endl; 32 | std::cout << "Result(6):" << dup(6, "*") << std::endl; 33 | std::cout << "Result(200):" << dup(200, "*") << std::endl; 34 | return 0; 35 | } 36 | -------------------------------------------------------------------------------- /test/simple_functions_chain/program.cxx: -------------------------------------------------------------------------------- 1 | // g++ -DV1 -o a/program.out -xc++ -g program.cxx 2 | // g++ -DV2 -o b/program.out -xc++ -g program.cxx 3 | 4 | #include 5 | 6 | int c(int arg1, int arg2) { 7 | int c_var1 = 100; 8 | int c_var2; 9 | #if V1 10 | c_var2 = 2100; 11 | #else 12 | c_var2 = 2000; 13 | #endif 14 | int c_var3 = 300; 15 | int c_var4 = 400; 16 | int c_var5 = 500; 17 | return c_var4 + c_var5; 18 | } 19 | 20 | int b(int arg1, int arg2) { 21 | int b_var1 = 10; 22 | int b_var2; 23 | #if V1 24 | b_var2 = 210; 25 | #else 26 | b_var2 = 200; 27 | #endif 28 | int b_var3 = 30; 29 | int b_var4 = 40; 30 | int b_var5 = 50; 31 | #if V1 32 | return c(b_var4, b_var5); 33 | #else 34 | return c(b_var3, b_var4); 35 | #endif 36 | } 37 | 38 | int a(int arg1, int arg2) { 39 | int a_var1 = 1; 40 | int a_var2; 41 | #if V1 42 | a_var2 = 21; 43 | #else 44 | a_var2 = 20; 45 | #endif 46 | int a_var3 = 3; 47 | int a_var4 = 4; 48 | int a_var5 = 5; 49 | return b(a_var4, a_var5); 50 | } 51 | 52 | int main() 53 | { 54 | std::cout << "Result:" << a(1, 2) << std::endl; 55 | return 0; 56 | } 57 | -------------------------------------------------------------------------------- /src/idd/ui/footer.py: -------------------------------------------------------------------------------- 1 | from rich.text import Text 2 | 3 | from textual.widgets import Footer 4 | 5 | class Footer(Footer): 6 | """Override the default Footer for Styling""" 7 | 8 | def make_key_text(self) -> Text: 9 | """Create text containing all the keys.""" 10 | text = Text( 11 | style="white on rgb(98,98,98)", 12 | no_wrap=True, 13 | overflow="ellipsis", 14 | justify="left", 15 | end="", 16 | ) 17 | for binding in self.app.bindings.shown_keys: 18 | key_display = ( 19 | binding.key.upper() 20 | if binding.key_display is None 21 | else binding.key_display 22 | ) 23 | hovered = self.highlight_key == binding.key 24 | key_text = Text.assemble( 25 | (f" {key_display} ", "reverse" if hovered else "default on default"), 26 | f" {binding.description} ", 27 | meta={"@click": f"app.press('{binding.key}')", "key": binding.key}, 28 | ) 29 | text.append_text(key_text) 30 | return text 31 | -------------------------------------------------------------------------------- /src/idd/debuggers/gdb/utils.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | num_tab_spaces = 4 4 | main_regex_pattern = r'^\S+' 5 | digits_regex_pattern = r'^\d+' 6 | 7 | def parse_gdb_line(input_string): 8 | input_string = clean_gdb_string(input_string) 9 | starting_integers_match = re.search(digits_regex_pattern, input_string) 10 | 11 | if starting_integers_match: 12 | # Get the matched starting integers 13 | starting_integers = starting_integers_match.group(0) 14 | 15 | # Define a regular expression pattern to match the rest of the string 16 | rest_of_string_pattern = rf'^{starting_integers}(.*)$' 17 | 18 | # Find the rest of the string after the starting integers 19 | rest_of_string_match = re.search(rest_of_string_pattern, input_string) 20 | 21 | if rest_of_string_match: 22 | # Get the matched rest of the string 23 | rest_of_string = rest_of_string_match.group(1) 24 | #rest_of_string = clean_gdb_string(rest_of_string) 25 | return '{} {}'.format(starting_integers, rest_of_string) 26 | else: 27 | return 'err' 28 | 29 | # Nothing to parse here 30 | return input_string 31 | 32 | def clean_gdb_string(input_string): 33 | input_string = input_string.replace('\\t', '' * num_tab_spaces) 34 | input_string = input_string.replace('\\\\n', '') 35 | input_string = input_string.replace('\\n', '') 36 | input_string = input_string.replace('\n', '') 37 | input_string = input_string.replace('\\"', '\"') 38 | input_string = input_string.replace('\"', '"') 39 | input_string = input_string.replace('\\', '') 40 | 41 | return input_string -------------------------------------------------------------------------------- /images/discord.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Introduction 2 | 3 | IDD is a tool for performing interactive dynamic differential debugging capable to identify functional and performance regressions. 4 | 5 | ## :beginner: About 6 | 7 | IDD loads two versions of the same application. The first one is the base version that works as expected while the second version of the same program has a regression introduced. IDD inspects the two versions of the applications using external tools like gdb and lldb. The two applications are executed side by side and the user is allowed to dispatch commands to the underlying debuggers in order to expect their internal states and isolate the origin of the regression. 8 | 9 | ## :rocket: Demo 10 | ![idd](https://github.com/compiler-research/idd/assets/7579600/dac1b3c6-44f0-48b2-a19d-92eb5f1d973f) 11 | 12 | ## :zap: Usage 13 | Write about how to use this project. 14 | 15 | `python -m idd -c gdb -ba -ra ` 16 | 17 | ### :electric_plug: Installation 18 | - Steps on how to install this project on Ubuntu 22.04 19 | 20 | -- Creating new environment: 21 | ``` 22 | $ python3 -m venv iddenv 23 | $ source iddenv/bin/activate 24 | ``` 25 | 26 | 27 | -- Installing required packages: 28 | 29 | ``` 30 | $ pip install -e. 31 | ``` 32 | 33 | ## :cherry_blossom: Community 34 | 35 | Join our discord for discussions and collaboration. 36 | 37 | 38 | 39 | 40 | ### :fire: Contribution 41 | 42 | Your contributions are always welcome and appreciated. Following are the things you can do to contribute to this project. 43 | 44 | 1. **Report a bug**
45 | If you think you have encountered a bug, and I should know about it, feel free to report it [here](https://github.com/compiler-research/idd/issues) and we could take care of it. 46 | 47 | 2. **Request a feature**
48 | You can also request for a feature [here](https://github.com/compiler-research/idd/issues), and if it will viable, it will be picked for development. 49 | 50 | 3. **Create a pull request**
51 | It can't get better then this, your pull request will be appreciated by the community. You can get started by picking up any open issues from [here]() and make a pull request. 52 | 53 | ## Cite 54 | ```bibtex 55 | @article{vassilev2020idd, 56 | title={IDD--a platform enabling differential debugging}, 57 | author={Vassilev, Martin and Vassilev, Vassil and Penev, Alexander}, 58 | journal={Cybernetics and Information Technologies}, 59 | volume={20}, 60 | number={1}, 61 | pages={53--67}, 62 | year={2020} 63 | } 64 | ``` 65 | 66 | ## Issues 67 | 1. ~~Support entering commands to a specific analyzer.~~ 68 | 2. ~~Make panels scrollable~~ 69 | 3. Make panels configurable 70 | -------------------------------------------------------------------------------- /src/idd/debuggers/lldb/lldb_commands.py: -------------------------------------------------------------------------------- 1 | import lldb 2 | import sys 3 | import os 4 | import time 5 | from six import StringIO as SixStringIO 6 | import six 7 | import lldb_utils 8 | import json 9 | 10 | special_commands = ["pframe", "plocals", "pargs", "plist"] 11 | 12 | def get_stacktrace(debugger, args, result, internal_dict): 13 | debugger.SetAsync(False) 14 | 15 | target = debugger.GetSelectedTarget() 16 | process = target.GetProcess() 17 | thread = process.GetSelectedThread() 18 | 19 | result = lldb_utils.print_stacktrace(thread) 20 | 21 | print(result) 22 | 23 | def get_locals(debugger, args, result, internal_dict): 24 | debugger.SetAsync(False) 25 | 26 | target = debugger.GetSelectedTarget() 27 | process = target.GetProcess() 28 | thread = process.GetSelectedThread() 29 | frame = thread.GetSelectedFrame() 30 | 31 | 32 | print(args) 33 | #args = lldb_utils.get_locals_as_string(frame) 34 | #print(args) 35 | 36 | def get_args(debugger, args, result, internal_dict): 37 | debugger.SetAsync(False) 38 | 39 | target = debugger.GetSelectedTarget() 40 | process = target.GetProcess() 41 | thread = process.GetSelectedThread() 42 | frame = thread.GetSelectedFrame() 43 | 44 | args = lldb_utils.get_args_as_string(frame) 45 | print(args) 46 | 47 | def print_list(debugger, args, result, internal_dict): 48 | try: 49 | f_result = lldb.SBCommandReturnObject() 50 | debugger.GetCommandInterpreter().HandleCommand("f", f_result) 51 | f_result = f_result.__str__().split("\n") 52 | 53 | current_line = f_result[6] 54 | current_line_num = current_line[3:] 55 | current_line_num = current_line_num.partition(" ")[0] 56 | 57 | raw_listing = lldb.SBCommandReturnObject() 58 | debugger.GetCommandInterpreter().HandleCommand("list " + current_line_num, raw_listing) 59 | 60 | listing = raw_listing.__str__().split("\n") 61 | 62 | for i in range(0, len(listing)): 63 | if listing[i].startswith(" " + current_line_num): 64 | listing[i] = "--> " + listing[i] 65 | 66 | listing = '\n'.join(str(x) for x in listing) 67 | 68 | result = { "line_num" : current_line_num, "entries" : listing } 69 | result = json.dumps(result) 70 | 71 | print(result) 72 | print("end_command") 73 | except: 74 | print("exception") 75 | print("end_command") 76 | 77 | def run_wrapper(debugger, args, result, internal_dict): 78 | debugger.SetAsync(False) 79 | 80 | try: 81 | command_result = lldb.SBCommandReturnObject() 82 | debugger.GetCommandInterpreter().HandleCommand(''.join(str(x) for x in args), command_result) 83 | 84 | print(command_result) 85 | print("end_command") 86 | except: 87 | print("exception") 88 | print("end_command") 89 | 90 | def __lldb_init_module(debugger, internal_dict): 91 | debugger.HandleCommand('command script add -f lldb_commands.get_locals plocals') 92 | debugger.HandleCommand('command script add -f lldb_commands.get_args pargs') 93 | debugger.HandleCommand('command script add -f lldb_commands.get_stacktrace pframe') 94 | debugger.HandleCommand('command script add -f lldb_commands.print_list plist') 95 | debugger.HandleCommand('command script add -f lldb_commands.run_wrapper run-wrapper') -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 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 | # poetry 98 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 99 | # This is especially recommended for binary packages to ensure reproducibility, and is more 100 | # commonly ignored for libraries. 101 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 102 | #poetry.lock 103 | 104 | # pdm 105 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 106 | #pdm.lock 107 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 108 | # in version control. 109 | # https://pdm.fming.dev/#use-with-ide 110 | .pdm.toml 111 | 112 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 113 | __pypackages__/ 114 | 115 | # Celery stuff 116 | celerybeat-schedule 117 | celerybeat.pid 118 | 119 | # SageMath parsed files 120 | *.sage.py 121 | 122 | # Environments 123 | .env 124 | .venv 125 | env/ 126 | venv/ 127 | ENV/ 128 | env.bak/ 129 | venv.bak/ 130 | 131 | # Spyder project settings 132 | .spyderproject 133 | .spyproject 134 | 135 | # Rope project settings 136 | .ropeproject 137 | 138 | # mkdocs documentation 139 | /site 140 | 141 | # mypy 142 | .mypy_cache/ 143 | .dmypy.json 144 | dmypy.json 145 | 146 | # Pyre type checker 147 | .pyre/ 148 | 149 | # pytype static type analyzer 150 | .pytype/ 151 | 152 | # Cython debug symbols 153 | cython_debug/ 154 | 155 | # PyCharm 156 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 157 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 158 | # and can be added to the global gitignore or merged into this file. For a more nuclear 159 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 160 | #.idea/ 161 | 162 | /test/*/a/* 163 | /test/*/b/* 164 | -------------------------------------------------------------------------------- /src/idd/debuggers/lldb/lldb_controller.py: -------------------------------------------------------------------------------- 1 | import os, sys, pty, tty, select, threading 2 | import platform 3 | import lldb 4 | from lldb import eLaunchFlagStopAtEntry 5 | 6 | class IDDLLDBController: 7 | def __init__(self, exe="", pid=None): 8 | self.exe = exe 9 | self.pid = pid 10 | self.debugger = lldb.SBDebugger.Create() 11 | self.debugger.SetAsync(False) 12 | self.debugger.SetUseColor(False) 13 | 14 | error = lldb.SBError() 15 | self.target = self.debugger.CreateTarget(exe, platform.machine(), "host", True, error) 16 | if not error.Success(): 17 | raise Exception(error.GetCString()) 18 | 19 | # self.master_fd, self.slave_fd = pty.openpty() 20 | # self.slave_name = os.ttyname(self.slave_fd) 21 | # tty.setraw(self.master_fd) 22 | 23 | # self.debuggee_output = [] 24 | # self._start_output_stream_thread() 25 | 26 | # self.target = self.debugger.CreateTarget(self.exe) 27 | # if not self.target.IsValid(): 28 | # raise Exception("Failed to create target") 29 | 30 | # self._launch_process() 31 | 32 | def _launch_process(self): 33 | launch_info = lldb.SBLaunchInfo([]) 34 | # launch_info.SetArguments(["./tmp/main.py"], True) 35 | # launch_info.SetWorkingDirectory(os.getcwd()) 36 | launch_info.SetLaunchFlags(eLaunchFlagStopAtEntry) 37 | 38 | launch_info.AddOpenFileAction(0, self.slave_name, read=True, write=False) # stdin 39 | launch_info.AddOpenFileAction(1, self.slave_name, read=False, write=True) # stdout 40 | launch_info.AddOpenFileAction(2, self.slave_name, read=False, write=True) # stderr 41 | 42 | error = lldb.SBError() 43 | self.process = self.target.Launch(launch_info, error) 44 | 45 | if not error.Success(): 46 | raise Exception(f"Launch failed: {error.GetCString()}") 47 | 48 | def _start_output_stream_thread(self): 49 | def stream_output(): 50 | while True: 51 | try: 52 | r, _, _ = select.select([self.master_fd], [], [], 0.1) 53 | if r: 54 | data = os.read(self.master_fd, 1024).decode(errors="replace") 55 | self.debuggee_output.append(data) 56 | except OSError: 57 | break 58 | 59 | self.output_thread = threading.Thread(target=stream_output, daemon=True) 60 | self.output_thread.start() 61 | 62 | # def write(self, command: str): 63 | # """Send LLDB command as if typed in interactive shell.""" 64 | # if not command.endswith("\n"): 65 | # command += "\n" 66 | # os.write(self.master_fd, command.encode()) 67 | 68 | def run_lldb_command(self, command: str): 69 | result = lldb.SBCommandReturnObject() 70 | self.debugger.GetCommandInterpreter().HandleCommand(command, result) 71 | if result.Succeeded(): 72 | return result.GetOutput().splitlines() 73 | return result.GetError().splitlines() 74 | 75 | 76 | def send_input_to_debuggee(self, text): 77 | """Send input to the debugged process' stdin.""" 78 | # if not text.endswith("\n"): 79 | # text += "\n" 80 | # os.write(self.master_fd, text.encode()) 81 | self.target.GetProcess().PutSTDIN(text) 82 | 83 | def get_debuggee_output(self): 84 | """Return all captured debuggee output so far.""" 85 | # os.set_blocking(self.master_fd, False) 86 | # try: 87 | # content = os.read(self.master_fd, 1024 * 1024 * 10).decode(errors="replace") 88 | # except BlockingIOError: 89 | # content = "" 90 | # return content.splitlines() 91 | return (self.target.GetProcess().GetSTDOUT(1024*1024*10) + self.target.GetProcess().GetSTDERR(1024*1024*10)).splitlines() 92 | 93 | # def pop_output(self): 94 | # """Return and clear debuggee output buffer.""" 95 | # output = self.debuggee_output 96 | # self.debuggee_output = [] 97 | # return output 98 | 99 | def terminate(self): 100 | # if self.process: 101 | # self.process.Kill() 102 | # os.close(self.master_fd) 103 | # os.close(self.slave_fd) 104 | lldb.SBDebugger.Destroy(self.debugger) 105 | -------------------------------------------------------------------------------- /src/idd/debuggers/lldb/lldb_new_driver.py: -------------------------------------------------------------------------------- 1 | from idd.driver import Driver 2 | from idd.debuggers.lldb.lldb_controller import IDDLLDBController 3 | from idd.debuggers.lldb.lldb_extensions import * 4 | from concurrent.futures import ThreadPoolExecutor 5 | 6 | 7 | class LLDBNewDriver(Driver): 8 | def __init__( 9 | self, base_exe=None, base_pid=None, regressed_exe=None, regressed_pid=None 10 | ): 11 | self.base_controller = IDDLLDBController(exe=base_exe) 12 | self.regressed_controller = IDDLLDBController(exe=regressed_exe) 13 | 14 | def run_single_command(self, command, target): 15 | if target == "base": 16 | result = self.base_controller.run_lldb_command(command) 17 | return result 18 | elif target == "regressed": 19 | result = self.regressed_controller.run_lldb_command(command) 20 | return result 21 | 22 | def run_parallel_command(self, command): 23 | with ThreadPoolExecutor() as executor: 24 | base_future = executor.submit(self.run_single_command, command, "base") 25 | regressed_future = executor.submit( 26 | self.run_single_command, command, "regressed" 27 | ) 28 | return { 29 | "base": base_future.result(), 30 | "regressed": regressed_future.result(), 31 | } 32 | 33 | def insert_stdin(self, text): 34 | self.base_controller.send_input_to_debuggee(text) 35 | self.regressed_controller.send_input_to_debuggee(text) 36 | 37 | def insert_stdin_single(self, text, target): 38 | if target == "base": 39 | self.base_controller.send_input_to_debuggee(text) 40 | elif target == "regressed": 41 | self.regressed_controller.send_input_to_debuggee(text) 42 | 43 | def get_state(self, target=None): 44 | if target == "base": 45 | return { 46 | "stack_frames": self.get_current_stack_frames(self.base_controller), 47 | "locals": self.get_current_local_vars(self.base_controller, None), 48 | "args": self.get_current_args(self.base_controller), 49 | "instructions": self.get_current_instructions(self.base_controller), 50 | "registers": self.get_current_registers(self.base_controller), 51 | } 52 | if target == "regressed": 53 | return { 54 | "stack_frames": self.get_current_stack_frames(self.regressed_controller), 55 | "locals": self.get_current_local_vars(self.regressed_controller, None), 56 | "args": self.get_current_args(self.regressed_controller), 57 | "instructions": self.get_current_instructions(self.regressed_controller), 58 | "registers": self.get_current_registers(self.regressed_controller), 59 | } 60 | 61 | with ThreadPoolExecutor() as executor: 62 | base_future = executor.submit(self.get_state, "base") 63 | regressed_future = executor.submit(self.get_state, "regressed") 64 | return { 65 | "base": base_future.result(), 66 | "regressed": regressed_future.result(), 67 | } 68 | 69 | def get_console_output(self, target=None): 70 | if target == "base": 71 | return self.base_controller.get_debuggee_output() 72 | if target == "regressed": 73 | return self.regressed_controller.get_debuggee_output() 74 | 75 | with ThreadPoolExecutor() as executor: 76 | base_future = executor.submit(self.get_console_output, "base") 77 | regressed_future = executor.submit(self.get_console_output, "regressed") 78 | return { 79 | "base": base_future.result(), 80 | "regressed": regressed_future.result(), 81 | } 82 | 83 | def get_current_stack_frames(self, controller): 84 | target = controller.debugger.GetTargetAtIndex(0) 85 | return get_current_stack_frame_from_target(target) or [] 86 | 87 | def get_current_args(self, controller): 88 | target = controller.debugger.GetTargetAtIndex(0) 89 | return get_args_as_list(target) or [] 90 | 91 | def get_current_local_vars(self, controller, filters): 92 | target = controller.debugger.GetTargetAtIndex(0) 93 | locals = get_local_vars_as_list(target) 94 | if filters == "ignore-order-declaration": 95 | locals.sort() 96 | return locals or [] 97 | 98 | def get_current_instructions(self, controller): 99 | target = controller.debugger.GetTargetAtIndex(0) 100 | return get_instructions_as_list(target) or [] 101 | 102 | def get_current_registers(self, controller): 103 | target = controller.debugger.GetTargetAtIndex(0) 104 | return get_registers_as_list(target) or [] 105 | 106 | def terminate(self): 107 | self.base_controller.terminate() 108 | self.regressed_controller.terminate() 109 | -------------------------------------------------------------------------------- /src/idd/debuggers/lldb/lldb_extensions.py: -------------------------------------------------------------------------------- 1 | def get_current_stack_frame_from_target(target): 2 | process = target.GetProcess() 3 | thread = process.GetSelectedThread() 4 | 5 | result = [] 6 | for frame in thread: 7 | function = frame.GetFunction() 8 | addr = frame.GetPCAddress() 9 | load_addr = addr.GetLoadAddress(target) 10 | function = frame.GetFunction() 11 | mod_name = frame.GetModule().GetFileSpec().GetFilename() 12 | 13 | if not function: 14 | # No debug info for 'function'. 15 | symbol = frame.GetSymbol() 16 | if not symbol: 17 | continue 18 | file_addr = addr.GetFileAddress() 19 | start_addr = symbol.GetStartAddress().GetFileAddress() 20 | symbol_name = symbol.GetName() 21 | symbol_offset = file_addr - start_addr 22 | result.append(' frame #{num}: {addr:#016x} {mod}`{symbol} + {offset}'.format( 23 | num=frame.GetFrameID(), addr=load_addr, mod=mod_name, symbol=symbol_name, offset=symbol_offset)) 24 | else: 25 | # Debug info is available for 'function'. 26 | func_name = frame.GetFunctionName() 27 | file_name = frame.GetLineEntry().GetFileSpec().GetFilename() 28 | line_num = frame.GetLineEntry().GetLine() 29 | result.append(' frame #{num}: {addr:#016x} {mod}`{func} at {file}:{line} {args}'.format( 30 | num=frame.GetFrameID(), addr=load_addr, mod=mod_name, 31 | func='%s [inlined]' % func_name if frame.IsInlined() else func_name, 32 | file=file_name, line=line_num, args=get_args_as_string(target))) 33 | # result.append(str(frame)) 34 | 35 | return result 36 | 37 | def get_args_as_list(target): 38 | process = target.GetProcess() 39 | thread = process.GetSelectedThread() 40 | args = [] 41 | 42 | for frame in thread: 43 | if frame.GetFrameID() == 0: 44 | vars = frame.GetVariables(True, False, False, True) 45 | for var in vars: 46 | args.append("(%s)%s=%s" % (var.GetTypeName(), 47 | var.GetName(), 48 | var.GetValue())) 49 | 50 | return args 51 | 52 | def get_args_as_string(target): 53 | process = target.GetProcess() 54 | thread = process.GetSelectedThread() 55 | args = [] 56 | 57 | for frame in thread: 58 | if frame.GetFrameID() == 0: 59 | vars = frame.GetVariables(True, False, False, True) 60 | for var in vars: 61 | args.append("(%s)%s=%s" % (var.GetTypeName(), 62 | var.GetName(), 63 | var.GetValue())) 64 | 65 | return ' '.join(str(x) for x in args) 66 | 67 | def get_local_vars_as_list(target): 68 | process = target.GetProcess() 69 | thread = process.GetSelectedThread() 70 | locals = [] 71 | 72 | for frame in thread: 73 | if frame.GetFrameID() == 0: 74 | vars = frame.GetVariables(False, True, False, True) 75 | for var in vars: 76 | locals.append("(%s)%s=%s" % (var.GetTypeName(), 77 | var.GetName(), 78 | var.GetValue())) 79 | 80 | return locals 81 | 82 | def get_instructions_as_list(target): 83 | process = target.GetProcess() 84 | thread = process.GetSelectedThread() 85 | instructions = [] 86 | for frame in thread: 87 | if frame.GetFrameID() == 0: 88 | symbol = frame.GetSymbol() 89 | if symbol.GetName() == "_class_initialize": 90 | return 91 | start_address = symbol.GetStartAddress().GetLoadAddress(target) 92 | end_address = symbol.GetEndAddress().GetLoadAddress(target) 93 | instruction_list = symbol.GetInstructions(target) 94 | previous_breakpoint_address = 0 95 | current_instruction_address = frame.GetPC() 96 | for i in instruction_list: 97 | address = i.GetAddress() 98 | load_address = address.GetLoadAddress(target) 99 | mnemonic = i.GetMnemonic(target) 100 | operand = i.GetOperands(target) 101 | if current_instruction_address == address: 102 | instructions.append("---> %s %s" % (mnemonic, operand)) 103 | else: 104 | instructions.append("%s %s" % (mnemonic, operand)) 105 | 106 | return instructions 107 | 108 | def get_registers_as_list(target): 109 | process = target.GetProcess() 110 | thread = process.GetSelectedThread() 111 | registers = [] 112 | for frame in thread: 113 | if frame.GetFrameID() == 0: 114 | regs = frame.GetRegisters()[0] 115 | for reg in regs: 116 | registers.append('%s => %s' % (reg.GetName(), reg.GetValue())) 117 | 118 | return registers 119 | 120 | def get_call_instructions(target): 121 | process = target.GetProcess() 122 | thread = process.GetSelectedThread() 123 | call_instructions = {} 124 | for frame in thread: 125 | if frame.GetFrameID() == 0: 126 | symbol = frame.GetSymbol() 127 | instruction_list = symbol.GetInstructions(target) 128 | for i in instruction_list: 129 | address = i.GetAddress().GetLoadAddress(target) 130 | mnemonic = i.GetMnemonic(target) 131 | if mnemonic is not None and mnemonic.startswith('call'): 132 | jmp_destination = int(i.GetOperands(target), 16) 133 | call_instructions[address] = jmp_destination 134 | 135 | return call_instructions -------------------------------------------------------------------------------- /src/idd/debuggers/lldb/lldb_driver.py: -------------------------------------------------------------------------------- 1 | import os 2 | import lldb 3 | 4 | from idd.driver import Driver, IDDParallelTerminate 5 | from idd.debuggers.lldb.lldb_extensions import * 6 | from multiprocessing import Process, Pipe 7 | 8 | 9 | processes = [] 10 | 11 | 12 | class LLDBGetState: 13 | pass 14 | 15 | 16 | class LLDBStdin: 17 | def __init__(self, text: str): 18 | self.text = text 19 | 20 | 21 | class LLDBDebugger: 22 | is_initted = False 23 | 24 | lldb_instance = None 25 | 26 | command_interpreter = None 27 | 28 | lldb_instances = None 29 | 30 | def __init__(self, exe="", pid=None): 31 | self.lldb_instance = lldb.SBDebugger.Create() 32 | self.lldb_instance.SetAsync(False) 33 | self.lldb_instance.SetUseColor(False) 34 | 35 | self.command_interpreter = self.lldb_instance.GetCommandInterpreter() 36 | 37 | if exe != "": 38 | error = lldb.SBError() 39 | self.target = self.lldb_instance.CreateTarget(exe, "x86_64", "host", True, error) 40 | if not error.Success(): 41 | raise Exception(error.GetCString()) 42 | 43 | launch_info = lldb.SBLaunchInfo(None) 44 | launch_info.SetExecutableFile(self.target.GetExecutable(), True) 45 | elif pid is not None: 46 | self.run_single_command("attach -p " + str(pid)) 47 | 48 | dirname = os.path.dirname(__file__) 49 | self.run_single_command("command script import " + os.path.join(dirname, "lldb_commands.py")) 50 | 51 | self.is_initted = True 52 | 53 | def run_single_command(self, command, *_): 54 | command_result = lldb.SBCommandReturnObject() 55 | self.command_interpreter.HandleCommand(command, command_result) 56 | 57 | if command_result.Succeeded(): 58 | return command_result.GetOutput().split("\n") 59 | else: 60 | return command_result.GetError().split("\n") 61 | 62 | def get_state(self, *_): 63 | return { 64 | 'stack_frames': self.get_current_stack_frames(), 65 | 'locals': self.get_current_local_vars(None), 66 | 'args': self.get_current_args(), 67 | 'instructions': self.get_current_instructions(), 68 | 'registers': self.get_current_registers(), 69 | } 70 | 71 | def get_current_stack_frames(self): 72 | target = self.lldb_instance.GetTargetAtIndex(0) 73 | stack_frame = get_current_stack_frame_from_target(target) 74 | return stack_frame 75 | 76 | def get_current_args(self): 77 | target = self.lldb_instance.GetTargetAtIndex(0) 78 | args = get_args_as_list(target) 79 | return args 80 | 81 | def get_current_local_vars(self, filters): 82 | target = self.lldb_instance.GetTargetAtIndex(0) 83 | target_locals = get_local_vars_as_list(target) 84 | if filters == 'ignore-order-declaration': 85 | target_locals.sort() 86 | return target_locals 87 | 88 | def get_current_instructions(self): 89 | target = self.lldb_instance.GetTargetAtIndex(0) 90 | args = get_instructions_as_list(target) 91 | return args 92 | 93 | def get_current_registers(self): 94 | target = self.lldb_instance.GetTargetAtIndex(0) 95 | args = get_registers_as_list(target) 96 | return args 97 | 98 | def get_current_calls(self): 99 | target = self.lldb_instance.GetTargetAtIndex(0) 100 | calls = get_call_instructions(target) 101 | return calls 102 | 103 | def insert_stdin(self, text: str): 104 | self.target.GetProcess().PutSTDIN(text) 105 | 106 | def terminate(self): 107 | return 108 | 109 | @staticmethod 110 | def run(lldb_args, pipe): 111 | lldb = LLDBDebugger(*lldb_args) 112 | while True: 113 | args, kwargs = pipe.recv() 114 | if isinstance(args, IDDParallelTerminate) or isinstance(kwargs, IDDParallelTerminate): 115 | return 116 | if isinstance(args, LLDBGetState) or isinstance(kwargs, LLDBGetState): 117 | res = lldb.get_state() 118 | pipe.send(res) 119 | elif isinstance(args, LLDBStdin): 120 | lldb.insert_stdin(args.text) 121 | else: 122 | res = lldb.run_single_command(*args, **kwargs) 123 | stdout = lldb.target.GetProcess().GetSTDOUT(1024 * 1024 * 10) 124 | if stdout: 125 | res.extend(stdout.split("\n")) 126 | stderr = lldb.target.GetProcess().GetSTDERR(1024 * 1024 * 10) 127 | if stderr: 128 | res.extend(stderr.split("\n")) 129 | pipe.send(res) 130 | 131 | 132 | 133 | class LLDBParallelDebugger(Driver): 134 | def __init__(self, base_args="", base_pid=None, regression_args="", regression_pid=None): 135 | self.base_pipe = create_LLDBDebugger_for_parallel(base_args, base_pid) 136 | self.regressed_pipe = create_LLDBDebugger_for_parallel(regression_args, regression_pid) 137 | 138 | def get_state(self, target=None): 139 | if target == "base": 140 | self.base_pipe.send((LLDBGetState(), LLDBGetState())) 141 | return self.base_pipe.recv() 142 | if target == "regressed": 143 | self.regressed_pipe.send((LLDBGetState(), LLDBGetState())) 144 | return self.regressed_pipe.recv() 145 | 146 | self.base_pipe.send((LLDBGetState(), LLDBGetState())) 147 | self.regressed_pipe.send((LLDBGetState(), LLDBGetState())) 148 | 149 | return { 150 | "base": self.base_pipe.recv(), 151 | "regressed": self.regressed_pipe.recv(), 152 | } 153 | 154 | def run_single_command(self, command, target): 155 | if target == "base": 156 | self.base_pipe.send(((command,), {})) 157 | return self.base_pipe.recv() 158 | if target == "regressed": 159 | self.regressed_pipe.send(((command,), {})) 160 | return self.regressed_pipe.recv() 161 | 162 | def run_parallel_command(self, command): 163 | self.base_pipe.send(((command,), {})) 164 | self.regressed_pipe.send(((command,), {})) 165 | 166 | return { 167 | "base": self.base_pipe.recv(), 168 | "regressed": self.regressed_pipe.recv(), 169 | } 170 | 171 | def insert_stdin(self, text: str): 172 | text = LLDBStdin(text) 173 | self.base_pipe.send((text, text)) 174 | self.regressed_pipe.send((text, text)) 175 | 176 | def insert_stdin_single(self, text: str, target: str): 177 | text = LLDBStdin(text) 178 | if target == "base": 179 | self.base_pipe.send((text, text)) 180 | if target == "regressed": 181 | self.regressed_pipe.send((text, text)) 182 | 183 | def terminate(self): 184 | terminate_all_IDDGdbController() 185 | 186 | 187 | def terminate_all_IDDGdbController(): 188 | for _, pipe in processes: 189 | pipe.send((IDDParallelTerminate(), IDDParallelTerminate())) 190 | for process, _ in processes: 191 | process.join() 192 | 193 | def create_LLDBDebugger_for_parallel(*args): 194 | global processes 195 | 196 | parent_conn, child_conn = Pipe() 197 | process = Process(target=LLDBDebugger.run, args=(args, child_conn)) 198 | processes.append((process, parent_conn)) 199 | process.start() 200 | return parent_conn 201 | -------------------------------------------------------------------------------- /src/idd/debuggers/gdb/idd_gdb_controller.py: -------------------------------------------------------------------------------- 1 | import os 2 | import logging 3 | import select 4 | import fcntl 5 | import pty 6 | import signal 7 | import threading 8 | import time 9 | 10 | from idd.driver import IDDParallelTerminate 11 | from idd.debuggers.gdb.utils import parse_gdb_line 12 | 13 | from pygdbmi.gdbcontroller import GdbController 14 | 15 | logger = logging.getLogger(__name__) 16 | 17 | DEFAULT_GDB_LAUNCH_COMMAND = ["gdb", "--nx", "--quiet", "--interpreter=mi3"] 18 | 19 | class IDDGdbController(GdbController): 20 | script_file_path = None 21 | 22 | def __init__(self, base_args="", base_pid=None, script_file_path=None): 23 | def make_fd_nonblocking(fd): 24 | flags = fcntl.fcntl(fd, fcntl.F_GETFL) 25 | fcntl.fcntl(fd, fcntl.F_SETFL, flags | os.O_NONBLOCK) 26 | 27 | self.pid = None 28 | self.script_file_path = script_file_path 29 | self.base_args = base_args 30 | self.base_pid = base_pid 31 | self.gdb_command = DEFAULT_GDB_LAUNCH_COMMAND[:] 32 | self.buffer = "" 33 | self.debuggee_output_buffer = [] 34 | self.debuggee_stream_thread = None 35 | 36 | # Create PTYs 37 | self.master_fd, self.slave_fd = pty.openpty() 38 | self.debuggee_master_fd, self.debuggee_slave_fd = pty.openpty() 39 | self.debuggee_tty = os.ttyname(self.debuggee_slave_fd) 40 | 41 | make_fd_nonblocking(self.master_fd) 42 | make_fd_nonblocking(self.debuggee_master_fd) 43 | 44 | 45 | if base_args: 46 | self.gdb_command.append(base_args) 47 | elif base_pid: 48 | self.gdb_command.append(f"--pid={base_pid}") 49 | 50 | self.spawn_new_gdb_subprocess() 51 | self.start_debuggee_output_streaming() 52 | 53 | def spawn_new_gdb_subprocess(self): 54 | if hasattr(self, "pid") and self.pid: 55 | self.terminate() 56 | 57 | logger.info("Launching GDB: " + " ".join(self.gdb_command)) 58 | 59 | self.pid = os.fork() 60 | if self.pid == 0: 61 | os.setsid() 62 | os.dup2(self.slave_fd, 0) 63 | os.dup2(self.slave_fd, 1) 64 | os.dup2(self.slave_fd, 2) 65 | os.close(self.master_fd) 66 | os.execvp(self.gdb_command[0], self.gdb_command) 67 | else: 68 | os.close(self.slave_fd) 69 | 70 | # Wait for initial GDB prompt to ensure GDB is ready 71 | self.read_until_prompt(timeout=2.0) 72 | 73 | # Assign inferior TTY to forward debuggee I/O 74 | self.write(f"set inferior-tty {self.debuggee_tty}") 75 | 76 | # Optional: read again to confirm it's accepted 77 | self.read_until_prompt(timeout=1.0) 78 | 79 | logger.info(f"GDB pid: {self.pid}") 80 | 81 | 82 | def write(self, command): 83 | os.write(self.master_fd, (command + "\n").encode()) 84 | 85 | def read(self): 86 | try: 87 | new_data = os.read(self.master_fd, 4096).decode(errors="replace") 88 | self.buffer += new_data 89 | if "\n" in self.buffer: 90 | lines = self.buffer.split("\n") 91 | self.buffer = lines[-1] 92 | return "\n".join(lines[:-1]) 93 | except BlockingIOError: 94 | return None 95 | except OSError: 96 | return None 97 | return None 98 | 99 | def read_until_prompt(self, timeout=0.5, grace_period=0.2) -> str: 100 | """ 101 | Accumulates GDB output until the final (gdb) prompt is seen and 102 | no new output appears for a short grace period. 103 | """ 104 | output = "" 105 | start_time = time.time() 106 | last_gdb_prompt_time = None 107 | 108 | while True: 109 | chunk = self.read() 110 | if chunk: 111 | output += chunk 112 | if "(gdb) " in chunk: 113 | last_gdb_prompt_time = time.time() 114 | 115 | now = time.time() 116 | 117 | # Check for grace period after seeing (gdb) 118 | if last_gdb_prompt_time is not None: 119 | if now - last_gdb_prompt_time >= grace_period: 120 | break 121 | 122 | if now - start_time > timeout: 123 | logger.warning("read_until_prompt() timed out") 124 | break 125 | 126 | time.sleep(0.09) 127 | 128 | return output 129 | 130 | 131 | def flush_debuggee_output(self): 132 | try: 133 | while True: 134 | if os.read(self.debuggee_master_fd, 4096): 135 | continue 136 | break 137 | except (BlockingIOError, OSError): 138 | pass 139 | 140 | self.debuggee_output_buffer = [] 141 | 142 | 143 | def read_debuggee_output(self): 144 | data = "" 145 | try: 146 | while True: 147 | chunk = os.read(self.debuggee_master_fd, 4096).decode(errors="replace") 148 | if not chunk: 149 | break 150 | data += chunk 151 | except BlockingIOError: 152 | pass # No more data to read for now 153 | except OSError: 154 | return None # Debuggee might have terminated 155 | 156 | return data if data else None 157 | 158 | def start_debuggee_output_streaming(self): 159 | def stream(): 160 | logger.debug("[debuggee stream] started") 161 | while self.is_gdb_alive(): 162 | output = self.read_debuggee_output() 163 | if output: 164 | self.debuggee_output_buffer.append(output) 165 | logger.debug(f"[debuggee] {output.strip()}") 166 | time.sleep(0.01) # Light pause to prevent tight spin 167 | 168 | logger.debug("[debuggee stream] terminated") 169 | 170 | self.debuggee_stream_thread = threading.Thread(target=stream, daemon=True) 171 | self.debuggee_stream_thread.start() 172 | 173 | def is_gdb_alive(self): 174 | try: 175 | if self.pid: 176 | os.kill(self.pid, 0) 177 | return True 178 | except OSError: 179 | return False 180 | return False 181 | 182 | def send_input_to_debuggee(self, user_input): 183 | try: 184 | os.write(self.debuggee_master_fd, (user_input + "\n").encode()) 185 | logger.debug(f"[send_input_to_debuggee] Sent: {user_input}") 186 | except OSError as e: 187 | logger.error(f"[send_input_to_debuggee] Failed to write input: {e}") 188 | 189 | def get_debuggee_output(self): 190 | return self.debuggee_output_buffer 191 | 192 | def pop_debuggee_output(self): 193 | output = self.debuggee_output_buffer 194 | self.debuggee_output_buffer = [] 195 | return output 196 | 197 | def is_waiting_for_input(self): 198 | """Detect if the debugged process is waiting for user input based on its output.""" 199 | if not self.debuggee_output_buffer: 200 | return False 201 | 202 | # Check last few lines of debuggee output 203 | recent_output = "".join(self.debuggee_output_buffer).splitlines()[-5:] 204 | for line in recent_output: 205 | line = line.strip().lower() 206 | if line.endswith(":") or line.endswith("?"): 207 | return True 208 | if any(keyword in line for keyword in ["cin", "scanf", "input", "enter value", "enter", "read"]): 209 | return True 210 | return False 211 | 212 | 213 | def terminate(self): 214 | if hasattr(self, "pid") and self.pid: 215 | logger.info(f"Terminating GDB process (PID {self.pid})") 216 | os.kill(self.pid, signal.SIGTERM) 217 | try: 218 | os.waitpid(self.pid, 0) 219 | except ChildProcessError: 220 | pass 221 | self.pid = None 222 | -------------------------------------------------------------------------------- /uv.lock: -------------------------------------------------------------------------------- 1 | version = 1 2 | requires-python = ">=3.9" 3 | 4 | [[package]] 5 | name = "idd" 6 | version = "0.1.0" 7 | source = { editable = "." } 8 | dependencies = [ 9 | { name = "pygdbmi" }, 10 | { name = "textual" }, 11 | ] 12 | 13 | [package.metadata] 14 | requires-dist = [ 15 | { name = "pygdbmi" }, 16 | { name = "textual" }, 17 | ] 18 | 19 | [[package]] 20 | name = "linkify-it-py" 21 | version = "2.0.3" 22 | source = { registry = "https://pypi.org/simple" } 23 | dependencies = [ 24 | { name = "uc-micro-py" }, 25 | ] 26 | sdist = { url = "https://files.pythonhosted.org/packages/2a/ae/bb56c6828e4797ba5a4821eec7c43b8bf40f69cda4d4f5f8c8a2810ec96a/linkify-it-py-2.0.3.tar.gz", hash = "sha256:68cda27e162e9215c17d786649d1da0021a451bdc436ef9e0fa0ba5234b9b048", size = 27946 } 27 | wheels = [ 28 | { url = "https://files.pythonhosted.org/packages/04/1e/b832de447dee8b582cac175871d2f6c3d5077cc56d5575cadba1fd1cccfa/linkify_it_py-2.0.3-py3-none-any.whl", hash = "sha256:6bcbc417b0ac14323382aef5c5192c0075bf8a9d6b41820a2b66371eac6b6d79", size = 19820 }, 29 | ] 30 | 31 | [[package]] 32 | name = "markdown-it-py" 33 | version = "3.0.0" 34 | source = { registry = "https://pypi.org/simple" } 35 | dependencies = [ 36 | { name = "mdurl" }, 37 | ] 38 | sdist = { url = "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", size = 74596 } 39 | wheels = [ 40 | { url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528 }, 41 | ] 42 | 43 | [package.optional-dependencies] 44 | linkify = [ 45 | { name = "linkify-it-py" }, 46 | ] 47 | plugins = [ 48 | { name = "mdit-py-plugins" }, 49 | ] 50 | 51 | [[package]] 52 | name = "mdit-py-plugins" 53 | version = "0.4.2" 54 | source = { registry = "https://pypi.org/simple" } 55 | dependencies = [ 56 | { name = "markdown-it-py" }, 57 | ] 58 | sdist = { url = "https://files.pythonhosted.org/packages/19/03/a2ecab526543b152300717cf232bb4bb8605b6edb946c845016fa9c9c9fd/mdit_py_plugins-0.4.2.tar.gz", hash = "sha256:5f2cd1fdb606ddf152d37ec30e46101a60512bc0e5fa1a7002c36647b09e26b5", size = 43542 } 59 | wheels = [ 60 | { url = "https://files.pythonhosted.org/packages/a7/f7/7782a043553ee469c1ff49cfa1cdace2d6bf99a1f333cf38676b3ddf30da/mdit_py_plugins-0.4.2-py3-none-any.whl", hash = "sha256:0c673c3f889399a33b95e88d2f0d111b4447bdfea7f237dab2d488f459835636", size = 55316 }, 61 | ] 62 | 63 | [[package]] 64 | name = "mdurl" 65 | version = "0.1.2" 66 | source = { registry = "https://pypi.org/simple" } 67 | sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729 } 68 | wheels = [ 69 | { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979 }, 70 | ] 71 | 72 | [[package]] 73 | name = "platformdirs" 74 | version = "4.3.6" 75 | source = { registry = "https://pypi.org/simple" } 76 | sdist = { url = "https://files.pythonhosted.org/packages/13/fc/128cc9cb8f03208bdbf93d3aa862e16d376844a14f9a0ce5cf4507372de4/platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907", size = 21302 } 77 | wheels = [ 78 | { url = "https://files.pythonhosted.org/packages/3c/a6/bc1012356d8ece4d66dd75c4b9fc6c1f6650ddd5991e421177d9f8f671be/platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb", size = 18439 }, 79 | ] 80 | 81 | [[package]] 82 | name = "pygdbmi" 83 | version = "0.11.0.0" 84 | source = { registry = "https://pypi.org/simple" } 85 | sdist = { url = "https://files.pythonhosted.org/packages/2a/d0/d386ad42b12b90e60293c56a3b793910f34aa21c63f7ddc8a857e498d458/pygdbmi-0.11.0.0.tar.gz", hash = "sha256:7a286be2fcf25650d9f66e11adc46e972cf078a466864a700cd44739ad261fb0", size = 24648 } 86 | wheels = [ 87 | { url = "https://files.pythonhosted.org/packages/c8/4b/71df806f4d260ddf01f9e431f5a6538a4155db3ec84a131d7e087178c591/pygdbmi-0.11.0.0-py3-none-any.whl", hash = "sha256:f7cac28e1d558927444c880ed1e65da1a5d8686121a3aac16f42fb84d3ceb60d", size = 21258 }, 88 | ] 89 | 90 | [[package]] 91 | name = "pygments" 92 | version = "2.18.0" 93 | source = { registry = "https://pypi.org/simple" } 94 | sdist = { url = "https://files.pythonhosted.org/packages/8e/62/8336eff65bcbc8e4cb5d05b55faf041285951b6e80f33e2bff2024788f31/pygments-2.18.0.tar.gz", hash = "sha256:786ff802f32e91311bff3889f6e9a86e81505fe99f2735bb6d60ae0c5004f199", size = 4891905 } 95 | wheels = [ 96 | { url = "https://files.pythonhosted.org/packages/f7/3f/01c8b82017c199075f8f788d0d906b9ffbbc5a47dc9918a945e13d5a2bda/pygments-2.18.0-py3-none-any.whl", hash = "sha256:b8e6aca0523f3ab76fee51799c488e38782ac06eafcf95e7ba832985c8e7b13a", size = 1205513 }, 97 | ] 98 | 99 | [[package]] 100 | name = "rich" 101 | version = "13.9.4" 102 | source = { registry = "https://pypi.org/simple" } 103 | dependencies = [ 104 | { name = "markdown-it-py" }, 105 | { name = "pygments" }, 106 | { name = "typing-extensions", marker = "python_full_version < '3.11'" }, 107 | ] 108 | sdist = { url = "https://files.pythonhosted.org/packages/ab/3a/0316b28d0761c6734d6bc14e770d85506c986c85ffb239e688eeaab2c2bc/rich-13.9.4.tar.gz", hash = "sha256:439594978a49a09530cff7ebc4b5c7103ef57baf48d5ea3184f21d9a2befa098", size = 223149 } 109 | wheels = [ 110 | { url = "https://files.pythonhosted.org/packages/19/71/39c7c0d87f8d4e6c020a393182060eaefeeae6c01dab6a84ec346f2567df/rich-13.9.4-py3-none-any.whl", hash = "sha256:6049d5e6ec054bf2779ab3358186963bac2ea89175919d699e378b99738c2a90", size = 242424 }, 111 | ] 112 | 113 | [[package]] 114 | name = "textual" 115 | version = "0.85.2" 116 | source = { registry = "https://pypi.org/simple" } 117 | dependencies = [ 118 | { name = "markdown-it-py", extra = ["linkify", "plugins"] }, 119 | { name = "platformdirs" }, 120 | { name = "rich" }, 121 | { name = "typing-extensions" }, 122 | ] 123 | sdist = { url = "https://files.pythonhosted.org/packages/71/69/8b2c90ef5863b67f2adb067772b259412130a10c7080e1fede39c6245f73/textual-0.85.2.tar.gz", hash = "sha256:2a416995c49d5381a81d0a6fd23925cb0e3f14b4f239ed05f35fa3c981bb1df2", size = 1462599 } 124 | wheels = [ 125 | { url = "https://files.pythonhosted.org/packages/e9/f0/29bd25c7cd53f2b53bc0205a936b5d3a37c88a70bb91037c939d313d8462/textual-0.85.2-py3-none-any.whl", hash = "sha256:9ccdeb6b8a6a0ff72d497f714934f2e524f2eb67783b459fb08b1339ee537dc0", size = 614939 }, 126 | ] 127 | 128 | [[package]] 129 | name = "typing-extensions" 130 | version = "4.12.2" 131 | source = { registry = "https://pypi.org/simple" } 132 | sdist = { url = "https://files.pythonhosted.org/packages/df/db/f35a00659bc03fec321ba8bce9420de607a1d37f8342eee1863174c69557/typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8", size = 85321 } 133 | wheels = [ 134 | { url = "https://files.pythonhosted.org/packages/26/9f/ad63fc0248c5379346306f8668cda6e2e2e9c95e01216d2b8ffd9ff037d0/typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", size = 37438 }, 135 | ] 136 | 137 | [[package]] 138 | name = "uc-micro-py" 139 | version = "1.0.3" 140 | source = { registry = "https://pypi.org/simple" } 141 | sdist = { url = "https://files.pythonhosted.org/packages/91/7a/146a99696aee0609e3712f2b44c6274566bc368dfe8375191278045186b8/uc-micro-py-1.0.3.tar.gz", hash = "sha256:d321b92cff673ec58027c04015fcaa8bb1e005478643ff4a500882eaab88c48a", size = 6043 } 142 | wheels = [ 143 | { url = "https://files.pythonhosted.org/packages/37/87/1f677586e8ac487e29672e4b17455758fce261de06a0d086167bb760361a/uc_micro_py-1.0.3-py3-none-any.whl", hash = "sha256:db1dffff340817673d7b466ec86114a9dc0e9d4d9b5ba229d9d60e5c12600cd5", size = 6229 }, 144 | ] 145 | -------------------------------------------------------------------------------- /src/idd/debuggers/gdb/gdb_commands.py: -------------------------------------------------------------------------------- 1 | import gdb 2 | import traceback 3 | import sys 4 | import json 5 | 6 | def stop_handler (event): 7 | plocals_result = gdb.execute("plocals", to_string=True) 8 | pargs_result = gdb.execute("pargs", to_string=True) 9 | pframe_result = gdb.execute("pframe", to_string=True) 10 | pasm_result = gdb.execute("pasm", to_string=True) 11 | pregisters_result = gdb.execute("pregisters", to_string=True) 12 | 13 | #print("begin_special_command") 14 | #print(json.dumps({ "plocals" : plocals_result, "pargs" : pargs_result, "pframe" : pframe_result, "pasm" : pasm_result, "pregisters" : pregisters_result })) 15 | #print("end_special_command") 16 | 17 | # gdb.events.stop.connect(stop_handler) 18 | # gdb.events.exited.connect (exit_handler) 19 | 20 | class PrintState (gdb.Command): 21 | def __init__ (self): 22 | super (PrintState, self).__init__ ("pstate", gdb.COMMAND_USER) 23 | 24 | def trim_middle_quotes(input_string): 25 | # Ensure the string starts and ends with quotes 26 | if input_string.startswith('"') and input_string.endswith('"'): 27 | # Remove the first and last quotes temporarily 28 | trimmed = input_string[1:-1] 29 | # Remove all remaining quotes 30 | trimmed = trimmed.replace('"', '') 31 | # Add back the starting and ending quotes 32 | # print(trimmed) 33 | return f'"{trimmed}"' 34 | return input_string # Return as-is if it doesn't start and end with quotes 35 | 36 | def trim_quotes(self, input_string): 37 | trimmed = input_string.replace('"','') 38 | return trimmed 39 | 40 | def invoke (self, arg, from_tty): 41 | result = {} 42 | 43 | locals = [] 44 | args = [] 45 | instructions = [] 46 | registers = [] 47 | 48 | length_per_ins = 4 49 | 50 | # get stack frame 51 | command_result = gdb.execute("bt", to_string=True) 52 | 53 | # leave only the starting and ending quotes 54 | # ensures correct parsing of the stack frames as 55 | stack_frames = [line for line in (command_result or "").split('\n') if line.strip()] 56 | trimmed_stack_frames = [self.trim_quotes(frame) for frame in stack_frames] 57 | result['stack_frames'] = trimmed_stack_frames or [] 58 | 59 | frame = gdb.selected_frame() 60 | block = frame.block() 61 | names = set() 62 | while block: 63 | for symbol in block: 64 | # get locals 65 | if (symbol.is_variable): 66 | name = symbol.name 67 | if not name in names: 68 | locals.append('{} = {}'.format(name, symbol.value(frame)).replace('"','')) 69 | 70 | # get args 71 | if (symbol.is_argument): 72 | name = symbol.name 73 | if not name in names: 74 | args.append('{} = {}\n'.format(name, symbol.value(frame)).replace('"','')) 75 | block = block.superblock 76 | 77 | # get instructions 78 | raw_instructions = frame.architecture().disassemble(frame.pc() - 4 * length_per_ins, count=10) 79 | for ins in raw_instructions: 80 | instructions.append("%s %s" % (ins['addr'], ins['asm'])) 81 | 82 | # get registers 83 | arch = frame.architecture() 84 | for rd in arch.registers('general'): 85 | value = gdb.parse_and_eval(f"${rd}") 86 | try: 87 | #value = gdb.parse_and_eval("%s" % rd) 88 | type = value.type 89 | if type.code != gdb.TYPE_CODE_PTR: 90 | if type.code == gdb.TYPE_CODE_VOID: 91 | type = gdb.lookup_type('int').pointer() 92 | else: 93 | type = type.pointer() 94 | elif (type.target().code == gdb.TYPE_CODE_VOID 95 | or type.target().code == gdb.TYPE_CODE_FUNC): 96 | type = gdb.lookup_type('int').pointer() 97 | string = "%-10s" % str(rd) 98 | try: 99 | value = value.cast(type) 100 | value = value.dereference() 101 | string += value.format_string(format='x') 102 | except Exception as e: 103 | registers.append(str(e)) 104 | finally: 105 | registers.append(string) 106 | except Exception as e: 107 | registers.append(str(e)) 108 | 109 | result['locals'] = locals or [] 110 | result['args'] = args or [] 111 | result['instructions'] = instructions or [] 112 | result['registers'] = registers or [] 113 | 114 | json_result = json.dumps(result) 115 | 116 | gdb.write(f'{json_result}\n', gdb.STDOUT) 117 | 118 | 119 | class PrintFrame (gdb.Command): 120 | def __init__ (self): 121 | super (PrintFrame, self).__init__ ("pframe", gdb.COMMAND_USER) 122 | 123 | def invoke (self, arg, from_tty): 124 | result = gdb.execute("bt", to_string=True) 125 | gdb.write(result, gdb.STDOUT) 126 | 127 | class PrintLocals (gdb.Command): 128 | def __init__ (self): 129 | super (PrintLocals, self).__init__ ("plocals", gdb.COMMAND_USER) 130 | 131 | def invoke (self, arg, from_tty): 132 | frame = gdb.selected_frame() 133 | block = frame.block() 134 | names = set() 135 | while block: 136 | for symbol in block: 137 | if (symbol.is_variable): 138 | name = symbol.name 139 | if not name in names: 140 | print('{} = {}'.format(name, symbol.value(frame))) 141 | names.add(name) 142 | block = block.superblock 143 | 144 | class PrintArgs (gdb.Command): 145 | def __init__ (self): 146 | super (PrintArgs, self).__init__ ("pargs", gdb.COMMAND_USER) 147 | 148 | def invoke (self, arg, from_tty): 149 | frame = gdb.selected_frame() 150 | block = frame.block() 151 | names = set() 152 | while block: 153 | for symbol in block: 154 | if (symbol.is_argument): 155 | name = symbol.name 156 | if not name in names: 157 | print('{} = {}\n'.format(name, symbol.value(frame))) 158 | names.add(name) 159 | block = block.superblock 160 | class PrintAsm (gdb.Command): 161 | def __init__ (self): 162 | super (PrintAsm, self).__init__ ("pasm", gdb.COMMAND_USER) 163 | 164 | def invoke (self, arg, from_tty): 165 | str = "" 166 | length_per_ins = 4 167 | 168 | frame = gdb.selected_frame() 169 | instructions = frame.architecture().disassemble(frame.pc() - 4 * length_per_ins, count=10) 170 | 171 | for ins in instructions: 172 | str += "%s %s \n" % (ins['addr'], ins['asm']) 173 | 174 | print(str) 175 | 176 | class PrintRegisters (gdb.Command): 177 | def __init__ (self): 178 | super (PrintRegisters, self).__init__ ("pregisters", gdb.COMMAND_USER) 179 | 180 | def invoke (self, arg, from_tty): 181 | string = "" 182 | frame = gdb.selected_frame() 183 | arch = frame.architecture() 184 | for rd in arch.registers('general'): 185 | value = gdb.parse_and_eval(f"${rd}") 186 | try: 187 | #value = gdb.parse_and_eval("%s" % rd) 188 | type = value.type 189 | if type.code != gdb.TYPE_CODE_PTR: 190 | if type.code == gdb.TYPE_CODE_VOID: 191 | type = gdb.lookup_type('int').pointer() 192 | else: 193 | type = type.pointer() 194 | elif (type.target().code == gdb.TYPE_CODE_VOID 195 | or type.target().code == gdb.TYPE_CODE_FUNC): 196 | type = gdb.lookup_type('int').pointer() 197 | string = "%-10s" % str(rd) 198 | try: 199 | value = value.cast(type) 200 | value = value.dereference() 201 | string += value.format_string(format='x') 202 | except Exception as e: 203 | string += str(e) 204 | finally: 205 | print(string) 206 | except Exception as e: 207 | string += str(e) 208 | 209 | class RunWrapper (gdb.Command): 210 | """Greet the whole world.""" 211 | 212 | def __init__ (self): 213 | super (RunWrapper, self).__init__ ("run-wrapper", gdb.COMMAND_USER) 214 | 215 | def invoke (self, arg, from_tty): 216 | try: 217 | result = gdb.execute(arg, to_string=True) 218 | print(result) 219 | except Exception as ex: 220 | ex_type, ex_value, ex_traceback = sys.exc_info() 221 | # Extract unformatter stack traces as tuples 222 | trace_back = traceback.extract_tb(ex_traceback) 223 | 224 | # Format stacktrace 225 | stack_trace = list() 226 | 227 | for trace in trace_back: 228 | stack_trace.append("File : %s , Line : %d, Func.Name : %s, Message : %s" % (trace[0], trace[1], trace[2], trace[3])) 229 | 230 | print("Exception type : %s " % ex_type.__name__) 231 | print("Exception message : %s" % ex_value) 232 | print("stack trace : %s" % stack_trace) 233 | 234 | PrintFrame() 235 | RunWrapper() 236 | PrintLocals() 237 | PrintArgs() 238 | PrintAsm() 239 | PrintRegisters() 240 | PrintState() -------------------------------------------------------------------------------- /src/idd/debuggers/gdb/gdb_driver.py: -------------------------------------------------------------------------------- 1 | from pygdbmi.gdbcontroller import GdbController 2 | from pprint import pprint 3 | from idd.driver import Driver 4 | 5 | import io 6 | import time 7 | import subprocess 8 | import selectors 9 | import sys 10 | import os, fcntl 11 | import select, time 12 | import threading, queue 13 | 14 | base_response = "" 15 | regressed_response = "" 16 | 17 | base_err_response = "" 18 | regressed_err_response = "" 19 | 20 | class GDBDebugger(Driver): 21 | base_gdb_instance = None 22 | regressed_gdb_instance = None 23 | gdb_instances = None 24 | 25 | _base_response = "" 26 | _regression_response = "" 27 | _base_observers = [] 28 | _regression_observers = [] 29 | 30 | def __init__(self): 31 | self._base_observers = "" 32 | self._regression_observers = "" 33 | self._regression_response = "" 34 | self._observers = [] 35 | 36 | @property 37 | def base_response(self): 38 | return self._base_response 39 | 40 | @base_response.setter 41 | def base_response(self, value): 42 | self._base_response = value 43 | if self._base_response != '': 44 | for callback in self._base_observers: 45 | callback(self._base_response) 46 | 47 | @property 48 | def regression_response(self): 49 | return self._regression_response 50 | 51 | @regression_response.setter 52 | def regression_response(self, value): 53 | self._regression_response = value 54 | if self._regression_response != '': 55 | for callback in self._regression_observers: 56 | callback(self._regression_response) 57 | 58 | def add_base_observer(self, callback): 59 | self._base_observers.append(callback) 60 | 61 | def add_regression_observer(self, callback): 62 | self._regression_observers.append(callback) 63 | 64 | def subprocess_readlines(self, out): 65 | while True: 66 | line = out.readline() 67 | if not line: 68 | return 69 | yield line 70 | 71 | def handle_base_err_output(self, stream, mask): 72 | global base_err_response 73 | 74 | if stream.closed: 75 | base_err_response = "stream is closed" 76 | 77 | if stream.readable(): 78 | temp = [] 79 | for line in self.subprocess_readlines(stream): 80 | temp.append(str(line.decode('utf-8'))) 81 | 82 | base_err_response = ''.join(str(x) for x in temp) 83 | else: 84 | base_err_response = "stream not readable" 85 | 86 | def handle_base_output(self, stream, mask): 87 | global base_response 88 | 89 | if stream.closed: 90 | base_response = "stream is closed" 91 | 92 | if stream.readable(): 93 | temp = [] 94 | for line in self.subprocess_readlines(stream): 95 | temp.append(line.decode('utf-8')) 96 | 97 | # base_response = #''.join(str(x) for x in temp) 98 | 99 | self.base_response = temp 100 | else: 101 | self.base_response = "stream not readable" 102 | 103 | def handle_regression_output(self, stream, mask): 104 | global regressed_response 105 | 106 | if stream.closed: 107 | regressed_response = "stream is closed" 108 | 109 | if stream.readable(): 110 | temp = [] 111 | for line in self.subprocess_readlines(stream): 112 | temp.append(line.decode('utf-8')) 113 | 114 | # regressed_response = temp #''.join(str(x) for x in temp) 115 | 116 | self.regression_response = temp 117 | else: 118 | self.regression_response = "stream not readable" 119 | 120 | def handle_regression_err_output(self, stream, mask): 121 | global regressed_err_response 122 | 123 | if stream.closed: 124 | regressed_err_response = "stream is closed" 125 | 126 | if stream.readable(): 127 | temp = [] 128 | stderr_response = stream.readlines() 129 | for item in stderr_response: 130 | temp.append(str(item.decode('utf-8'))) 131 | 132 | regressed_err_response = ''.join(str(x) for x in temp) 133 | else: 134 | regressed_err_response = "stream not readable" 135 | 136 | def listen_base_stdout(self, gdb_instance): 137 | selector = selectors.DefaultSelector() 138 | selector.register(gdb_instance.stdout, selectors.EVENT_READ, self.handle_base_output) 139 | selector.register(gdb_instance.stderr, selectors.EVENT_READ, self.handle_base_err_output) 140 | 141 | while gdb_instance.poll() is None: 142 | # Wait for events and handle them with their registered callbacks 143 | events = selector.select() 144 | for key, mask in events: 145 | callback = key.data 146 | callback(key.fileobj, mask) 147 | 148 | return True 149 | 150 | def listen_regression_stdout(self, gdb_instance): 151 | selector = selectors.DefaultSelector() 152 | selector.register(gdb_instance.stdout, selectors.EVENT_READ, self.handle_regression_output) 153 | selector.register(gdb_instance.stderr, selectors.EVENT_READ, self.handle_regression_err_output) 154 | 155 | while gdb_instance.poll() is None: 156 | events = selector.select() 157 | for key, mask in events: 158 | callback = key.data 159 | callback(key.fileobj, mask) 160 | 161 | return True 162 | 163 | def process_response(self, base_response, regression_response): 164 | if len(regression_response) > 2 and len(base_response) > 2: 165 | if regression_response[len(regression_response) - 2] == "end_command\n" and base_response[len(base_response) - 2] == "end_command\n": 166 | return { 'base': base_response, 'regressed': regression_response } 167 | 168 | return None 169 | 170 | def __init__(self, base_args, regression_args): 171 | ba = ["gdb", "--args", base_args] 172 | self.base_gdb_instance = subprocess.Popen(ba, stderr=subprocess.PIPE, stdout=subprocess.PIPE, stdin=subprocess.PIPE) 173 | 174 | ra = ["gdb", "--args", regression_args] 175 | self.regressed_gdb_instance = subprocess.Popen(ra, stderr=subprocess.PIPE, stdout=subprocess.PIPE, stdin=subprocess.PIPE) 176 | 177 | fcntl.fcntl(self.base_gdb_instance.stdout, fcntl.F_SETFL, os.O_NONBLOCK) 178 | fcntl.fcntl(self.regressed_gdb_instance.stdout, fcntl.F_SETFL, os.O_NONBLOCK) 179 | 180 | fcntl.fcntl(self.base_gdb_instance.stderr, fcntl.F_SETFL, os.O_NONBLOCK) 181 | fcntl.fcntl(self.regressed_gdb_instance.stderr, fcntl.F_SETFL, os.O_NONBLOCK) 182 | 183 | fcntl.fcntl(self.base_gdb_instance.stdin, fcntl.F_SETFL, os.O_NONBLOCK) 184 | fcntl.fcntl(self.regressed_gdb_instance.stdin, fcntl.F_SETFL, os.O_NONBLOCK) 185 | 186 | self.gdb_instances = { 'base': self.base_gdb_instance, 'regressed': self.regressed_gdb_instance } 187 | 188 | thread_base = threading.Thread(target=self.listen_base_stdout, args=(self.base_gdb_instance,)) 189 | thread_base.start() 190 | thread_regressed = threading.Thread(target=self.listen_regression_stdout, args=(self.regressed_gdb_instance,)) 191 | thread_regressed.start() 192 | 193 | dirname = os.path.dirname(__file__) 194 | self.run_parallel_raw_command("source " + os.path.join(dirname, "gdb_commands.py")) 195 | 196 | async def run_parallel_command(self, command): 197 | # self.base_response = "" 198 | # self.regression_response = "" 199 | 200 | 201 | 202 | await self.run_single_command(command, "base") 203 | await self.run_single_command(command, "regressed") 204 | 205 | async def run_single_command(self, command, version): 206 | global base_response 207 | global regressed_response 208 | 209 | #try: 210 | #raise Exception("3: " + command) 211 | self.gdb_instances[version].stdin.write("run-wrapper {command}\n".format(command = command).encode()) 212 | self.gdb_instances[version].stdin.flush() 213 | #except Exception as ex: 214 | # import traceback 215 | # ex_type, ex_value, ex_traceback = sys.exc_info() 216 | # # Extract unformatter stack traces as tuples 217 | # trace_back = traceback.extract_tb(ex_traceback) 218 | 219 | # Format stacktrace 220 | # stack_trace = list() 221 | 222 | # for trace in trace_back: 223 | # stack_trace.append("File : %s , Line : %d, Func.Name : %s, Message : %s" % (trace[0], trace[1], trace[2], trace[3])) 224 | 225 | # print("Exception type : %s " % ex_type.__name__) 226 | # print("Exception message : %s" % ex_value) 227 | # print("stack trace : %s" % stack_trace) 228 | 229 | # temp = None 230 | 231 | # timeout = time.time() + 5 232 | 233 | # if version == "base": 234 | # while True: 235 | # if base_response != None and ("end_command" in base_response) or time.time() > timeout: 236 | # temp = base_response 237 | # temp = temp.replace("end_command\n(gdb) ", "") 238 | # base_response = "" 239 | # break 240 | 241 | # if version == "regressed": 242 | # while True: 243 | # if regressed_response != None and ("end_command" in regressed_response) or time.time() > timeout: 244 | # temp = regressed_response 245 | # temp = temp.replace("end_command\n(gdb) ", "") 246 | # regressed_response = "" 247 | # break 248 | 249 | # return temp 250 | #raise Exception("4: " + command) 251 | 252 | def run_parallel_raw_command(self, command): 253 | base_result = self.run_single_raw_command(command, "base") 254 | regression_result = self.run_single_raw_command(command, "regressed") 255 | 256 | return { "base": base_result, "regressed": regression_result } 257 | 258 | def run_single_raw_command(self, command, version): 259 | global base_response 260 | global regressed_response 261 | 262 | self.gdb_instances[version].stdin.write("{command}\n".format(command = command).encode()) 263 | self.gdb_instances[version].stdin.flush() 264 | 265 | temp = None 266 | 267 | if version == "base": 268 | while True: 269 | if base_response != None: 270 | temp = base_response 271 | base_response = "" 272 | break 273 | 274 | if version == "regressed": 275 | while True: 276 | if regressed_response != None: 277 | temp = regressed_response 278 | regressed_response = "" 279 | break 280 | 281 | return temp 282 | -------------------------------------------------------------------------------- /src/idd/debuggers/gdb/gdb_mi_driver.py: -------------------------------------------------------------------------------- 1 | import os 2 | import json 3 | import logging, threading 4 | 5 | from pygdbmi import gdbmiparser 6 | 7 | from idd.debuggers.gdb.idd_gdb_controller import IDDGdbController 8 | from idd.driver import Driver 9 | 10 | from idd.debuggers.gdb.utils import parse_gdb_line 11 | 12 | base_response = [] 13 | regressed_response = [] 14 | 15 | logger = logging.getLogger(__name__) 16 | logging.basicConfig(level=logging.INFO) 17 | 18 | class GDBMiDebugger(Driver): 19 | base_gdb_instance = None 20 | 21 | regressed_gdb_instance = None 22 | 23 | gdb_instances = None 24 | 25 | def __init__(self, base_args, base_script_file_path, regression_args, regression_script_file_path, 26 | base_pid=None, regression_pid=None): 27 | self.base_gdb_instance = IDDGdbController(base_args, base_pid, base_script_file_path) 28 | self.regressed_gdb_instance = IDDGdbController(regression_args, regression_pid, regression_script_file_path) 29 | 30 | self.gdb_instances = { 'base': self.base_gdb_instance, 'regressed': self.regressed_gdb_instance } 31 | 32 | dirname = os.path.dirname(__file__) 33 | self.run_parallel_command("source " + os.path.join(dirname, "gdb_commands.py")) 34 | self.run_parallel_command("set debuginfod enabled off") 35 | 36 | def run_parallel_command(self, command): 37 | base_result = [] 38 | regressed_result = [] 39 | 40 | def get_result(instance, output_holder): 41 | raw_result = instance.read_until_prompt() 42 | parsed = self.parse_command_output(raw_result) 43 | output_holder.append(parsed) 44 | 45 | # Start both receivers in parallel 46 | base_thread = threading.Thread(target=get_result, args=(self.base_gdb_instance, base_result)) 47 | regressed_thread = threading.Thread(target=get_result, args=(self.regressed_gdb_instance, regressed_result)) 48 | 49 | #self.base_gdb_instance.flush_debuggee_output() 50 | self.base_gdb_instance.write(command) 51 | 52 | #self.base_gdb_instance.flush_debuggee_output() 53 | self.regressed_gdb_instance.write(command) 54 | 55 | base_thread.start() 56 | regressed_thread.start() 57 | base_thread.join() 58 | regressed_thread.join() 59 | 60 | return { 61 | "base": base_result[0], 62 | "regressed": regressed_result[0], 63 | } 64 | 65 | 66 | def parse_command_output(self, raw_result): 67 | """Parses raw GDB output from PTY into structured data.""" 68 | response = [] 69 | 70 | if not raw_result: 71 | return response # Return an empty list if no output 72 | 73 | # Split by carriage returns (used in PTY), but also clean up empty lines 74 | lines = [line.strip() for line in raw_result.strip().split("\r") if line.strip()] 75 | 76 | for raw_line in lines: 77 | try: 78 | parsed = gdbmiparser.parse_response(raw_line) 79 | except ValueError as e: 80 | logger.warning(f"Unparsable line from GDB: {raw_line!r} ({e})") 81 | continue 82 | 83 | if parsed["type"] == "console": 84 | line = str(parsed["payload"]).strip() 85 | 86 | if not line: 87 | continue 88 | 89 | # Detect interactive GDB prompts 90 | if line.endswith("(y or n)?") or line.endswith("[y/n]"): 91 | logger.warning(f"GDB is waiting for user input: {line}") 92 | try: 93 | user_input = input("GDB Prompt detected. Please enter response (y/n): ").strip() 94 | except KeyboardInterrupt: 95 | logger.warning("User cancelled GDB input prompt.") 96 | user_input = "n" 97 | 98 | self.write(user_input) 99 | continue # Skip this prompt line 100 | 101 | # Parse the cleaned line (e.g., remove MI wrappers, etc.) 102 | processed_output = parse_gdb_line(line) 103 | response.append(processed_output) 104 | 105 | #elif parsed["type"] in {"log", "target", "notify"}: 106 | # You can optionally handle these too 107 | #response.append(f"[{parsed['type']}] {parsed.get('payload', '')}") 108 | 109 | #elif parsed["type"] == "result": 110 | # Sometimes it's helpful to log MI command results too 111 | #response.append(f"[MI Result] ^{parsed.get('message', '')}") 112 | 113 | return response 114 | 115 | 116 | def run_single_command(self, command, version): 117 | global base_response 118 | global regressed_response 119 | 120 | try: 121 | #self.gdb_instances[version].flush_debuggee_output() 122 | self.gdb_instances[version].write(command) 123 | raw_result = self.gdb_instances[version].read_until_prompt() 124 | 125 | except Exception as e: 126 | logger.exception(f"Error executing GDB command: {command}") 127 | # self.handle_gdb_crash() 128 | return [] 129 | 130 | return self.parse_command_output(raw_result) 131 | 132 | def run_single_special_command(self, command, version): 133 | global base_response 134 | global regressed_response 135 | 136 | try: 137 | #self.gdb_instances[version].flush_debuggee_output() 138 | self.gdb_instances[version].write(command) 139 | raw_result = self.gdb_instances[version].read_until_prompt() 140 | except Exception as e: 141 | logger.exception(f"Error executing GDB command: {command}") 142 | # self.handle_gdb_crash() 143 | return [] 144 | 145 | return self.parse_special_command_output(raw_result) 146 | 147 | def parse_special_command_output(self, raw_result): 148 | response = [] 149 | 150 | if not raw_result: 151 | return response # Return an empty list if no output 152 | 153 | # Split by carriage returns (used in PTY), but also clean up empty lines 154 | lines = [line.strip() for line in raw_result.strip().split("\r") if line.strip()] 155 | 156 | for raw_line in lines: 157 | try: 158 | parsed = gdbmiparser.parse_response(raw_line) 159 | except ValueError as e: 160 | logger.warning(f"Unparsable line from GDB: {raw_line!r} ({e})") 161 | continue 162 | 163 | if parsed["type"] == "console": 164 | line = str(parsed["payload"]).strip() 165 | 166 | if not line: 167 | continue 168 | 169 | # Detect interactive GDB prompts 170 | if line.endswith("(y or n)?") or line.endswith("[y/n]"): 171 | logger.warning(f"GDB is waiting for user input: {line}") 172 | try: 173 | user_input = input("GDB Prompt detected. Please enter response (y/n): ").strip() 174 | except KeyboardInterrupt: 175 | logger.warning("User cancelled GDB input prompt.") 176 | user_input = "n" 177 | 178 | self.write(user_input) 179 | continue # Skip this prompt line 180 | 181 | # Parse the cleaned line (e.g., remove MI wrappers, etc.) 182 | processed_output = parse_gdb_line(line) 183 | try: 184 | parsed_dict = json.loads(processed_output) 185 | except json.JSONDecodeError: 186 | parsed_dict = processed_output 187 | 188 | if parsed_dict: 189 | return parsed_dict 190 | 191 | def get_state(self, version=None): 192 | if version is not None: 193 | return self.run_single_special_command("pstate", version) 194 | 195 | #self.base_gdb_instance.flush_debuggee_output() 196 | #self.regressed_gdb_instance.flush_debuggee_output() 197 | # get base and regression state 198 | self.base_gdb_instance.write((" {command}\n".format(command = "pstate"))) 199 | self.regressed_gdb_instance.write((" {command}\n".format(command = "pstate"))) 200 | 201 | # wait till base is done 202 | raw_result = self.base_gdb_instance.read_until_prompt() 203 | base_state = self.parse_special_command_output(raw_result) 204 | 205 | # wait till regression is done 206 | raw_result = self.regressed_gdb_instance.read_until_prompt() 207 | regression_state = self.parse_special_command_output(raw_result) 208 | 209 | return { "base" : base_state, "regressed" : regression_state } 210 | 211 | def get_current_stack_frames(self, state): 212 | base_stack_frame = state['base']['stack_frame'] 213 | regression_stack_frame = state['base']['stack_frame'] 214 | 215 | return { "base" : base_stack_frame, "regressed" : regression_stack_frame } 216 | 217 | def get_current_args(self): 218 | base_stack_frame = self.run_single_command('pargs', 'base') 219 | regression_stack_frame = self.run_single_command('pargs', 'regressed') 220 | 221 | return { "base" : base_stack_frame, "regressed" : regression_stack_frame } 222 | 223 | def get_current_local_vars(self): 224 | base_stack_frame = self.run_single_command('plocals', 'base') 225 | regression_stack_frame = self.run_single_command('plocals', 'regressed') 226 | 227 | return { "base" : base_stack_frame, "regressed" : regression_stack_frame } 228 | 229 | def get_current_instructions(self): 230 | base_stack_frame = self.run_single_command('pasm', 'base') 231 | regression_stack_frame = self.run_single_command('pasm', 'regressed') 232 | 233 | return { "base" : base_stack_frame, "regressed" : regression_stack_frame } 234 | 235 | def get_current_registers(self): 236 | base_stack_frame = self.run_single_command('pregisters', 'base') 237 | regression_stack_frame = self.run_single_command('pregisters', 'regressed') 238 | 239 | return { "base" : base_stack_frame, "regressed" : regression_stack_frame } 240 | 241 | def run_parallel_raw_command(self, command): 242 | self.base_gdb_instance.write((("{command}\n".format(command = command),), {"timeout_sec": 60})) 243 | self.regressed_gdb_instance.write((("{command}\n".format(command = command),), {"timeout_sec": 60})) 244 | 245 | raw_result = self.base_gdb_instance.recv() 246 | base_result = str(self.parse_raw_command_output(raw_result)) 247 | raw_result = self.regressed_gdb_instance.recv() 248 | regression_result = str(self.parse_raw_command_output(raw_result)) 249 | 250 | return { "base": base_result, "regressed": regression_result } 251 | 252 | def parse_raw_command_output(self, raw_result): 253 | result = [] 254 | for item in raw_result: 255 | result.append(str(item)) 256 | return result 257 | 258 | def run_single_raw_command(self, command, version): 259 | self.gdb_instances[version].write((("{command}\n".format(command = command),), {"timeout_sec": 60})) 260 | raw_result = self.gdb_instances[version].recv() 261 | return self.parse_raw_command_output(raw_result) 262 | 263 | def insert_stdin(self, text: str): 264 | self.base_gdb_instance.send_input_to_debuggee(text) 265 | self.regressed_gdb_instance.send_input_to_debuggee(text) 266 | 267 | def insert_stdin_single(self, text: str, target: str): 268 | if target == "base": 269 | self.base_gdb_instance.send_input_to_debuggee(text) 270 | if target == "regressed": 271 | self.regressed_gdb_instance.send_input_to_debuggee(text) 272 | 273 | def terminate(self): 274 | print("Terminating GDB instances") 275 | self.base_gdb_instance.terminate() 276 | self.regressed_gdb_instance.terminate() 277 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /src/idd/differ.py: -------------------------------------------------------------------------------- 1 | from difflib import SequenceMatcher 2 | 3 | #https://github.com/python/cpython/blob/3.12/Lib/difflib.py 4 | class Differ: 5 | r""" 6 | Differ is a class for comparing sequences of lines of text, and 7 | producing human-readable differences or deltas. Differ uses 8 | SequenceMatcher both to compare sequences of lines, and to compare 9 | sequences of characters within similar (near-matching) lines. 10 | Each line of a Differ delta begins with a two-letter code: 11 | '- ' line unique to sequence 1 12 | '+ ' line unique to sequence 2 13 | ' ' line common to both sequences 14 | '? ' line not present in either input sequence 15 | Lines beginning with '? ' attempt to guide the eye to intraline 16 | differences, and were not present in either input sequence. These lines 17 | can be confusing if the sequences contain tab characters. 18 | Note that Differ makes no claim to produce a *minimal* diff. To the 19 | contrary, minimal diffs are often counter-intuitive, because they synch 20 | up anywhere possible, sometimes accidental matches 100 pages apart. 21 | Restricting synch points to contiguous matches preserves some notion of 22 | locality, at the occasional cost of producing a longer diff. 23 | Example: Comparing two texts. 24 | First we set up the texts, sequences of individual single-line strings 25 | ending with newlines (such sequences can also be obtained from the 26 | `readlines()` method of file-like objects): 27 | >>> text1 = ''' 1. Beautiful is better than ugly. 28 | ... 2. Explicit is better than implicit. 29 | ... 3. Simple is better than complex. 30 | ... 4. Complex is better than complicated. 31 | ... '''.splitlines(keepends=True) 32 | >>> len(text1) 33 | 4 34 | >>> text1[0][-1] 35 | '\n' 36 | >>> text2 = ''' 1. Beautiful is better than ugly. 37 | ... 3. Simple is better than complex. 38 | ... 4. Complicated is better than complex. 39 | ... 5. Flat is better than nested. 40 | ... '''.splitlines(keepends=True) 41 | Next we instantiate a Differ object: 42 | >>> d = Differ() 43 | Note that when instantiating a Differ object we may pass functions to 44 | filter out line and character 'junk'. See Differ.__init__ for details. 45 | Finally, we compare the two: 46 | >>> result = list(d.compare(text1, text2)) 47 | 'result' is a list of strings, so let's pretty-print it: 48 | >>> from pprint import pprint as _pprint 49 | >>> _pprint(result) 50 | [' 1. Beautiful is better than ugly.\n', 51 | '- 2. Explicit is better than implicit.\n', 52 | '- 3. Simple is better than complex.\n', 53 | '+ 3. Simple is better than complex.\n', 54 | '? ++\n', 55 | '- 4. Complex is better than complicated.\n', 56 | '? ^ ---- ^\n', 57 | '+ 4. Complicated is better than complex.\n', 58 | '? ++++ ^ ^\n', 59 | '+ 5. Flat is better than nested.\n'] 60 | As a single multi-line string it looks like this: 61 | >>> print(''.join(result), end="") 62 | 1. Beautiful is better than ugly. 63 | - 2. Explicit is better than implicit. 64 | - 3. Simple is better than complex. 65 | + 3. Simple is better than complex. 66 | ? ++ 67 | - 4. Complex is better than complicated. 68 | ? ^ ---- ^ 69 | + 4. Complicated is better than complex. 70 | ? ++++ ^ ^ 71 | + 5. Flat is better than nested. 72 | Methods: 73 | __init__(linejunk=None, charjunk=None) 74 | Construct a text differencer, with optional filters. 75 | compare(a, b) 76 | Compare two sequences of lines; generate the resulting delta. 77 | """ 78 | 79 | def __init__(self, linejunk=None, charjunk=None): 80 | """ 81 | Construct a text differencer, with optional filters. 82 | The two optional keyword parameters are for filter functions: 83 | - `linejunk`: A function that should accept a single string argument, 84 | and return true iff the string is junk. The module-level function 85 | `IS_LINE_JUNK` may be used to filter out lines without visible 86 | characters, except for at most one splat ('#'). It is recommended 87 | to leave linejunk None; the underlying SequenceMatcher class has 88 | an adaptive notion of "noise" lines that's better than any static 89 | definition the author has ever been able to craft. 90 | - `charjunk`: A function that should accept a string of length 1. The 91 | module-level function `IS_CHARACTER_JUNK` may be used to filter out 92 | whitespace characters (a blank or tab; **note**: bad idea to include 93 | newline in this!). Use of IS_CHARACTER_JUNK is recommended. 94 | """ 95 | 96 | self.linejunk = linejunk 97 | self.charjunk = charjunk 98 | 99 | def compare(self, a, b): 100 | r""" 101 | Compare two sequences of lines; generate the resulting delta. 102 | Each sequence must contain individual single-line strings ending with 103 | newlines. Such sequences can be obtained from the `readlines()` method 104 | of file-like objects. The delta generated also consists of newline- 105 | terminated strings, ready to be printed as-is via the writeline() 106 | method of a file-like object. 107 | Example: 108 | >>> print(''.join(Differ().compare('one\ntwo\nthree\n'.splitlines(True), 109 | ... 'ore\ntree\nemu\n'.splitlines(True))), 110 | ... end="") 111 | - one 112 | ? ^ 113 | + ore 114 | ? ^ 115 | - two 116 | - three 117 | ? - 118 | + tree 119 | + emu 120 | """ 121 | 122 | cruncher = SequenceMatcher(self.linejunk, a, b) 123 | for tag, alo, ahi, blo, bhi in cruncher.get_opcodes(): 124 | if tag == 'replace': 125 | g = self._fancy_replace(a, alo, ahi, b, blo, bhi) 126 | elif tag == 'delete': 127 | g = self._dump('-', a, alo, ahi) 128 | elif tag == 'insert': 129 | g = self._dump('+', b, blo, bhi) 130 | elif tag == 'equal': 131 | g = self._dump(' ', a, alo, ahi) 132 | else: 133 | raise ValueError('unknown tag %r' % (tag,)) 134 | 135 | yield from g 136 | 137 | def _dump(self, tag, x, lo, hi): 138 | """Generate comparison results for a same-tagged range.""" 139 | for i in range(lo, hi): 140 | if tag == '-': 141 | yield '%s [red] %s [/red]' % (tag, x[i]) 142 | elif tag == '+': 143 | yield '%s [green] %s [/green]' % (tag, x[i]) 144 | else: 145 | yield '%s %s' % (tag, x[i]) 146 | 147 | def _plain_replace(self, a, alo, ahi, b, blo, bhi): 148 | assert alo < ahi and blo < bhi 149 | # dump the shorter block first -- reduces the burden on short-term 150 | # memory if the blocks are of very different sizes 151 | if bhi - blo < ahi - alo: 152 | first = self._dump('+', b, blo, bhi) 153 | second = self._dump('-', a, alo, ahi) 154 | else: 155 | first = self._dump('-', a, alo, ahi) 156 | second = self._dump('+', b, blo, bhi) 157 | 158 | for g in first, second: 159 | yield from g 160 | 161 | def _fancy_replace(self, a, alo, ahi, b, blo, bhi): 162 | r""" 163 | When replacing one block of lines with another, search the blocks 164 | for *similar* lines; the best-matching pair (if any) is used as a 165 | synch point, and intraline difference marking is done on the 166 | similar pair. Lots of work, but often worth it. 167 | Example: 168 | >>> d = Differ() 169 | >>> results = d._fancy_replace(['abcDefghiJkl\n'], 0, 1, 170 | ... ['abcdefGhijkl\n'], 0, 1) 171 | >>> print(''.join(results), end="") 172 | - abcDefghiJkl 173 | ? ^ ^ ^ 174 | + abcdefGhijkl 175 | ? ^ ^ ^ 176 | """ 177 | 178 | # don't synch up unless the lines have a similarity score of at 179 | # least cutoff; best_ratio tracks the best score seen so far 180 | best_ratio, cutoff = 0.74, 0.75 181 | cruncher = SequenceMatcher(self.charjunk) 182 | eqi, eqj = None, None # 1st indices of equal lines (if any) 183 | 184 | # search for the pair that matches best without being identical 185 | # (identical lines must be junk lines, & we don't want to synch up 186 | # on junk -- unless we have to) 187 | for j in range(blo, bhi): 188 | bj = b[j] 189 | cruncher.set_seq2(bj) 190 | for i in range(alo, ahi): 191 | ai = a[i] 192 | if ai == bj: 193 | if eqi is None: 194 | eqi, eqj = i, j 195 | continue 196 | cruncher.set_seq1(ai) 197 | # computing similarity is expensive, so use the quick 198 | # upper bounds first -- have seen this speed up messy 199 | # compares by a factor of 3. 200 | # note that ratio() is only expensive to compute the first 201 | # time it's called on a sequence pair; the expensive part 202 | # of the computation is cached by cruncher 203 | if cruncher.real_quick_ratio() > best_ratio and \ 204 | cruncher.quick_ratio() > best_ratio and \ 205 | cruncher.ratio() > best_ratio: 206 | best_ratio, best_i, best_j = cruncher.ratio(), i, j 207 | if best_ratio < cutoff: 208 | # no non-identical "pretty close" pair 209 | if eqi is None: 210 | # no identical pair either -- treat it as a straight replace 211 | yield from self._plain_replace(a, alo, ahi, b, blo, bhi) 212 | return 213 | # no close pair, but an identical pair -- synch up on that 214 | best_i, best_j, best_ratio = eqi, eqj, 1.0 215 | else: 216 | # there's a close pair, so forget the identical pair (if any) 217 | eqi = None 218 | 219 | # a[best_i] very similar to b[best_j]; eqi is None iff they're not 220 | # identical 221 | 222 | # pump out diffs from before the synch point 223 | yield from self._fancy_helper(a, alo, best_i, b, blo, best_j) 224 | 225 | # do intraline marking on the synch pair 226 | aelt, belt = a[best_i], b[best_j] 227 | if eqi is None: 228 | # pump out a '-', '?', '+', '?' quad for the synched lines 229 | atags = btags = "" 230 | cruncher.set_seqs(aelt, belt) 231 | for tag, ai1, ai2, bj1, bj2 in cruncher.get_opcodes(): 232 | la, lb = ai2 - ai1, bj2 - bj1 233 | if tag == 'replace': 234 | atags += '^' * la 235 | btags += '^' * lb 236 | elif tag == 'delete': 237 | atags += '-' * la 238 | elif tag == 'insert': 239 | btags += '+' * lb 240 | elif tag == 'equal': 241 | atags += ' ' * la 242 | btags += ' ' * lb 243 | else: 244 | raise ValueError('unknown tag %r' % (tag,)) 245 | # yield from self._qformat(aelt, belt, atags, btags) 246 | yield from self._html_format(aelt, belt, atags, btags) 247 | else: 248 | # the synch pair is identical 249 | yield ' ' + aelt 250 | 251 | # pump out diffs from after the synch point 252 | yield from self._fancy_helper(a, best_i+1, ahi, b, best_j+1, bhi) 253 | 254 | def _fancy_helper(self, a, alo, ahi, b, blo, bhi): 255 | g = [] 256 | if alo < ahi: 257 | if blo < bhi: 258 | g = self._fancy_replace(a, alo, ahi, b, blo, bhi) 259 | else: 260 | g = self._dump('-', a, alo, ahi) 261 | elif blo < bhi: 262 | g = self._dump('+', b, blo, bhi) 263 | 264 | yield from g 265 | 266 | def _qformat(self, aline, bline, atags, btags): 267 | r""" 268 | Format "?" output and deal with tabs. 269 | Example: 270 | >>> d = Differ() 271 | >>> results = d._qformat('\tabcDefghiJkl\n', '\tabcdefGhijkl\n', 272 | ... ' ^ ^ ^ ', ' ^ ^ ^ ') 273 | >>> for line in results: print(repr(line)) 274 | ... 275 | '- \tabcDefghiJkl\n' 276 | '? \t ^ ^ ^\n' 277 | '+ \tabcdefGhijkl\n' 278 | '? \t ^ ^ ^\n' 279 | """ 280 | atags = _keep_original_ws(aline, atags).rstrip() 281 | btags = _keep_original_ws(bline, btags).rstrip() 282 | 283 | yield "- " + aline 284 | if atags: 285 | yield f"? {atags}\n" 286 | 287 | yield "+ " + bline 288 | if btags: 289 | yield f"? {btags}\n" 290 | 291 | def _html_format(self, aline, bline, atags, btags): 292 | r""" 293 | Format "?" output and deal with tabs. 294 | Example: 295 | >>> d = Differ() 296 | >>> results = d._qformat('\tabcDefghiJkl\n', '\tabcdefGhijkl\n', 297 | ... ' ^ ^ ^ ', ' ^ ^ ^ ') 298 | >>> for line in results: print(repr(line)) 299 | ... 300 | '- \tabcDefghiJkl\n' 301 | '? \t ^ ^ ^\n' 302 | '+ \tabcdefGhijkl\n' 303 | '? \t ^ ^ ^\n' 304 | """ 305 | atags = _keep_original_ws(aline, atags).rstrip() 306 | btags = _keep_original_ws(bline, btags).rstrip() 307 | 308 | if atags: 309 | aline = "- " + reformat(atags, aline) 310 | # makes the whole line red in case there is only a change 311 | #aline = "- " + "[red bold underline]" + reformat(atags, aline) + "[/red bold underline]" 312 | yield aline 313 | 314 | if btags: 315 | bline = "+ " + reformat(btags, bline) 316 | # makes the whole line green in case there is only a change 317 | # bline = "+ " + "[green bold underline]" + reformat(btags, bline) + "[/green bold underline]" 318 | yield bline 319 | 320 | def reformat(tags, line): 321 | temp_line = "" 322 | for i in range(0, len(tags)): 323 | if tags[i] == "^": 324 | temp_line += "[yellow bold underline]" + line[i] + "[/yellow bold underline]" 325 | else: 326 | temp_line += line[i] 327 | 328 | temp_line += line[len(tags):] 329 | return temp_line 330 | 331 | def _keep_original_ws(s, tag_s): 332 | """Replace whitespace with the original whitespace characters in `s`""" 333 | return ''.join( 334 | c if tag_c == " " and c.isspace() else tag_c 335 | for c, tag_c in zip(s, tag_s) 336 | ) 337 | -------------------------------------------------------------------------------- /src/idd/cli.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import argparse 4 | import sys 5 | import os 6 | from asyncio import sleep 7 | 8 | from textual import on 9 | from textual import events 10 | from textual.app import App, ComposeResult 11 | from textual.reactive import Reactive 12 | from textual.widgets import Input 13 | from textual.containers import Horizontal, Vertical 14 | 15 | from idd.diff_driver import DiffDriver 16 | 17 | from idd.ui.footer import Footer 18 | from idd.ui.header import Header 19 | from idd.ui.scrollable_area import TextScrollView 20 | 21 | 22 | class DiffDebug(App): 23 | CSS_PATH = os.path.join(os.path.dirname(__file__), "layout.tcss") 24 | 25 | current_index: Reactive[int] = Reactive(-1) 26 | tab_index = ["parallel_command_bar", "base_command_bar", "regressed_command_bar"] 27 | show_bar = Reactive(False) 28 | 29 | debugger_command_input_box: None 30 | diff_driver = DiffDriver() 31 | base_args = "" 32 | regression_args = "" 33 | 34 | diff_area1 = TextScrollView(title="Base Diff", component_id="diff-area1") 35 | diff_area2 = TextScrollView(title="Regression Diff", component_id = "diff-area2") 36 | diff_frames1 = TextScrollView(title="Base Stackframe", component_id = "diff-frames1") 37 | diff_frames2 = TextScrollView(title="Regression Stackframe", component_id = "diff-frames2") 38 | diff_locals1 = TextScrollView(title="Base Locals", component_id = "diff-locals1") 39 | diff_locals2 = TextScrollView(title="Regression Locals", component_id = "diff-locals2") 40 | diff_args1 = TextScrollView(title="Base Args", component_id = "diff-args1") 41 | diff_args2 = TextScrollView(title="Regression Args", component_id = "diff-args2") 42 | diff_asm1 = TextScrollView(title="Base Asm", component_id = "diff-asm1") 43 | diff_asm2 = TextScrollView(title="Regression Asm", component_id = "diff-asm2") 44 | diff_reg1 = TextScrollView(title="Base Registers", component_id = "diff-reg1") 45 | diff_reg2 = TextScrollView(title="Regression Registers", component_id = "diff-reg2") 46 | base_input_bar = Input(placeholder="Input for base debuggee...", id="base-input-bar") 47 | regressed_input_bar = Input(placeholder="Input for regression debuggee...", id="regressed-input-bar") 48 | 49 | #executable_path1 = DiffArea(title="base executable and arguments", value="") 50 | #executable_path2 = DiffArea(title="regression executable and arguments", value="") 51 | 52 | # Command input bars 53 | parallel_command_bar = Input(placeholder="Enter your command here...", name="command", id="parallel-command-bar") 54 | base_command_bar = Input(placeholder="Enter your base command here...", name="base_command_bar", id="base-command-bar") 55 | regressed_command_bar = Input(placeholder="Enter your regression command here...", name="regressed_command_bar", id="regressed-command-bar") 56 | 57 | def __init__(self, disable_asm=False, disable_registers=False, only_base=False): 58 | super().__init__() 59 | self.only_base = only_base 60 | self.disable_asm = disable_asm 61 | self.disable_registers = disable_registers 62 | self.common_history = [""] 63 | self.common_history_index = 0 64 | self.base_history = [""] 65 | self.base_history_index = 0 66 | self.regressed_history = [""] 67 | self.regressed_history_index = 0 68 | self.base_awaiting_shown = False 69 | self.regressed_awaiting_shown = False 70 | 71 | async def on_mount(self) -> None: 72 | #self.set_interval(0.1, self.refresh_debuggee_status) 73 | self.set_interval(0.25, self.watch_debuggee_output) 74 | 75 | 76 | async def set_command_result(self, version) -> None: 77 | state = Debugger.get_state(version) 78 | 79 | if comparator == "lldb": 80 | await self.set_debugee_console_output(version) 81 | await self.set_pframes_result(state, version) 82 | await self.set_pargs_result(state, version) 83 | await self.set_plocals_result(state, version) 84 | if not self.disable_asm: 85 | await self.set_pasm_result(state, version) 86 | if not self.disable_registers: 87 | await self.set_pregisters_result(state, version) 88 | 89 | async def set_common_command_result(self, command_result) -> None: 90 | if command_result: 91 | raw_base_contents = command_result["base"] 92 | raw_regression_contents = command_result["regressed"] 93 | 94 | await self.compare_contents(raw_base_contents, raw_regression_contents) 95 | 96 | state = Debugger.get_state() 97 | 98 | await self.set_pframes_command_result(state) 99 | await self.set_pargs_command_result(state) 100 | await self.set_plocals_command_result(state) 101 | if comparator == "lldb": 102 | await self.set_debugee_console_output() 103 | if not self.disable_asm: 104 | await self.set_pasm_command_result(state) 105 | if not self.disable_registers: 106 | await self.set_pregisters_command_result(state) 107 | 108 | #calls = Debugger.get_current_calls() 109 | 110 | async def set_debugee_console_output(self, target=None): 111 | if target == "base": 112 | result = Debugger.get_console_output(target) 113 | self.diff_area1.append(result) 114 | elif target == "regressed": 115 | result = Debugger.get_console_output(target) 116 | self.diff_area2.append(result) 117 | else: 118 | result = Debugger.get_console_output() 119 | await self.compare_contents(result["base"], result["regressed"]) 120 | 121 | async def compare_contents(self, raw_base_contents, raw_regression_contents): 122 | if raw_base_contents or raw_regression_contents: 123 | diff1 = self.diff_driver.get_diff(raw_base_contents, raw_regression_contents, "base") 124 | self.diff_area1.append(diff1) 125 | 126 | diff2 = self.diff_driver.get_diff(raw_regression_contents, raw_base_contents, "regressed") 127 | self.diff_area2.append(diff2) 128 | 129 | async def set_pframes_result(self, state, version) -> None: 130 | if state == None or "stack_frames" not in state: 131 | return 132 | 133 | file_contents = state["stack_frames"] 134 | if version == "base": 135 | self.diff_frames1.text(file_contents) 136 | else: 137 | self.diff_frames2.text(file_contents) 138 | 139 | async def set_pframes_command_result(self, state) -> None: 140 | if state["base"] == None or "stack_frames" not in state["base"] or state["regressed"] == None or "stack_frames" not in state["regressed"]: 141 | return 142 | 143 | base_file_contents = state["base"]["stack_frames"] 144 | regressed_file_contents = state["regressed"]["stack_frames"] 145 | 146 | diff1 = self.diff_driver.get_diff(base_file_contents, regressed_file_contents, "base") 147 | self.diff_frames1.text(diff1) 148 | 149 | diff2 = self.diff_driver.get_diff(regressed_file_contents, base_file_contents, "regressed") 150 | self.diff_frames2.text(diff2) 151 | 152 | async def set_plocals_result(self, state, version) -> None: 153 | if state == None or "locals" not in state: 154 | return 155 | 156 | file_contents = state["locals"] 157 | if version == "base": 158 | self.diff_locals1.text(file_contents) 159 | else: 160 | self.diff_locals2.text(file_contents) 161 | 162 | 163 | async def set_plocals_command_result(self, state) -> None: 164 | if state["base"] == None or "locals" not in state["base"] or state["regressed"] == None or "locals" not in state["regressed"]: 165 | return 166 | 167 | base_file_contents = state["base"]["locals"] 168 | regressed_file_contents = state["regressed"]["locals"] 169 | 170 | diff1 = self.diff_driver.get_diff(base_file_contents, regressed_file_contents, "base") 171 | self.diff_locals1.text(diff1) 172 | 173 | diff2 = self.diff_driver.get_diff(regressed_file_contents, base_file_contents, "regressed") 174 | self.diff_locals2.text(diff2) 175 | 176 | async def set_pargs_result(self, state, version) -> None: 177 | if state == None or "args" not in state: 178 | return 179 | 180 | file_contents = state["args"] 181 | if version == "base": 182 | self.diff_args1.text(file_contents) 183 | else: 184 | self.diff_args2.text(file_contents) 185 | 186 | async def set_pargs_command_result(self, state) -> None: 187 | if state["base"] == None or "args" not in state["base"] or state["regressed"] == None or "args" not in state["regressed"]: 188 | return 189 | 190 | base_file_contents = state["base"]["args"] 191 | regressed_file_contents = state["regressed"]["args"] 192 | 193 | diff1 = self.diff_driver.get_diff(base_file_contents, regressed_file_contents, "base") 194 | self.diff_args1.text(diff1) 195 | 196 | diff2 = self.diff_driver.get_diff(regressed_file_contents, base_file_contents, "regressed") 197 | self.diff_args2.text(diff2) 198 | 199 | async def set_pasm_result(self, state, version) -> None: 200 | if state == None or "instructions" not in state: 201 | return 202 | 203 | file_contents = state["instructions"] 204 | if version == "base": 205 | self.diff_asm1.text(file_contents) 206 | else: 207 | self.diff_asm2.text(file_contents) 208 | 209 | async def set_pasm_command_result(self, state) -> None: 210 | if state["base"] == None or "instructions" not in state["base"] or state["regressed"] == None or "instructions" not in state["regressed"]: 211 | return 212 | 213 | base_file_contents = state["base"]["instructions"] 214 | regressed_file_contents = state["regressed"]["instructions"] 215 | 216 | diff1 = self.diff_driver.get_diff(base_file_contents, regressed_file_contents, "base") 217 | self.diff_asm1.text(diff1) 218 | 219 | diff2 = self.diff_driver.get_diff(regressed_file_contents, base_file_contents, "regressed") 220 | self.diff_asm2.text(diff2) 221 | 222 | async def set_pregisters_result(self, state, version) -> None: 223 | if state == None or "registers" not in state: 224 | return 225 | 226 | file_contents = state["registers"] 227 | if version == "base": 228 | self.diff_reg1.text(file_contents) 229 | else: 230 | self.diff_reg2.text(file_contents) 231 | 232 | async def set_pregisters_command_result(self, state) -> None: 233 | if state["base"] == None or "registers" not in state["base"] or state["regressed"] == None or "registers" not in state["regressed"]: 234 | return 235 | 236 | base_file_contents = state["base"]["registers"] 237 | regressed_file_contents = state["regressed"]["registers"] 238 | 239 | diff1 = self.diff_driver.get_diff(base_file_contents, regressed_file_contents, "base") 240 | self.diff_reg1.text(diff1) 241 | 242 | diff2 = self.diff_driver.get_diff(regressed_file_contents, base_file_contents, "regressed") 243 | self.diff_reg2.text(diff2) 244 | 245 | async def watch_debuggee_output(self) -> None: 246 | if hasattr(Debugger, "base_gdb_instance"): 247 | base_output = Debugger.base_gdb_instance.pop_debuggee_output() 248 | if base_output: 249 | self.diff_area1.append(base_output) 250 | if Debugger.base_gdb_instance.is_waiting_for_input(): 251 | if not self.base_awaiting_shown: 252 | self.diff_area1.append("[awaiting input]") 253 | self.base_awaiting_shown = True 254 | else: 255 | self.base_awaiting_shown = False 256 | 257 | if hasattr(Debugger, "regressed_gdb_instance"): 258 | regressed_output = Debugger.regressed_gdb_instance.pop_debuggee_output() 259 | if regressed_output: 260 | self.diff_area2.append(regressed_output) 261 | if Debugger.regressed_gdb_instance.is_waiting_for_input(): 262 | if not self.regressed_awaiting_shown: 263 | self.diff_area2.append("[awaiting input]") 264 | self.regressed_awaiting_shown = True 265 | else: 266 | self.regressed_awaiting_shown = False 267 | 268 | def compose(self) -> ComposeResult: 269 | """Compose the layout of the application.""" 270 | if self.only_base: 271 | with Vertical(): 272 | yield Header() 273 | with Horizontal(classes="base_only_row1"): 274 | yield self.diff_frames1 275 | with Horizontal(classes="base_only_row2"): 276 | with Horizontal(): 277 | yield self.diff_locals1 278 | yield self.diff_args1 279 | if not self.disable_registers and not self.disable_asm: 280 | with Vertical(): 281 | with Horizontal(): 282 | yield self.diff_reg1 283 | with Horizontal(): 284 | yield self.diff_asm1 285 | elif not self.disable_asm: 286 | with Vertical(): 287 | yield self.diff_asm1 288 | elif not self.disable_registers: 289 | with Vertical(): 290 | yield self.diff_reg1 291 | with Horizontal(classes="base_only_row3"): 292 | yield self.diff_area1 293 | with Horizontal(classes="base_only_row4"): 294 | yield self.base_command_bar 295 | return 296 | 297 | with Vertical(): 298 | yield Header() 299 | 300 | with Horizontal(classes="row1"): 301 | yield self.diff_frames1 302 | yield self.diff_frames2 303 | 304 | with Horizontal(classes="row2"): 305 | with Horizontal(): 306 | yield self.diff_locals1 307 | yield self.diff_args1 308 | if not self.disable_registers and not self.disable_asm: 309 | with Vertical(): 310 | with Horizontal(): 311 | yield self.diff_reg1 312 | with Horizontal(): 313 | yield self.diff_asm1 314 | elif not self.disable_asm: 315 | with Vertical(): 316 | yield self.diff_asm1 317 | elif not self.disable_registers: 318 | with Vertical(): 319 | yield self.diff_reg1 320 | 321 | with Horizontal(): 322 | yield self.diff_locals2 323 | yield self.diff_args1 324 | yield self.diff_args2 325 | if not self.disable_registers and not self.disable_asm: 326 | with Vertical(): 327 | with Horizontal(): 328 | yield self.diff_reg2 329 | with Horizontal(): 330 | yield self.diff_asm2 331 | elif not self.disable_asm: 332 | with Vertical(): 333 | yield self.diff_asm2 334 | elif not self.disable_registers: 335 | with Vertical(): 336 | yield self.diff_reg2 337 | 338 | #yield self.executable_path1 339 | #yield self.executable_path2 340 | 341 | with Horizontal(classes="row3"): 342 | with Vertical(): 343 | yield self.base_command_bar 344 | with Vertical(): 345 | yield self.regressed_command_bar 346 | 347 | with Horizontal(classes="row4"): 348 | yield self.diff_area1 349 | yield self.diff_area2 350 | 351 | with Horizontal(classes="row5"): 352 | yield self.parallel_command_bar 353 | 354 | self.parallel_command_bar.focus() 355 | 356 | yield Footer() 357 | 358 | @on(Input.Submitted) 359 | async def execute_debugger_command(self, event: Input.Changed) -> None: 360 | # Updating the UI to show the reasons why validation failed 361 | if event.control.id == 'parallel-command-bar': 362 | if self.parallel_command_bar.value == "quit" or \ 363 | self.parallel_command_bar.value == "exit": 364 | Debugger.terminate() 365 | exit(0) 366 | 367 | if self.parallel_command_bar.value.startswith("stdin "): 368 | Debugger.insert_stdin(self.parallel_command_bar.value[6:] + "\n") 369 | self.diff_area1.append([self.parallel_command_bar.value[6:]]) 370 | self.diff_area2.append([self.parallel_command_bar.value[6:]]) 371 | 372 | result = {} 373 | 374 | elif self.parallel_command_bar.value != "": 375 | result = Debugger.run_parallel_command(self.parallel_command_bar.value) 376 | 377 | self.diff_area1.append([self.parallel_command_bar.value]) 378 | self.diff_area2.append([self.parallel_command_bar.value]) 379 | 380 | # append to history 381 | self.common_history.append(self.parallel_command_bar.value) 382 | self.common_history_index = len(self.common_history) 383 | 384 | else: 385 | # execute last command from history 386 | result = Debugger.run_parallel_command(self.common_history[-1]) 387 | 388 | self.diff_area1.append([self.parallel_command_bar.value]) 389 | self.diff_area2.append([self.parallel_command_bar.value]) 390 | 391 | if result: 392 | await self.set_common_command_result(result) 393 | 394 | self.parallel_command_bar.value = "" 395 | 396 | elif event.control.id == 'base-command-bar': 397 | if self.only_base and (self.base_command_bar.value == "exit" or self.base_command_bar.value == "quit"): 398 | Debugger.terminate() 399 | exit(0) 400 | 401 | if self.base_command_bar.value.startswith("stdin "): 402 | Debugger.insert_stdin_single(self.base_command_bar.value[6:] + "\n", "base") 403 | self.diff_area1.append([self.base_command_bar.value[6:]]) 404 | 405 | elif self.base_command_bar.value != "": 406 | result = Debugger.run_single_command(self.base_command_bar.value, "base") 407 | self.diff_area1.append([self.base_command_bar.value]) 408 | self.diff_area1.append(result) 409 | 410 | # append to history 411 | self.base_history.append(self.base_command_bar.value) 412 | self.base_history_index = len(self.base_history) 413 | 414 | else: 415 | # execute last command from history 416 | result = Debugger.run_single_command(self.base_history[-1], "base") 417 | 418 | self.diff_area1.append([self.base_command_bar.value]) 419 | self.diff_area1.append(result) 420 | 421 | await self.set_command_result("base") 422 | 423 | self.base_command_bar.value = "" 424 | 425 | elif event.control.id == 'regressed-command-bar': 426 | if self.regressed_command_bar.value.startswith("stdin "): 427 | Debugger.insert_stdin_single(self.regressed_command_bar.value[6:] + "\n", "regressed") 428 | self.diff_area2.append([self.regressed_command_bar.value[6:]]) 429 | 430 | elif self.regressed_command_bar.value != "": 431 | result = Debugger.run_single_command(self.regressed_command_bar.value, "regressed") 432 | self.diff_area2.append([self.regressed_command_bar.value]) 433 | self.diff_area2.append(result) 434 | 435 | # append to history 436 | self.regressed_history.append(self.regressed_command_bar.value) 437 | self.regressed_history_index = len(self.regressed_history) 438 | 439 | else: 440 | # execute last command from history 441 | result = Debugger.run_single_command(self.regressed_history[-1], "regressed") 442 | self.diff_area2.append([self.regressed_command_bar.value]) 443 | self.diff_area2.append(result) 444 | 445 | await self.set_command_result("regressed") 446 | 447 | self.regressed_command_bar.value = "" 448 | 449 | elif event.input.id == "base-input-bar": 450 | Debugger.base_gdb_instance.send_input_to_debuggee(event.value) 451 | 452 | elif event.input.id == "regressed-input-bar": 453 | Debugger.regressed_gdb_instance.send_input_to_debuggee(event.value) 454 | 455 | async def on_key(self, event: events.Key) -> None: 456 | if self.focused is None: 457 | return 458 | 459 | if self.focused.id == "parallel-command-bar": 460 | if event.key == "up": 461 | self.common_history_index = (self.common_history_index - 1) % len(self.common_history) 462 | elif event.key == "down": 463 | self.common_history_index = (self.common_history_index + 1) % len(self.common_history) 464 | else: 465 | return 466 | 467 | self.parallel_command_bar.value = self.common_history[self.common_history_index] 468 | 469 | elif self.focused.id == "base-command-bar": 470 | if event.key == "up": 471 | self.base_history_index = (self.base_history_index - 1) % len(self.base_history) 472 | elif event.key == "down": 473 | self.base_history_index = (self.base_history_index + 1) % len(self.base_history) 474 | else: 475 | return 476 | 477 | self.base_command_bar.value = self.base_history[self.base_history_index] 478 | 479 | elif self.focused.id == "regressed-command-bar": 480 | if event.key == "up": 481 | self.regressed_history_index = (self.regressed_history_index - 1) % len(self.regressed_history) 482 | elif event.key == "down": 483 | self.regressed_history_index = (self.regressed_history_index + 1) % len(self.regressed_history) 484 | else: 485 | return 486 | 487 | self.regressed_command_bar.value = self.regressed_history[self.regressed_history_index] 488 | 489 | 490 | Debugger = None 491 | comparator = None 492 | def main() -> None: 493 | global Debugger, comparator 494 | 495 | parser = argparse.ArgumentParser(description='Diff Debug for simple debugging!') 496 | 497 | base_arg_group = parser.add_mutually_exclusive_group() 498 | regressed_arg_group = parser.add_mutually_exclusive_group() 499 | 500 | parser.add_argument('-c','--comparator', help='Choose a comparator', default='gdb') 501 | base_arg_group.add_argument('-ba','--base-args', help='Base executable args', default="", nargs='+') 502 | # base_arg_group.add_argument('-bpid','--base-processid', help='Base process ID', default=None) 503 | parser.add_argument('-bs','--base-script-path', help='Base preliminary script file path', default=None, nargs='+') 504 | regressed_arg_group.add_argument('-ra','--regression-args', help='Regression executable args', default="", nargs='+') 505 | # regressed_arg_group.add_argument('-rpid','--regression-processid', help='Regression process ID', default=None) 506 | parser.add_argument('-rs','--regression-script-path', help='Regression preliminary script file path', default=None, nargs='+') 507 | parser.add_argument('-r','--remote_host', help='The host of the remote server', default='localhost') 508 | parser.add_argument('-p','--platform', help='The platform of the remote server: macosx, linux', default='linux') 509 | parser.add_argument('-t','--triple', help='The target triple: x86_64-apple-macosx, x86_64-gnu-linux', default='x86_64-gnu-linux') 510 | parser.add_argument('-lvf','--local-vars-filter', help='Filter for the local vars: local-vars-filter', default='x86_64-gnu-linux') 511 | parser.add_argument('--disable-assembly', help='Disables the assembly panel', default=False, action='store_true') 512 | parser.add_argument('--disable-registers', help='Disables the registers panel', default=False, action='store_true') 513 | args = vars(parser.parse_args()) 514 | 515 | comparator = args['comparator'] 516 | ba = ' '.join(args['base_args']) 517 | bpid = None # args['base_processid'] 518 | bs = ' '.join(args['base_script_path']) if args['base_script_path'] is not None else None 519 | ra = ' '.join(args['regression_args']) 520 | rpid = None # args['regression_processid'] 521 | rs = ' '.join(args['regression_script_path']) if args["regression_script_path"] is not None else None 522 | base_only = False 523 | 524 | if comparator == 'gdb': 525 | from idd.debuggers.gdb.gdb_mi_driver import GDBMiDebugger, IDDGdbController 526 | 527 | if ra == "" and rpid is None: 528 | Debugger = IDDGdbController(ba, bpid, bs) 529 | base_only = True 530 | else: 531 | Debugger = GDBMiDebugger(ba, bs, ra, rs, base_pid=bpid, regression_pid=rpid) 532 | 533 | elif comparator == 'lldb': 534 | from idd.debuggers.lldb.lldb_driver import LLDBParallelDebugger, LLDBDebugger 535 | from idd.debuggers.lldb.lldb_new_driver import LLDBNewDriver 536 | 537 | if ra == "" and rpid is None: 538 | Debugger = LLDBDebugger(ba, bpid) 539 | base_only = True 540 | else: 541 | Debugger = LLDBNewDriver(ba, bpid, ra, rpid) 542 | else: 543 | sys.exit("Invalid comparator set") 544 | 545 | disable_registers = args["disable_registers"] 546 | disable_assembly = args["disable_assembly"] 547 | dd = DiffDebug(disable_assembly, disable_registers, base_only) 548 | dd.run() 549 | 550 | if __name__ == "__main__": 551 | main() 552 | --------------------------------------------------------------------------------