From ebbac7703a06b67f2a0119df0861d87bba1b64f9 Mon Sep 17 00:00:00 2001 From: Nikita Vasilev Date: Mon, 27 Jul 2026 11:14:02 +0400 Subject: [PATCH] feat: implement the `list_threads` command --- .lldbinit | 2 +- README.md | 1 + bootstrap | 46 ------------------ bootstrap.sh | 18 ++++--- lldb/commands/DTInspection.py | 91 ++++++++++++++++++++++++++++++++++- 5 files changed, 102 insertions(+), 56 deletions(-) delete mode 100755 bootstrap diff --git a/.lldbinit b/.lldbinit index 7a1a05b..bbc39d8 100644 --- a/.lldbinit +++ b/.lldbinit @@ -1,5 +1,5 @@ # Load the main LLDB initialization script that dynamically loads custom commands -command script import ~/lldb/lldbinit.py +command script import ~/.lldb/lldb/lldbinit.py # Global setting to ensure LLDB doesn't skip the prologue of functions during debugging settings set target.skip-prologue false \ No newline at end of file diff --git a/README.md b/README.md index ca097b7..828a680 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,7 @@ This project enhances the LLDB debugging experience with custom Python commands, | **`pdefaults`** | Dumps `NSUserDefaults` contents as a formatted key-value table. | `[-s suite] [-f filter] [-o]` | | **`hex_dump`** | Reads memory at a specific address and returns a formatted hex dump. | `
[-c count] [-o offset]` | | **`print_frame`** | Displays a detailed low-level architectural snapshot and resolved symbolic context of the current stack frame. | None | +| **`list_threads`** | Iterates through all process threads and displays their status, name, and queue info. | None | ### Useful Aliases & Regex Commands diff --git a/bootstrap b/bootstrap deleted file mode 100755 index ef34d90..0000000 --- a/bootstrap +++ /dev/null @@ -1,46 +0,0 @@ -#!/usr/bin/env bash - -# -# dotfiles - bootstrap -# Copyright © 2025 Space Code. All rights reserved. -# -# This script synchronizes the dotfiles from the repository to the user's -# home directory. It is identical to bootstrap.sh and provided as an -# alternative command name. -# - -# Navigate to the directory where the script is located -cd "$(dirname "${BASH_SOURCE[0]}")" || exit - -# Pull the latest changes from the main branch -git pull origin main - -# Function to update the home directory with the dotfiles -update() { - # Synchronize configuration files using rsync - rsync --exclude ".git/" \ - --exclude ".DS_Store" \ - --exclude "bootstrap.sh" \ - --exclude "README.md" \ - --exclude "LICENSE" \ - --exclude ".venv" \ - --exclude ".ruff_cache/" \ - -avh --no-perms . ~ - - # Reload the bash profile - source ~/.bash_profile -} - -# Handle command-line arguments and confirmation -if [[ "$1" == "--force" || "$1" == "-f" ]]; then - update -else - read -rp "This may overwrite existing files in your home directory. Are you sure? (y/n) " response - echo - if [[ "$response" =~ ^[Yy]$ ]]; then - update - fi -fi - -# Clean up -unset -f update diff --git a/bootstrap.sh b/bootstrap.sh index 5ffd633..3fb0eeb 100755 --- a/bootstrap.sh +++ b/bootstrap.sh @@ -17,21 +17,23 @@ git pull origin main # Function to update the home directory with the dotfiles update() { + mkdir -p ~/.lldb + + rsync -avh --no-perms ./.lldbinit ~ + # rsync parameters: # -a: archive mode (preserves permissions, symlinks, etc.) # -v: verbose output # -h: human-readable numbers # --no-perms: do not preserve permissions (useful for cross-OS sync) # --exclude: skip files that should not be in the home directory - rsync --exclude ".git/" \ - --exclude ".DS_Store" \ - --exclude "bootstrap.sh" \ - --exclude "README.md" \ - --exclude "LICENSE" \ - -avh --no-perms . ~ + rsync -avh --no-perms \ + --exclude=".DS_Store" \ + --exclude="__pycache__/" \ + --exclude="*.pyc" \ + ./lldb ~/.lldb/ - # Reload the bash profile to apply environment changes immediately - source ~/.bash_profile + echo "✅ LLDB scripts updated successfully in ~/.lldb/" } # Check if the --force or -f flag is provided to skip the confirmation prompt diff --git a/lldb/commands/DTInspection.py b/lldb/commands/DTInspection.py index cdc3801..36702a1 100644 --- a/lldb/commands/DTInspection.py +++ b/lldb/commands/DTInspection.py @@ -17,7 +17,7 @@ def commands(): Returns a list of custom LLDB command instances defined in this module. The registration logic in lldbinit.py calls this function. """ - return [DTHexDumpCommand(), DTPrintFrameCommand()] + return [DTHexDumpCommand(), DTPrintFrameCommand(), DTListThreadsCommand()] class DTHexDumpCommand(bc.BaseCommand): @@ -272,3 +272,92 @@ def run(self, args, options): print("=" * 80 + "\n") return + + +class DTListThreadsCommand(bc.BaseCommand): + """LLDB command class to inspect and list process threads.""" + + def name(self): + """Returns the command name as it will be used in the LLDB console.""" + return "list_threads" + + def description(self): + """Returns a short description of the command's functionality.""" + return "Iterates through all process threads and displays their status, name, and queue info." + + def run(self, args, options): + """ + The main execution logic for the list_threads command. + """ + + target = lldb.debugger.GetSelectedTarget() + process = target.GetProcess() + + if not process or not process.IsValid(): + print("error: valid active process not found") + return + + output = ( + f"\n{'ID':<6} {'INDEX':<7} {'NAME':<20} {'STATE':<15} {'QUEUE/PRIORITY'}\n" + ) + output += "-" * 65 + "\n" + + num_threads = process.GetNumThreads() + + for i in range(num_threads): + thread = process.GetThreadAtIndex(i) + if not thread.IsValid(): + continue + + thread_id = f"0x{thread.GetThreadID():x}" + index = thread.GetIndexID() + + name = thread.GetName() + + if not name: + name = "" + + state_str = get_state_string(thread) + + queue_name = thread.GetQueueName() + priority_info = queue_name if queue_name else "N/A" + + is_selected = "*" if thread == process.GetSelectedThread() else " " + + output += f"{is_selected}{thread_id:<5} {index:<7} {name:<20} {state_str:<15} {priority_info}\n" + + print(output) + + return + + +def get_state_string(thread): + """ + Converts thread state enum and stop reason into a human-readable string. + Avoids calling `thread.GetStopDescription()` to prevent thread hangs/deadlocks. + """ + if thread.IsStopped(): + reason = thread.GetStopReason() + reason_str = stop_reason_to_string(reason) + return f"Stopped ({reason_str})" if reason_str else "Stopped" + elif thread.IsSuspended(): + return "Suspended" + else: + return "Running" + + +def stop_reason_to_string(reason): + """ + Maps lldb.eStopReason enum to a readable short string. + """ + reasons = { + lldb.eStopReasonBreakpoint: "breakpoint", + lldb.eStopReasonWatchpoint: "watchpoint", + lldb.eStopReasonSignal: "signal", + lldb.eStopReasonException: "exception", + lldb.eStopReasonExec: "exec", + lldb.eStopReasonPlanComplete: "step", + lldb.eStopReasonThreadExiting: "exiting", + lldb.eStopReasonInstrumentation: "instrumentation", + } + return reasons.get(reason, "")