Improve TUI handling and documentation (#2446)

* Fix ansi color truncation in TUI windows

All remaining ansi escape codes after the substring of real characters weren't printed.
This caused some colors to linger over to the next line.

* Add two default TUI layouts

One including the sourcecode section and one without.
The allows to try the TUI quickly by selecting one of the layouts: `layout pwndbg` or `layout pwndbg_code`.

* Don't use \t tabs in context threads section

Horizontal scroll in TUI mode would jump around causing visual glitches. Use spaces instead.

* Add warning when section is in TUI layout but not in 'context-sections'

The section won't be updated automatically on stop.

* Update FEATURES docs including screenshot

* Don't create TUI windows when GDB is compiled without it

Don't stop pwndbg from loading on gdb without tui integration.

* Fix redraw of all windows after toggling TUI mode

When switching back to TUI mode, the contextoutput would be sent before a TUI window's `render` function was called. This caused some sections to be printed normally in the cmd window instead of being sent to their TUI window.

Keep a list of all open context TUI windows and redirect contextoutput for all of them once the first window is rerendered instead of waiting for them to be rendered too.

Also fix the _static_enabled class variable usage.
This commit is contained in:
peace-maker 2024-09-23 01:57:44 +02:00 committed by GitHub
parent 88c363a65e
commit 3506114d5c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 104 additions and 28 deletions

View File

@ -71,14 +71,21 @@ import splitmind
end
```
The context sections are available as native gdb TUI windows as well as `pwndbg_[sectionname]` windows.
#### GDB TUI
The context sections are available as native [GDB TUI](https://sourceware.org/gdb/current/onlinedocs/gdb.html/TUI.html) windows named `pwndbg_[sectionname]`.
Try creating a layout and selecting it:
There are some predefined layouts coming with pwndbg which you can select using `layout pwndbg` or `layout pwndbg_code`.
To create [your own layout](https://sourceware.org/gdb/current/onlinedocs/gdb.html/TUI-Commands.html) and selecting it use normal `tui new-layout` syntax like:
```
tui new-layout pwndbg {-horizontal { { -horizontal { pwndbg_code 2 pwndbg_disasm 8 } 2 { { -horizontal pwndbg_legend 8 pwndbg_control 2 } 1 pwndbg_regs 6 pwndbg_stack 6 } 3 } 7 cmd 3 } 3 { pwndbg_backtrace 1 } 1 } 1 status 1
layout pwndbg
tui new-layout pwndbg_custom {-horizontal { { -horizontal { pwndbg_code 1 pwndbg_disasm 1 } 2 { {-horizontal pwndbg_legend 8 pwndbg_control 2 } 1 pwndbg_regs 6 pwndbg_stack 6 } 3 } 7 cmd 3 } 3 { pwndbg_backtrace 2 pwndbg_threads 1 pwndbg_expressions 2 } 1 } 1 status 1
layout pwndbg_custom
```
![](caps/context_tui.png)
Use `focus cmd` to focus the command window and have the arrow keys scroll through the command history again. `tui disable` to disable TUI mode and go back to CLI mode when running commands with longer output. `ctrl-x + a` toggles between TUI and CLI mode quickly. Hold shift to ignore the TUI mouse integration and use the mouse normally to select text or copy data.
### Watch Expressions
You can add expressions to be watched by the context.

BIN
caps/context_tui.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 232 KiB

View File

@ -1139,24 +1139,33 @@ def context_threads(with_banner=True, target=sys.stdout, width=None):
if len(displayed_threads) < 2:
return []
out = [pwndbg.ui.banner(f"threads ({len(all_threads)} total)", target=target, width=width)]
out = (
[pwndbg.ui.banner(f"threads ({len(all_threads)} total)", target=target, width=width)]
if with_banner
else []
)
max_name_length = 0
max_global_num_len = 0
for thread in displayed_threads:
name = thread.name or ""
if len(name) > max_name_length:
max_name_length = len(name)
if len(str(thread.global_num)) > max_global_num_len:
max_global_num_len = len(str(thread.global_num))
for thread in filter(lambda t: t.is_valid(), displayed_threads):
selected = "" if thread is original_thread else " "
name = thread.name if thread.name is not None else ""
padding = max_name_length - len(name)
name_padding = max_name_length - len(name)
global_num_padding = max(2, max_global_num_len - len(str(thread.global_num)))
status = get_thread_status(thread)
line = (
f" {selected} {thread.global_num}\t"
f" {selected} {thread.global_num} "
f"{' ' * global_num_padding}"
f'"{pwndbg.color.cyan(name)}" '
f'{" " * padding}'
f'{" " * name_padding}'
f"{status}: "
)

View File

@ -997,6 +997,8 @@ class GDB(pwndbg.dbg_mod.Debugger):
except gdb.error:
pass
pwndbg.gdblib.tui.setup()
# Reading Comment file
from pwndbg.commands import comments

View File

@ -1,4 +1,42 @@
from __future__ import annotations
import gdb
import pwndbg.gdblib.tui.context
import pwndbg.gdblib.tui.control
def setup() -> None:
tui_layouts = [
(
"tui new-layout pwndbg "
"{-horizontal "
" { "
" { -horizontal "
" { pwndbg_disasm 1 } 2 "
" { "
" { -horizontal pwndbg_legend 8 pwndbg_control 2 } 1 pwndbg_regs 6 pwndbg_stack 6 "
" } 3 "
" } 7 cmd 3 "
" } 3 { pwndbg_backtrace 2 pwndbg_threads 1 pwndbg_expressions 2 } 1 "
"} 1 status 1"
),
(
"tui new-layout pwndbg_code "
"{-horizontal "
" { "
" { -horizontal "
" { pwndbg_code 1 pwndbg_disasm 1 } 2 "
" { "
" { -horizontal pwndbg_legend 8 pwndbg_control 2 } 1 pwndbg_regs 6 pwndbg_stack 6 "
" } 3 "
" } 7 cmd 3 "
" } 3 { pwndbg_backtrace 2 pwndbg_threads 1 pwndbg_expressions 2 } 1 "
"} 1 status 1"
),
]
for layout in tui_layouts:
try:
gdb.execute(layout)
except gdb.error:
pass

View File

@ -7,6 +7,8 @@ from typing import Pattern
import gdb
import pwndbg
from pwndbg.color import message
from pwndbg.commands.context import context
from pwndbg.commands.context import context_sections
from pwndbg.commands.context import contextoutput
@ -26,7 +28,8 @@ class ContextTUIWindow:
_ansi_escape_regex: Pattern[str]
_enabled: bool
_static_enabled = False
_static_enabled: bool = True
_context_windows: List[ContextTUIWindow] = []
def __init__(self, tui_window: "gdb.TuiWindow", section: str) -> None:
self._tui_window = tui_window
@ -43,8 +46,10 @@ class ContextTUIWindow:
self._enabled = False
self._enable()
gdb.events.before_prompt.connect(self._before_prompt_listener)
ContextTUIWindow._context_windows.append(self)
def close(self) -> None:
ContextTUIWindow._context_windows.remove(self)
if self._enabled:
self._disable()
gdb.events.before_prompt.disconnect(self._before_prompt_listener)
@ -53,6 +58,19 @@ class ContextTUIWindow:
# render is called again after the TUI was disabled
self._verify_enabled_state()
if (
not self._lines
and self._section != "legend"
and self._section not in str(pwndbg.config.context_sections)
):
self._tui_window.write(
message.warn(
f"Section '{self._section}' is not in 'context-sections' and won't be updated automatically."
),
True,
)
return
height = self._tui_window.height
width = self._tui_window.width
start = self._vscroll_start
@ -93,12 +111,10 @@ class ContextTUIWindow:
self.render()
def _enable(self):
_static_enabled = True
self._update()
self._enabled = True
def _disable(self):
_static_enabled = False
self._old_width = 0
resetcontextoutput(self._section)
self._enabled = False
@ -128,13 +144,15 @@ class ContextTUIWindow:
is_valid = self._tui_window.is_valid()
if is_valid:
if not self._enabled:
should_trigger_context = not self._static_enabled
self._enable()
if should_trigger_context and gdb.selected_inferior().pid:
for context_window in ContextTUIWindow._context_windows:
context_window._enable()
if not ContextTUIWindow._static_enabled and pwndbg.dbg.selected_inferior().alive():
context()
ContextTUIWindow._static_enabled = True
else:
if self._enabled:
self._disable()
ContextTUIWindow._static_enabled = False
return is_valid
def _ansi_substr(self, line: str, start_char: int, end_char: int) -> str:
@ -145,12 +163,12 @@ class ContextTUIWindow:
colored_idx = 0
char_count = 0
while colored_idx < len(line):
c = line[colored_end_idx]
c = line[colored_idx]
# collect all ansi escape sequences before the start of the colored substring
# as well as after the end of the colored substring
# skip them while counting the characters to slice
if c == "\x1b":
m = self._ansi_escape_regex.match(line[colored_end_idx:])
m = self._ansi_escape_regex.match(line[colored_idx:])
if m:
colored_idx += m.end()
if char_count < start_char:
@ -175,10 +193,11 @@ class ContextTUIWindow:
)
sections = ["legend"] + [
if hasattr(gdb, "register_window_type"):
sections = ["legend"] + [
section.__name__.replace("context_", "") for section in context_sections.values()
]
for section_name in sections:
]
for section_name in sections:
# https://github.com/python/mypy/issues/12557
target_func: Callable[..., gdb._Window] = (
lambda window, section_name=section_name: ContextTUIWindow(window, section_name)

View File

@ -47,6 +47,7 @@ TIPS: List[str] = [
"Need to `mmap` or `mprotect` memory in the debugee? Use commands with the same name to inject and run such syscalls",
"Use `hi` to see if a an address belongs to a glibc heap chunk",
"Use `contextprev` and `contextnext` to display a previous context output again without scrolling",
"Try splitting the context output into multiple TUI windows using `layout pwndbg` (`tui disable` or `ctrl-x + a` to go back to CLI mode)",
]