├── .gitignore ├── CHANGELOG.md ├── Cargo.lock ├── Cargo.toml ├── IPC.md ├── LICENSE ├── README.md ├── build.rs ├── screenshot.png ├── src ├── completion │ └── mod.rs ├── gdb.rs ├── gdb_expression_parsing │ ├── ast.rs │ ├── lexer.rs │ ├── mod.rs │ └── parser.rs ├── gdbmi │ ├── commands.rs │ ├── mod.rs │ └── output.rs ├── ipc.rs ├── layout.rs ├── main.rs └── tui │ ├── commands.rs │ ├── console.rs │ ├── expression_table.rs │ ├── mod.rs │ ├── srcview.rs │ └── tui.rs └── ugdbception.png /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [0.1.12] - 2025-03-09 4 | ### Changed 5 | - Add line debug info in release mode 6 | ### Fixed 7 | - Crash on backspace after non-ASCII character in terminal (via unsegen-terminal) 8 | 9 | ## [0.1.11] - 2022-01-14 10 | ### Changed 11 | - Ignore leading whitespace in commands. 12 | ### Added 13 | - Support for some more ansi escape codes in terminal. 14 | ### Fixed 15 | - Crash when using !show command. 16 | - Hang when using shell command (`shell` is disallowed now). 17 | - Fix crash due to bug in unsegen_terminal 18 | 19 | ## [0.1.10] - 2020-06-03 20 | ### Changed 21 | - Reduce dependencies and thus compile time. 22 | - Make expression table scrollable. 23 | - Reduce updates to expression table during editing. 24 | ### Added 25 | - Add cli history search. 26 | - Make layout configurable. 27 | - Add !layout command. 28 | - Add ability to change expression integer format in expression table. 29 | - Add hotkey (ctrl-w) for adding watchpoints for expressions in expression table. 30 | - Add !show command to explicitly load files in the pager. 31 | - Add show_file ipc command. 32 | - Add rr-path flag for specifying alternate rr binary. 33 | 34 | ## [0.1.9] - 2020-11-14 35 | ### Changed 36 | - Generate gdb completions dynamically. 37 | ### Added 38 | - Add rr support. 39 | - Allow initial expression table entries to be specified using -e. 40 | 41 | ## [0.1.8] - 2020-07-15 42 | ### Changed 43 | - Remove libgit2 build dependency. 44 | ### Fixed 45 | - Compilation failing for arm-unknown-linux-* (via dependency). 46 | - Bold style not reseting on some terminals. 47 | 48 | ## [0.1.7] - 2020-04-03 49 | ### Fixed 50 | - Breakpoint message parsing for newer versions of gdb. 51 | ### Added 52 | - Warning when trying to disassemble source files when gdb is busy. 53 | 54 | ## [0.1.6] - 2020-02-03 55 | ### Fixed 56 | - Source view would sometimes still show an outdated version of the displayed file. 57 | - Crash when failing to spawn gdb process (#13). 58 | 59 | ## [0.1.5] - 2019-10-24 60 | ### Added 61 | - Tab completion support in console and expression table. 62 | 63 | ## [0.1.4] - 2019-07-21 64 | ### Fixed 65 | - Incorrect background color selection (#4). 66 | - Crash due to out-of-bounds write to terminal. 67 | - Avoid pager consuming all screen space on long file or function names (#8). 68 | 69 | ## [0.1.3] - 2019-04-04 70 | ### Changed 71 | - Always show stack frame info above pager (not only for source code). 72 | ### Fixed 73 | - Unexpected switches between src/asm modes in pager (#1). 74 | - Decrease wait time when stepping/toggling pager modes by only recomputing pager content when necessary. 75 | - Avoid crash in case of out-of-order gdbmi responses. 76 | 77 | ## [0.1.2] - 2019-03-31 78 | ### Changed 79 | - Allow publication on crates.io 80 | 81 | ## [0.1.1] - 2019-03-24 82 | ### Changed 83 | - Fix building outside of the git repository. 84 | 85 | ## [0.1.0] - 2019-03-23 86 | ### Added 87 | - Initial release. 88 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "ugdb" 3 | version = "0.1.12" 4 | authors = ["ftilde "] 5 | description = "An alternative TUI for gdb" 6 | repository = "https://github.com/ftilde/ugdb" 7 | readme = "README.md" 8 | license = "MIT" 9 | keywords = ["tui", "gdb"] 10 | edition = "2018" 11 | 12 | [[bin]] 13 | doc = false 14 | name = "ugdb" 15 | 16 | [profile.release] 17 | debug = 1 18 | 19 | [dependencies] 20 | structopt = "0.2.1" 21 | time = "0.1" 22 | backtrace = "0.3" 23 | termion = "1.5" 24 | nix = "0.17" 25 | flexi_logger = "^0.11.2" 26 | log = "0.4" 27 | derive_more = "0.14" 28 | 29 | # For IPC 30 | json = "0.11" 31 | rand = "0.3" 32 | unix_socket = "0.5" 33 | unicode-segmentation = "1.2" 34 | parse_int = "0.5" 35 | 36 | unsegen = "0.3.1" 37 | unsegen_signals = "0.3" 38 | unsegen_terminal = "0.3.2" 39 | #unsegen_terminal = { path = "../unsegen_terminal" } 40 | unsegen_jsonviewer = "0.3" 41 | #unsegen_jsonviewer = { path="../unsegen_jsonviewer" } 42 | unsegen_pager = "0.3" 43 | #unsegen_pager = { path="../unsegen_pager"} 44 | 45 | # gdbmi 46 | nom = "2.1" 47 | 48 | [build-dependencies] 49 | toml = "0.4" 50 | 51 | 52 | # Use one of these for development of new features of unsegen 53 | #[patch.crates-io] 54 | #unsegen = { git = 'https://github.com/ftilde/unsegen' } 55 | #unsegen = { path = "../unsegen" } 56 | -------------------------------------------------------------------------------- /IPC.md: -------------------------------------------------------------------------------- 1 | # ugdb IPC 2 | 3 | ugdb creates a unix domain socket at `$XDG_RUNTIME_DIR/ugdb/$RANDOM_CHARACTER_SEQUENCE` (or `/tmp/ugdb/$RANDOM_CHARACTER_SEQUENCE` if `$XDG_RUNTIME_DIR` is not set) which can be used to control ugdb from other applications. 4 | 5 | ## Message structure 6 | 7 | The IPC message structure is inspired by the [i3 ipc format](https://i3wm.org/docs/ipc.html) but is not completely identical: 8 | Each message starts with a 12 byte header of which the first 8 bytes are fixed to "ugdb-ipc" and the following 4 bytes describe the length of the message as a 32 bit little endian unsigned integer. 9 | The message follows immediately after the header. 10 | 11 | The message body itself is a utf8 string that encodes a json object where the exact structure depends on the type of message (see below). 12 | 13 | ## Requests 14 | 15 | Requests have the fields `function` and `parameters` where the structure of `parameters` depends on the value selected for function. 16 | Currently, 3 functions are available: 17 | 18 | ### `get_instance_info` 19 | 20 | Get information about the ugdb instance that controls this socket. 21 | Parameters are unused. 22 | 23 | ```json 24 | { 25 | "function": "get_instance_info", 26 | "parameters": {} 27 | } 28 | ``` 29 | 30 | Currently the response only contains the working directory of the gdb instance: 31 | 32 | ```json 33 | { 34 | "type": "success", 35 | "result": { 36 | "working_directory": "/some/path" 37 | } 38 | } 39 | ``` 40 | ``` 41 | 42 | ### `show_file` 43 | 44 | Show the specified file and at the specified line in the ugdb pager. 45 | Parameters are given as follows: 46 | 47 | ```json 48 | { 49 | "function": "show_file", 50 | "parameters": { 51 | "file": "/path/to/some/file.c", 52 | "line": 42 53 | } 54 | } 55 | ``` 56 | 57 | On success it returns a string that describes the action that was performed. 58 | 59 | ### `set_breakpoint` 60 | 61 | Try to insert a breakpoint at the given line in the given file 62 | Parameters are given as follows: 63 | ```json 64 | { 65 | "function": "set_breakpoint", 66 | "parameters": { 67 | "file": "/path/to/some/file.c", 68 | "line": 42 69 | } 70 | } 71 | ``` 72 | 73 | On success it returns a string that describes the action that was performed. 74 | 75 | ## Responses 76 | 77 | Responses are objects that always contain a String describing the `type`. 78 | The type is always either `success` or `error. 79 | 80 | ### Success 81 | 82 | On success `result` contains either return information or info about the action that was performed. 83 | 84 | ```json 85 | { 86 | "type": "success", 87 | "result": ... 88 | } 89 | ``` 90 | 91 | ### Error 92 | 93 | An error contains the `reason` for the failure as well as `details` that can provide some context for interpreting the failure (e.g., malformed parameters will be returned to the sender as details). 94 | 95 | ```json 96 | { 97 | "type": "error", 98 | "reason": "Malformed (non-object) request", 99 | "details": "{definitely not json" 100 | } 101 | ``` 102 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 ftilde 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ugdb 2 | 3 | ugdb is an [unsegen](https://github.com/ftilde/unsegen) based alternative TUI for [gdb](https://www.gnu.org/software/gdb/). 4 | 5 | ## Screenshots 6 | ![](screenshot.png) 7 | 8 | What's that? Oh, yes. Yes, you can totally use `ugdb` to debug `ugdb` debugging `ugdb`. 9 | 10 | ![](ugdbception.png) 11 | 12 | ## Building 13 | 14 | ugdb is written in Rust and needs a working installation of cargo to build. 15 | 16 | ``` 17 | $ git clone https://github.com/ftilde/ugdb 18 | $ cd ugdb 19 | $ cargo build --release 20 | $ target/release/ugdb 21 | ``` 22 | 23 | ## Installation 24 | 25 | If you're an Arch Linux user, then you can install `ugdb` from the [arch user repository](https://aur.archlinux.org/packages/ugdb/): 26 | ``` 27 | yay -S ugdb 28 | ``` 29 | 30 | `ugdb` can also be installed from [crates.io](https://crates.io/crates/ugdb) using [cargo](https://github.com/rust-lang/cargo/): 31 | ``` 32 | cargo install ugdb 33 | ``` 34 | 35 | ## Usage 36 | 37 | The command line interface is aimed to be *mostly* compatible with gdb: 38 | ``` 39 | $ ugdb --help 40 | ugdb 0.1.12 41 | ftilde 42 | An alternative TUI for gdb 43 | 44 | USAGE: 45 | ugdb [FLAGS] [OPTIONS] [--] [program]... 46 | 47 | FLAGS: 48 | -h, --help Prints help information 49 | --nh Do not execute commands from ~/.gdbinit. 50 | -n, --nx Do not execute commands from any .gdbinit initialization files. 51 | -q, --quiet "Quiet". Do not print the introductory and copyright messages. These messages are also suppressed 52 | in batch mode. 53 | --rr Start ugdb as an interface for rr. Trailing ugdb arguments will be passed to rr replay instead. 54 | -V, --version Prints version information 55 | 56 | OPTIONS: 57 | -b 58 | Set the line speed (baud rate or bits per second) of any serial interface used by GDB for remote debugging. 59 | 60 | --cd 61 | Run GDB using directory as its working directory, instead of the current directory. 62 | 63 | -x, --command Execute GDB commands from file. 64 | -c, --core Use file file as a core dump to examine. 65 | --gdb Path to alternative gdb binary. [default: gdb] 66 | -e, --initial-expression ... Define initial entries for the expression table. 67 | --layout 68 | Define the initial tui layout via a format string. [default: (1s-1c)|(1e-1t)] 69 | 70 | --log_dir 71 | Directory in which the log file will be stored. [default: /tmp] 72 | 73 | -p, --pid Attach to process with given id. 74 | --rr-path Path to alternative rr binary. [default: rr] 75 | -d, --directory 76 | Add directory to the path to search for source files. 77 | 78 | -s, --symbols Read symbols from the given file. 79 | 80 | ARGS: 81 | ... Path to program to debug (with arguments). 82 | ``` 83 | 84 | Some notable differences: 85 | 86 | * Command line arguments to the program to be debugged can be specified without the `-a`-flag of gdb. (But don't forget `--`!) 87 | * You can specify an alternative gdb via the `--gdb` argument. Go debug your Rust: `$ ugdb --gdb=rust-gdb`! By default, `gdb` in `$PATH` will be used. 88 | * An alternative log file directory can be specified using `--log_dir` argument. By default, log files are created in `/tmp/`. 89 | * Some flags might be missing either because they make no sense (e.g., `--tui`) or because I forgot to add them. In the latter case feel free to open an issue. 90 | 91 | 92 | ## User interface 93 | The interface consists of 4 containers between which the user can switch with vim-like controls: 94 | To enter selection mode, press `ESC` (indicated by orange separators). 95 | You can then navigate between containers using arrow keys or hjkl. 96 | Press `Enter` to enter *insert*-mode and interact with the selected container. 97 | Alternatively press the shortcut key for the specific container to directly enter it (see below) from selection mode. 98 | 99 | ### GDB console 100 | 101 | Interact using the standard gdb interface. Enter by pressing `i`. 102 | 103 | * `PageUp`/`PageDown` scroll the output of the console. 104 | * `Ctrl-b`/`Ctrl-e` jump to the beginning/end of the buffer. 105 | * Use arrow keys/Backspace/`Home`/`End` to move the cursor. 106 | * Characters are inserted at the cursor position. 107 | * Use `Tab`/`Ctrl-n`/`Ctrl-p` for identifier and gdb command completion. 108 | * Use `Ctrl-r` to initiate history search and `Ctrl-c`/left/right to accept and continue editing. 109 | 110 | ### Pager 111 | 112 | View and browse source code or assembly around the current program location. Enter by pressing `s`. 113 | 114 | * Scroll up/down using arrow keys or jk and jump using `Home`/`End`. 115 | * Navigate the stack using `PageUp`/`PageDown`. 116 | * Use `Space` to toggle breakpoints at the current location in the pager. 117 | * Toggle between source, assembly, and side-by-side mode using `d` (if available). 118 | 119 | ### Expression table 120 | 121 | View and watch the (structured) results of gdb expressions (everything you can put after `p` in the console). 122 | Changes between steps are highlighted. 123 | Enter by pressing `e`. 124 | 125 | * Enter an expression in the left column 126 | * Press `Enter` to advance to the next row to enter another expression. 127 | * Navigate using arrow keys. 128 | * Use `Space` in the right column to interact with the structure viewer. 129 | * Use `Tab`/`Ctrl-n`/`Ctrl-p` for identifier completion. 130 | * Use `Ctrl-f` to cycle through original/hex/decimal/octal/binary format for integers. 131 | * Use `Ctrl-w` to (try to) set an access watchpoint for the current expression. 132 | 133 | Note: The viewer is somewhat broken for displaying structures with custom pretty-printers. 134 | A workaround would be to use [variable objects](https://sourceware.org/gdb/onlinedocs/gdb/GDB_002fMI-Variable-Objects.html), but that would not allow for evaluation of arbitrary expressions. 135 | 136 | ### Terminal 137 | 138 | The tty of the program to be debugged is automatically redirected to this virtual terminal. 139 | Enter by pressing `t`, or press `T` for *locked mode*. 140 | Locked mode can only be exited by pressing `Esc` twice in rapid succession. 141 | All other input is directly sent to the virtual terminal. 142 | However, for most application the regular insert mode is sufficient and can be left by a single press of `Esc`. 143 | 144 | * Scroll up/down using `PageUp`/`PageDown`. 145 | * `Home`/`End` directly jump to the beginning/end of the output. 146 | 147 | The virtual terminal does not yet properly implement all ansi functions, but it quite usable for a number of terminal applications. 148 | 149 | ## IPC Interface 150 | 151 | `ugdb` can be controlled remotely via a unix domain socket-based IPC interface. 152 | The interface is documented [here](https://github.com/ftilde/ugdb/blob/master/IPC.md). 153 | In practice this means that you can install [vim-ugdb](https://github.com/ftilde/vim-ugdb) and set breakpoints in ugdb from vim using the `UGDBBreakpoint` command. 154 | 155 | ## Builtin commands 156 | 157 | These commands all start with a leading `!` and can be entered instead of regular gdb commands into the gdb console. 158 | 159 | ### `!reload` 160 | 161 | Read the current executable from disk. 162 | You should run this, for example, when you have recompiled the binary that you are debugging and want to reuse an existing ugdb session. 163 | 164 | ### `!show ` 165 | 166 | Show the specified file in the pager. 167 | This may be useful if you interactively want to set breakpoints, but can't or don't want to use the IPC call from your editor (see [vim-ugdb](https://github.com/ftilde/vim-ugdb)). 168 | 169 | ### `!layout ` 170 | 171 | Change ugdb's tui layout at runtime. 172 | The layout string represents a tree with single letters as leafs representing the different panes of ugdb (`c` for the GDB console, `s` for the pager, `e` for the expression table, and `t` for the terminal). 173 | Nodes can be arranged in horizontal (e.g., `c|s|e`) or vertical (e.g., `c-s-e`) layouts using the separators `|` and `-`. 174 | Brackets can be used to nest horizontal and vertical layouts (e.g., `(c|s)-e`). 175 | Finally, integers preceding a node optionally define a weight (other than the default weight of 1) that will be used when assigning screen space to the node. 176 | For example, `1c|3s` will create arrange the console and pager horizontally while assigning roughly 25% of the space to the console and 75% to the pager. 177 | The default layout of ugdb is `(1s-1c)|(1e-1t)`. 178 | An initial layout can also be specified using the command line parameter `--layout`. 179 | 180 | ## FAQ 181 | 182 | ### I get the error message "Cannot *something* because gdb is busy" 183 | 184 | Because we communicate with gdb in synchronous mode, some tasks that require cooperation of gdb (such as setting breakpoints and disassembling source files) cannot be done when gdb is busy, i.e., when the currently debugged program is running. 185 | In this case you have to interrupt execution by pressing Ctrl-C in the console first. 186 | It may be possible to lift this limitation in the future using the [non-stop-mode](https://sourceware.org/gdb/current/onlinedocs/gdb/Asynchronous-and-non_002dstop-modes.html#Asynchronous-and-non_002dstop-modes), but there are no immediate plans for implementation. 187 | 188 | ## Some notes on the status 189 | 190 | This project mostly scratches my own itch -- successfully. I use it as my primary debugger. In that sense I consider this project as "done", but additional sub-itches may be sub-scratched in the future. 191 | 192 | ## Licensing 193 | 194 | `ugdb` is released under the MIT license. 195 | -------------------------------------------------------------------------------- /build.rs: -------------------------------------------------------------------------------- 1 | extern crate toml; 2 | 3 | use toml::{from_str, Value}; 4 | 5 | fn get_revision() -> Option { 6 | let output = std::process::Command::new("git") 7 | .args(&["rev-parse", "HEAD"]) 8 | .output() 9 | .ok()?; 10 | if !output.status.success() { 11 | return None; 12 | } 13 | let prefix = &output.stdout[..8]; 14 | Some(String::from_utf8(prefix.to_vec()).unwrap()) 15 | } 16 | 17 | fn main() { 18 | // Find git revision of current version, if possible 19 | let revision_str = get_revision().unwrap_or_else(|| " release".to_owned()); 20 | println!("cargo:rustc-env=REVISION={}", revision_str); 21 | // The .git directory mtime should change if something is commited, so we rerun the build 22 | // script in that case to update the revision. 23 | println!("cargo:rerun-if-changed=.git"); 24 | 25 | // Find current release version (crate version specified in Cargo.toml) 26 | let file_str = include_str!("Cargo.toml"); 27 | let config: Value = from_str(file_str).unwrap(); 28 | let version_str = config.as_table().unwrap()["package"].as_table().unwrap()["version"] 29 | .as_str() 30 | .unwrap(); 31 | println!("cargo:rustc-env=CRATE_VERSION={}", version_str); 32 | } 33 | -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ftilde/ugdb/891106a1ba54a68faaf084ec63b94674256c62c0/screenshot.png -------------------------------------------------------------------------------- /src/completion/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::gdbmi::commands::MiCommand; 2 | use crate::gdbmi::output::{JsonValue, ResultClass}; 3 | use crate::Context; 4 | use log::{error, info}; 5 | use std::ffi::OsString; 6 | use std::ops::Range; 7 | use std::path::Path; 8 | use std::process::{Command, Stdio}; 9 | 10 | pub struct CompletionState { 11 | original: String, 12 | cursor_pos: usize, //invariant: at grapheme cluster boundary 13 | completion_options: Vec, 14 | current_option: usize, //invariant: in [0, options.size()], last is original 15 | } 16 | 17 | impl CompletionState { 18 | fn new(original: String, cursor_pos: usize, completion_options: Vec) -> Self { 19 | CompletionState { 20 | original, 21 | cursor_pos, 22 | completion_options, 23 | current_option: 0, 24 | } 25 | } 26 | fn empty(original: String, cursor_pos: usize) -> Self { 27 | Self::new(original, cursor_pos, Vec::new()) 28 | } 29 | pub fn current_line_parts(&self) -> (&str, &str, &str) { 30 | ( 31 | &self.original[..self.cursor_pos], 32 | self.current_option(), 33 | &self.original[self.cursor_pos..], 34 | ) 35 | } 36 | 37 | pub fn current_option(&self) -> &str { 38 | self.completion_options 39 | .get(self.current_option) 40 | .map(|s| s.as_str()) 41 | .unwrap_or("") 42 | } 43 | 44 | fn num_options(&self) -> usize { 45 | self.completion_options.len() + 1 46 | } 47 | 48 | pub fn select_next_option(&mut self) { 49 | self.current_option = (self.current_option + 1) % self.num_options() 50 | } 51 | pub fn select_prev_option(&mut self) { 52 | self.current_option = if self.current_option == 0 { 53 | self.num_options() - 1 54 | } else { 55 | self.current_option - 1 56 | }; 57 | } 58 | } 59 | 60 | pub trait Completer { 61 | fn complete(&mut self, original: &str, cursor_pos: usize) -> CompletionState; 62 | } 63 | 64 | struct CommandCompleter<'a> { 65 | binary_path: &'a Path, 66 | init_options: &'a [OsString], 67 | } 68 | 69 | fn gen_command_list(binary_path: &Path, init_options: &[OsString]) -> std::io::Result> { 70 | let child = Command::new(binary_path) 71 | .args(init_options) 72 | .arg("-batch") 73 | .arg("-ex") 74 | .arg("help all") 75 | .stdout(Stdio::piped()) 76 | .spawn()?; 77 | let gdb_output = child.wait_with_output()?; 78 | let gdb_output = String::from_utf8_lossy(&gdb_output.stdout); 79 | Ok(parse_command_names(&gdb_output)) 80 | } 81 | 82 | fn parse_command_names(gdb_output: &str) -> Vec { 83 | gdb_output 84 | .lines() 85 | .filter_map(|l| { 86 | let end = l.find(" -- ")?; 87 | let before_info = &l[..end]; 88 | Some(before_info.split(',')) 89 | }) 90 | .flatten() 91 | .map(|l| l.trim().to_owned()) 92 | .collect() 93 | } 94 | 95 | impl Completer for CommandCompleter<'_> { 96 | fn complete(&mut self, original: &str, cursor_pos: usize) -> CompletionState { 97 | // Possible optimization: Only generate command list once, but it does not appear to be a 98 | // real bottleneck so far. 99 | let candidates = match gen_command_list(self.binary_path, self.init_options) { 100 | Ok(commands) => find_candidates(&original[..cursor_pos], &commands), 101 | Err(e) => { 102 | error!("Failed to generate gdb command list: {}", e); 103 | Vec::new() 104 | } 105 | }; 106 | CompletionState::new(original.to_owned(), cursor_pos, candidates) 107 | } 108 | } 109 | 110 | pub struct IdentifierCompleter<'a>(pub &'a mut Context); 111 | 112 | struct VarObject { 113 | name: String, 114 | expr: Option, 115 | typ: Option, 116 | } 117 | 118 | impl VarObject { 119 | fn from_val(o: &JsonValue) -> Result { 120 | let name = if let Some(name) = o["name"].as_str() { 121 | name.to_string() 122 | } else { 123 | return Err("Missing field 'name'".into()); 124 | }; 125 | let expr = o["exp"].as_str().map(|s| s.to_owned()); 126 | let typ = o["type"].as_str().map(|s| s.to_owned()); 127 | Ok(VarObject { name, expr, typ }) 128 | } 129 | fn create(p: &mut Context, expr: &str) -> Result { 130 | let res = p 131 | .gdb 132 | .mi 133 | .execute(MiCommand::var_create(None, expr, None)) 134 | .map_err(|e| format!("{:?}", e))?; 135 | 136 | match res.class { 137 | ResultClass::Done => {} 138 | ResultClass::Error => return Err(format!("{}", res.results["msg"])), 139 | o => return Err(format!("Unexpected result class: {:?}", o)), 140 | } 141 | 142 | VarObject::from_val(&JsonValue::Object(res.results)) 143 | } 144 | 145 | fn children(&self, p: &mut Context) -> Result, String> { 146 | let res = p 147 | .gdb 148 | .mi 149 | .execute(MiCommand::var_list_children(&self.name, true, None)) 150 | .map_err(|e| format!("{:?}", e))?; 151 | 152 | match res.class { 153 | ResultClass::Done => {} 154 | ResultClass::Error => return Err(format!("{}", res.results["msg"])), 155 | o => return Err(format!("Unexpected result class: {:?}", o)), 156 | } 157 | 158 | res.results["children"] 159 | .members() 160 | .map(VarObject::from_val) 161 | .collect::, String>>() 162 | } 163 | 164 | fn collect_children_exprs( 165 | &self, 166 | p: &mut Context, 167 | output: &mut Vec, 168 | ) -> Result<(), String> { 169 | // try to flatten public/private fields etc. 170 | let flatten_exprs = ["", ""]; 171 | 172 | for child in self.children(p)? { 173 | if child.typ.is_none() 174 | || child.expr.is_none() 175 | || flatten_exprs 176 | .iter() 177 | .any(|n| *n == child.expr.as_ref().unwrap()) 178 | { 179 | // This is the case for pseudo children (like public, ...) 180 | child.collect_children_exprs(p, output)?; 181 | } else if let Some(expr) = child.expr { 182 | output.push(expr); 183 | } 184 | } 185 | Ok(()) 186 | } 187 | 188 | fn delete(self, p: &mut Context) -> Result<(), String> { 189 | let res = p 190 | .gdb 191 | .mi 192 | .execute(MiCommand::var_delete(self.name, true)) 193 | .map_err(|e| format!("{:?}", e))?; 194 | 195 | match res.class { 196 | ResultClass::Done => {} 197 | ResultClass::Error => return Err(format!("{}", res.results["msg"])), 198 | o => return Err(format!("Unexpected result class: {:?}", o)), 199 | } 200 | Ok(()) 201 | } 202 | } 203 | 204 | fn get_children(p: &mut Context, expr: &str) -> Result, String> { 205 | let root = VarObject::create(p, expr)?; 206 | 207 | let mut children = Vec::new(); 208 | 209 | root.collect_children_exprs(p, &mut children)?; 210 | 211 | root.delete(p)?; 212 | 213 | Ok(children) 214 | } 215 | 216 | fn get_variables(p: &mut Context) -> Result, String> { 217 | let res = p 218 | .gdb 219 | .mi 220 | .execute(MiCommand::stack_list_variables(None, None)) 221 | .map_err(|e| format!("{:?}", e))?; 222 | 223 | match res.class { 224 | ResultClass::Done => {} 225 | ResultClass::Error => return Err(format!("{}", res.results["msg"])), 226 | o => return Err(format!("Unexpected result class: {:?}", o)), 227 | } 228 | 229 | Ok(res.results["variables"] 230 | .members() 231 | .map(|o| o["name"].as_str().unwrap().to_string()) 232 | .collect::>()) 233 | } 234 | 235 | impl Completer for IdentifierCompleter<'_> { 236 | fn complete(&mut self, original: &str, cursor_pos: usize) -> CompletionState { 237 | let expr = if let Ok(e) = CompletableExpression::from_str(&original[..cursor_pos]) { 238 | e 239 | } else { 240 | return CompletionState::empty(original.to_owned(), cursor_pos); 241 | }; 242 | let res = if expr.parent.is_empty() { 243 | get_variables(self.0) 244 | } else { 245 | get_children(self.0, &expr.parent) 246 | }; 247 | let children = match res { 248 | Ok(c) => c, 249 | Err(e) => { 250 | info!("Could not complete identifier: {:?}", e); 251 | vec![] 252 | } 253 | }; 254 | let candidates = find_candidates(&expr.prefix, children.as_slice()); 255 | CompletionState::new(original.to_owned(), cursor_pos, candidates) 256 | } 257 | } 258 | 259 | pub struct CmdlineCompleter<'a>(pub &'a mut Context); 260 | impl Completer for CmdlineCompleter<'_> { 261 | fn complete(&mut self, original: &str, cursor_pos: usize) -> CompletionState { 262 | if original[..cursor_pos].find(' ').is_some() { 263 | // gdb command already typed, try to complete identifier in expression 264 | IdentifierCompleter(self.0).complete(original, cursor_pos) 265 | } else { 266 | // First "word" in command line, complete gdb command 267 | CommandCompleter { 268 | binary_path: self.0.gdb.mi.binary_path(), 269 | init_options: self.0.gdb.mi.init_options(), 270 | } 271 | .complete(original, cursor_pos) 272 | } 273 | //TODO: path completer? not sure how to distinguish between IdentifierCompleter and path 274 | //completer. Maybe based on gdb command... 275 | } 276 | } 277 | 278 | #[derive(Copy, Clone, PartialEq, Debug)] 279 | enum ExpressionTokenType { 280 | Atom, 281 | Arrow, 282 | Dot, 283 | LParen, 284 | RParen, 285 | LBracket, 286 | RBracket, 287 | Asterisk, 288 | Sep, 289 | } 290 | 291 | #[derive(Clone, PartialEq, Debug)] 292 | struct ExpressionToken { 293 | ttype: ExpressionTokenType, 294 | pos: Range, 295 | } 296 | 297 | #[derive(Debug, PartialEq)] 298 | enum TokenizeError { 299 | UnfinishedString, 300 | } 301 | 302 | fn tokenize_expression(s: &str) -> Result, TokenizeError> { 303 | let mut chars = s.chars().enumerate().peekable(); 304 | let mut output = Vec::new(); 305 | let is_atom_char = |c: char| c.is_alphanumeric() || c == '_'; 306 | 'outer: while let Some((i, c)) = chars.next() { 307 | let tok = |o: &mut Vec, t: ExpressionTokenType| { 308 | o.push(ExpressionToken { 309 | ttype: t, 310 | pos: i..i + 1, 311 | }); 312 | }; 313 | match c { 314 | ' ' | '\t' | '\n' => {} 315 | '(' => tok(&mut output, ExpressionTokenType::LParen), 316 | '[' => tok(&mut output, ExpressionTokenType::LBracket), 317 | ')' => tok(&mut output, ExpressionTokenType::RParen), 318 | ']' => tok(&mut output, ExpressionTokenType::RBracket), 319 | '*' => tok(&mut output, ExpressionTokenType::Asterisk), 320 | '.' => tok(&mut output, ExpressionTokenType::Dot), 321 | '-' => match chars.peek().map(|(_, c)| *c) { 322 | Some('>') => { 323 | let _ = chars.next(); 324 | output.push(ExpressionToken { 325 | ttype: ExpressionTokenType::Arrow, 326 | pos: i..i + 2, 327 | }); 328 | } 329 | Some('-') => { 330 | let _ = chars.next(); 331 | output.push(ExpressionToken { 332 | ttype: ExpressionTokenType::Sep, 333 | pos: i..i + 1, 334 | }); 335 | output.push(ExpressionToken { 336 | ttype: ExpressionTokenType::Sep, 337 | pos: i + 1..i + 2, 338 | }); 339 | } 340 | Some(_) | None => { 341 | tok(&mut output, ExpressionTokenType::Sep); 342 | } 343 | }, 344 | '"' => { 345 | let mut escaped = false; 346 | let start = i; 347 | for (i, c) in chars.by_ref() { 348 | match (c, escaped) { 349 | ('"', false) => { 350 | output.push(ExpressionToken { 351 | ttype: ExpressionTokenType::Atom, 352 | pos: start..i + 1, 353 | }); 354 | continue 'outer; 355 | } 356 | (_, true) => escaped = false, 357 | ('\\', false) => escaped = true, 358 | (_, false) => {} 359 | } 360 | } 361 | return Err(TokenizeError::UnfinishedString); 362 | } 363 | c if is_atom_char(c) => { 364 | let start = i; 365 | let mut prev_i = i; 366 | loop { 367 | match chars.peek().cloned() { 368 | Some((i, c)) if is_atom_char(c) => { 369 | let _ = chars.next(); 370 | prev_i = i; 371 | } 372 | Some(_) | None => { 373 | output.push(ExpressionToken { 374 | ttype: ExpressionTokenType::Atom, 375 | pos: start..prev_i + 1, 376 | }); 377 | break; 378 | } 379 | } 380 | } 381 | } 382 | _ => tok(&mut output, ExpressionTokenType::Sep), 383 | } 384 | } 385 | Ok(output) 386 | } 387 | 388 | #[derive(Debug, PartialEq, Clone)] 389 | pub struct CompletableExpression { 390 | parent: String, 391 | prefix: String, 392 | } 393 | 394 | impl CompletableExpression { 395 | fn from_str(s: &str) -> Result { 396 | let mut tokens = tokenize_expression(s)?.into_iter().rev().peekable(); 397 | let prefix; 398 | let mut needs_deref = false; 399 | if let Some(ExpressionToken { 400 | ttype: ExpressionTokenType::Atom, 401 | pos, 402 | }) = tokens.peek().cloned() 403 | { 404 | if pos.end == s.len() { 405 | prefix = &s[pos]; 406 | let _ = tokens.next(); 407 | } else { 408 | prefix = ""; 409 | } 410 | } else { 411 | prefix = ""; 412 | } 413 | let parent_end; 414 | match tokens.next() { 415 | Some(ExpressionToken { 416 | ttype: ExpressionTokenType::Dot, 417 | pos, 418 | }) => parent_end = pos.start, 419 | Some(ExpressionToken { 420 | ttype: ExpressionTokenType::Arrow, 421 | pos, 422 | }) => { 423 | parent_end = pos.start; 424 | needs_deref = true; 425 | } 426 | _ => { 427 | return Ok(CompletableExpression { 428 | parent: "".to_owned(), 429 | prefix: prefix.to_owned(), 430 | }); 431 | } 432 | } 433 | 434 | // Now we need to find the beginning of parent! 435 | let mut paren_level = 0i32; 436 | let mut bracket_level = 0i32; 437 | let mut parent_begin = None; 438 | let mut active_atom = false; 439 | let mut prev_token_begin = s.len(); 440 | for token in tokens { 441 | match token.ttype { 442 | ExpressionTokenType::RParen => { 443 | paren_level += 1; 444 | active_atom = false; 445 | } 446 | ExpressionTokenType::RBracket => { 447 | bracket_level += 1; 448 | active_atom = false; 449 | } 450 | ExpressionTokenType::LParen => { 451 | paren_level -= 1; 452 | active_atom = false; 453 | if paren_level < 0 { 454 | parent_begin = Some(prev_token_begin); 455 | break; 456 | } 457 | } 458 | ExpressionTokenType::LBracket => { 459 | bracket_level -= 1; 460 | active_atom = false; 461 | if bracket_level < 0 { 462 | parent_begin = Some(prev_token_begin); 463 | break; 464 | } 465 | } 466 | ExpressionTokenType::Dot | ExpressionTokenType::Arrow => { 467 | active_atom = false; 468 | } 469 | t if paren_level == 0 && bracket_level == 0 => { 470 | if t != ExpressionTokenType::Atom || active_atom { 471 | parent_begin = Some(prev_token_begin); 472 | break; 473 | } else { 474 | active_atom = true; 475 | } 476 | } 477 | _ => {} 478 | } 479 | prev_token_begin = token.pos.start; 480 | } 481 | let parent_begin = parent_begin.unwrap_or(0); 482 | let parent = &s[parent_begin..parent_end]; 483 | let parent = if needs_deref { 484 | format!("*({})", parent) 485 | } else { 486 | parent.to_owned() 487 | }; 488 | 489 | Ok(CompletableExpression { 490 | parent, 491 | prefix: prefix.to_owned(), 492 | }) 493 | } 494 | } 495 | 496 | fn find_candidates>(prefix: &str, candidates: &[S]) -> Vec { 497 | candidates 498 | .iter() 499 | .filter_map(|candidate| { 500 | if candidate.as_ref().starts_with(prefix) { 501 | Some(candidate.as_ref()[prefix.len()..].to_owned()) 502 | } else { 503 | None 504 | } 505 | }) 506 | .collect::>() 507 | } 508 | 509 | #[cfg(test)] 510 | mod test { 511 | use super::*; 512 | 513 | fn current_line(state: &CompletionState) -> String { 514 | let parts = state.current_line_parts(); 515 | format!("{}{}{}", parts.0, parts.1, parts.2) 516 | } 517 | #[test] 518 | fn test_completion_state() { 519 | let mut state = 520 | CompletionState::new("ba)".to_owned(), 2, vec!["r".to_owned(), "z".to_owned()]); 521 | assert_eq!(current_line(&state), "bar)"); 522 | state.select_next_option(); 523 | assert_eq!(current_line(&state), "baz)"); 524 | state.select_next_option(); 525 | assert_eq!(current_line(&state), "ba)"); 526 | state.select_next_option(); 527 | assert_eq!(current_line(&state), "bar)"); 528 | state.select_prev_option(); 529 | assert_eq!(current_line(&state), "ba)"); 530 | } 531 | #[test] 532 | fn test_completion_state_empty() { 533 | let mut state = CompletionState::new("ba)".to_owned(), 2, vec![]); 534 | assert_eq!(current_line(&state), "ba)"); 535 | state.select_next_option(); 536 | assert_eq!(current_line(&state), "ba)"); 537 | state.select_next_option(); 538 | assert_eq!(current_line(&state), "ba)"); 539 | state.select_next_option(); 540 | assert_eq!(current_line(&state), "ba)"); 541 | state.select_prev_option(); 542 | assert_eq!(current_line(&state), "ba)"); 543 | } 544 | #[test] 545 | fn test_gdb_help_parser() { 546 | let input = " 547 | tsave -- Save the trace data to a file. 548 | while-stepping -- Specify single-stepping behavior at a tracepoint. 549 | 550 | Command class: user-defined 551 | 552 | myadder -- User-defined. 553 | 554 | Unclassified commands 555 | 556 | add-inferior -- Add a new inferior. 557 | help, h -- Print list of commands. 558 | function _any_caller_is -- Check all calling function's names. 559 | "; 560 | let got = parse_command_names(input); 561 | let expected = [ 562 | "tsave", 563 | "while-stepping", 564 | "myadder", 565 | "add-inferior", 566 | "help", 567 | "h", 568 | "function _any_caller_is", 569 | ] 570 | .iter() 571 | .map(|s| s.to_string()) 572 | .collect::>(); 573 | assert_eq!(got, expected); 574 | } 575 | #[test] 576 | fn test_command_completer() { 577 | let state = CommandCompleter { 578 | binary_path: Path::new("gdb"), 579 | init_options: &[], 580 | } 581 | .complete("he", 2); 582 | assert_eq!(current_line(&state), "help"); 583 | assert_eq!(state.completion_options, vec!["lp"]); 584 | } 585 | #[test] 586 | fn test_find_candidates() { 587 | assert_eq!(find_candidates::<&str>("", &[]), Vec::<&str>::new()); 588 | assert_eq!(find_candidates::<&str>("foo", &[]), Vec::<&str>::new()); 589 | assert_eq!( 590 | find_candidates("", &["foo".to_owned(), "bar".to_owned(), "baz".to_owned()]), 591 | vec!["foo", "bar", "baz"] 592 | ); 593 | assert_eq!( 594 | find_candidates( 595 | "ba", 596 | &["foo".to_owned(), "bar".to_owned(), "baz".to_owned()] 597 | ), 598 | vec!["r", "z"] 599 | ); 600 | assert_eq!( 601 | find_candidates( 602 | "baf", 603 | &["foo".to_owned(), "bar".to_owned(), "baz".to_owned()] 604 | ), 605 | Vec::<&str>::new() 606 | ); 607 | } 608 | 609 | fn assert_eq_tokenize(s: &str, v: Vec<(ExpressionTokenType, Range)>) { 610 | let expected = v 611 | .into_iter() 612 | .map(|(t, r)| ExpressionToken { ttype: t, pos: r }) 613 | .collect::>(); 614 | let got = tokenize_expression(s).unwrap(); 615 | assert_eq!(got, expected); 616 | } 617 | #[test] 618 | fn test_tokenize_expression() { 619 | assert_eq_tokenize("", Vec::new()); 620 | assert_eq_tokenize( 621 | "123 asdf", 622 | vec![ 623 | (ExpressionTokenType::Atom, 0..3), 624 | (ExpressionTokenType::Atom, 4..8), 625 | ], 626 | ); 627 | assert_eq_tokenize( 628 | " * * ,, . ", 629 | vec![ 630 | (ExpressionTokenType::Asterisk, 1..2), 631 | (ExpressionTokenType::Asterisk, 3..4), 632 | (ExpressionTokenType::Sep, 5..6), 633 | (ExpressionTokenType::Sep, 6..7), 634 | (ExpressionTokenType::Dot, 8..9), 635 | ], 636 | ); 637 | assert_eq_tokenize( 638 | "( (][)", 639 | vec![ 640 | (ExpressionTokenType::LParen, 0..1), 641 | (ExpressionTokenType::LParen, 3..4), 642 | (ExpressionTokenType::RBracket, 4..5), 643 | (ExpressionTokenType::LBracket, 5..6), 644 | (ExpressionTokenType::RParen, 6..7), 645 | ], 646 | ); 647 | assert_eq_tokenize( 648 | "< \"foo\"", 649 | vec![ 650 | (ExpressionTokenType::Sep, 0..1), 651 | (ExpressionTokenType::Atom, 2..7), 652 | ], 653 | ); 654 | assert_eq_tokenize( 655 | "-->", 656 | vec![ 657 | (ExpressionTokenType::Sep, 0..1), 658 | (ExpressionTokenType::Sep, 1..2), 659 | (ExpressionTokenType::Sep, 2..3), 660 | ], 661 | ); 662 | assert_eq_tokenize( 663 | "->-", 664 | vec![ 665 | (ExpressionTokenType::Arrow, 0..2), 666 | (ExpressionTokenType::Sep, 2..3), 667 | ], 668 | ); 669 | assert_eq_tokenize( 670 | "foo->bar", 671 | vec![ 672 | (ExpressionTokenType::Atom, 0..3), 673 | (ExpressionTokenType::Arrow, 3..5), 674 | (ExpressionTokenType::Atom, 5..8), 675 | ], 676 | ); 677 | assert_eq!( 678 | tokenize_expression(" asdf \" kldsj"), 679 | Err(TokenizeError::UnfinishedString) 680 | ); 681 | } 682 | fn assert_eq_completable_expression(s: &str, parent: &str, prefix: &str) { 683 | let got = CompletableExpression::from_str(s).unwrap(); 684 | let expected = CompletableExpression { 685 | parent: parent.to_owned(), 686 | prefix: prefix.to_owned(), 687 | }; 688 | assert_eq!(got, expected); 689 | } 690 | #[test] 691 | fn test_completable_expression() { 692 | assert_eq_completable_expression("", "", ""); 693 | assert_eq_completable_expression("foo.bar", "foo", "bar"); 694 | assert_eq_completable_expression("foo->bar", "*(foo)", "bar"); 695 | assert_eq_completable_expression("(foo[2]->bar", "*(foo[2])", "bar"); 696 | assert_eq_completable_expression("][foo(1,23).", "foo(1,23)", ""); 697 | assert_eq_completable_expression("][foo(1,23)", "", ""); 698 | assert_eq_completable_expression("foo + b", "", "b"); 699 | assert_eq_completable_expression("\"ldkf\" f", "", "f"); 700 | assert_eq_completable_expression(" foo", "", "foo"); 701 | assert_eq_completable_expression("foo ", "", ""); 702 | assert_eq_completable_expression("f foo[2].f", "foo[2]", "f"); 703 | assert_eq_completable_expression("f \"foo\"[2].f", "\"foo\"[2]", "f"); 704 | } 705 | } 706 | -------------------------------------------------------------------------------- /src/gdb.rs: -------------------------------------------------------------------------------- 1 | // This module encapsulates some functionality of gdb. Depending on how general this turns out, we 2 | // may want to move it to a separate crate or merge it with gdbmi-rs 3 | use crate::gdbmi::{ 4 | self, 5 | commands::{BreakPointLocation, BreakPointNumber, MiCommand}, 6 | output::{BreakPointEvent, JsonValue, Object, ResultClass}, 7 | ExecuteError, 8 | }; 9 | use std::{ 10 | collections::{HashMap, HashSet}, 11 | fmt, 12 | ops::{Add, Sub}, 13 | path::PathBuf, 14 | }; 15 | use unsegen::base::LineNumber; 16 | 17 | #[derive(Debug, Clone)] 18 | pub struct SrcPosition { 19 | pub file: PathBuf, 20 | pub line: LineNumber, 21 | } 22 | 23 | impl SrcPosition { 24 | pub const fn new(file: PathBuf, line: LineNumber) -> Self { 25 | SrcPosition { file, line } 26 | } 27 | } 28 | 29 | #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] 30 | pub struct Address(pub usize); 31 | impl Address { 32 | pub fn parse(string: &str) -> Result { 33 | usize::from_str_radix(&string[2..], 16) 34 | .map(Address) 35 | .map_err(|e| (e, string.to_owned())) 36 | } 37 | } 38 | impl fmt::Display for Address { 39 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 40 | write!(f, "0x{:x}", self.0) 41 | } 42 | } 43 | impl Add for Address { 44 | type Output = Self; 45 | fn add(self, rhs: usize) -> Self { 46 | Address(self.0 + rhs) 47 | } 48 | } 49 | 50 | impl Sub for Address { 51 | type Output = Self; 52 | fn sub(self, rhs: usize) -> Self { 53 | Address(self.0 - rhs) 54 | } 55 | } 56 | 57 | pub struct BreakPoint { 58 | pub number: BreakPointNumber, 59 | pub address: Option
, 60 | pub enabled: bool, 61 | pub src_pos: Option, // May not be present if debug information is missing! 62 | } 63 | 64 | impl BreakPoint { 65 | pub fn from_json(bkpt: &Object) -> Self { 66 | let number = bkpt["number"] 67 | .as_str() 68 | .expect("find bp number") 69 | .parse::() 70 | .expect("Parse usize"); 71 | let enabled = bkpt["enabled"].as_str().expect("find enabled") == "y"; 72 | let address = bkpt["addr"] 73 | .as_str() 74 | .and_then(|addr| Address::parse(addr).ok()); //addr may not be present or contain 75 | let src_pos = { 76 | let maybe_file = bkpt["fullname"].as_str(); 77 | let maybe_line = bkpt["line"] 78 | .as_str() 79 | .map(|l_nr| LineNumber::new(l_nr.parse::().expect("Parse usize"))); 80 | if let (Some(file), Some(line)) = (maybe_file, maybe_line) { 81 | Some(SrcPosition::new(PathBuf::from(file), line)) 82 | } else { 83 | None 84 | } 85 | }; 86 | BreakPoint { 87 | number, 88 | address, 89 | enabled, 90 | src_pos, 91 | } 92 | } 93 | } 94 | 95 | pub struct BreakPointSet { 96 | map: HashMap, 97 | pub last_change: std::time::Instant, 98 | } 99 | 100 | impl Default for BreakPointSet { 101 | fn default() -> Self { 102 | Self::new() 103 | } 104 | } 105 | 106 | impl BreakPointSet { 107 | pub fn new() -> Self { 108 | BreakPointSet { 109 | map: HashMap::new(), 110 | last_change: std::time::Instant::now(), 111 | } 112 | } 113 | 114 | fn notify_change(&mut self) { 115 | self.last_change = std::time::Instant::now(); 116 | } 117 | 118 | pub fn update_breakpoint(&mut self, new_bp: BreakPoint) { 119 | let _ = self.map.insert(new_bp.number, new_bp); 120 | //debug_assert!(res.is_some(), "Modified non-existent breakpoint"); 121 | self.notify_change(); 122 | } 123 | 124 | pub fn remove_breakpoint(&mut self, bp_num: BreakPointNumber) { 125 | self.map.remove(&bp_num); 126 | if bp_num.minor.is_none() { 127 | //TODO: ensure removal of child breakpoints 128 | } 129 | self.notify_change(); 130 | } 131 | } 132 | 133 | impl std::ops::Deref for BreakPointSet { 134 | type Target = HashMap; 135 | 136 | fn deref(&self) -> &Self::Target { 137 | &self.map 138 | } 139 | } 140 | 141 | #[allow(clippy::upper_case_acronyms)] 142 | pub struct GDB { 143 | pub mi: gdbmi::GDB, 144 | pub breakpoints: BreakPointSet, 145 | } 146 | 147 | pub enum BreakpointOperationError { 148 | Busy, 149 | ExecutionError(String), 150 | } 151 | 152 | impl GDB { 153 | pub fn new(mi: gdbmi::GDB) -> Self { 154 | GDB { 155 | mi, 156 | breakpoints: BreakPointSet::new(), 157 | } 158 | } 159 | 160 | pub fn kill(&mut self) { 161 | self.mi.interrupt_execution().expect("interrupt worked"); 162 | self.mi.execute_later(&gdbmi::commands::MiCommand::exit()); 163 | } 164 | 165 | pub fn insert_breakpoint( 166 | &mut self, 167 | location: BreakPointLocation, 168 | ) -> Result<(), BreakpointOperationError> { 169 | let bp_result = self 170 | .mi 171 | .execute(&MiCommand::insert_breakpoint(location)) 172 | .map_err(|e| match e { 173 | ExecuteError::Busy => BreakpointOperationError::Busy, 174 | ExecuteError::Quit => panic!("Could not insert breakpoint: GDB quit"), 175 | })?; 176 | match bp_result.class { 177 | ResultClass::Done => { 178 | self.handle_breakpoint_event(BreakPointEvent::Created, &bp_result.results); 179 | Ok(()) 180 | } 181 | ResultClass::Error => Err(BreakpointOperationError::ExecutionError( 182 | bp_result 183 | .results 184 | .get("msg") 185 | .and_then(|msg_obj| msg_obj.as_str()) 186 | .map(|s| s.to_owned()) 187 | .unwrap_or_else(|| bp_result.results.dump()), 188 | )), 189 | _ => { 190 | panic!("Unexpected resultclass: {:?}", bp_result.class); 191 | } 192 | } 193 | } 194 | 195 | pub fn delete_breakpoints>( 196 | &mut self, 197 | bp_numbers: I, 198 | ) -> Result<(), BreakpointOperationError> { 199 | let bp_result = self 200 | .mi 201 | .execute(MiCommand::delete_breakpoints(bp_numbers.clone())) 202 | .map_err(|e| match e { 203 | ExecuteError::Busy => BreakpointOperationError::Busy, 204 | ExecuteError::Quit => panic!("Could not insert breakpoint: GDB quit"), 205 | })?; 206 | match bp_result.class { 207 | ResultClass::Done => { 208 | let major_to_delete = bp_numbers.map(|n| n.major).collect::>(); 209 | let bkpts_to_delete = self 210 | .breakpoints 211 | .map 212 | .keys() 213 | .filter_map(|&k| { 214 | if major_to_delete.contains(&k.major) { 215 | Some(k) 216 | } else { 217 | None 218 | } 219 | }) 220 | .collect::>(); 221 | for bkpt in bkpts_to_delete { 222 | self.breakpoints.remove_breakpoint(bkpt); 223 | } 224 | Ok(()) 225 | } 226 | ResultClass::Error => Err(BreakpointOperationError::ExecutionError( 227 | bp_result 228 | .results 229 | .get("msg") 230 | .and_then(|msg_obj| msg_obj.as_str()) 231 | .map(|s| s.to_owned()) 232 | .unwrap_or_else(|| bp_result.results.dump()), 233 | )), 234 | _ => { 235 | panic!("Unexpected resultclass: {:?}", bp_result.class); 236 | } 237 | } 238 | } 239 | 240 | pub fn handle_breakpoint_event(&mut self, bp_type: BreakPointEvent, info: &Object) { 241 | match bp_type { 242 | BreakPointEvent::Created | BreakPointEvent::Modified => { 243 | match &info["bkpt"] { 244 | JsonValue::Object(bkpt) => { 245 | let bp = BreakPoint::from_json(bkpt); 246 | self.breakpoints.update_breakpoint(bp); 247 | 248 | // If there are multiple locations (recent versions of) gdb return the 249 | // sub-breakpoints in the array "locations". 250 | if let Some(JsonValue::Array(bkpts)) = bkpt.get("locations") { 251 | for bkpt in bkpts { 252 | if let JsonValue::Object(bkpt) = bkpt { 253 | let bp = BreakPoint::from_json(bkpt); 254 | self.breakpoints.update_breakpoint(bp); 255 | } else { 256 | panic!("Malformed breakpoint list"); 257 | } 258 | } 259 | } 260 | } 261 | JsonValue::Array(bkpts) => { 262 | // In previous versions, gdb returned multiple sub-breakpoints as a series 263 | // of objects under the "bkpt" key (thus breaking the spec). This appears 264 | // to be fixed now, but we keep the current case (for now) for users of old 265 | // gdb versions. 266 | for bkpt in bkpts { 267 | if let JsonValue::Object(bkpt) = bkpt { 268 | let bp = BreakPoint::from_json(bkpt); 269 | self.breakpoints.update_breakpoint(bp); 270 | } else { 271 | panic!("Malformed breakpoint list"); 272 | } 273 | } 274 | } 275 | _ => { 276 | panic!("Invalid bkpt structure"); 277 | } 278 | } 279 | } 280 | BreakPointEvent::Deleted => { 281 | let id = info["id"] 282 | .as_str() 283 | .expect("find id") 284 | .parse::() 285 | .expect("Parse usize"); 286 | self.breakpoints.remove_breakpoint(id); 287 | } 288 | } 289 | } 290 | 291 | // Warning: This is a hack, as gdbmi does not currently offer a command to query the current target 292 | // May not work and can break at any time. 293 | pub fn get_target(&mut self) -> Result, ExecuteError> { 294 | let result = self.mi.execute(MiCommand::list_thread_groups(false, &[]))?; 295 | if result.class == ResultClass::Done { 296 | Ok(result.results["groups"] 297 | .members() 298 | .filter_map(|thread| thread["executable"].as_str()) 299 | .next() 300 | .map(|exec| exec.into())) 301 | } else { 302 | Ok(None) 303 | } 304 | } 305 | 306 | pub fn get_stack_level(&mut self) -> Result { 307 | let frame = self.mi.execute(MiCommand::stack_info_frame(None))?; 308 | response::get_u64(&frame.results["frame"], "level") 309 | } 310 | 311 | pub fn get_stack_depth(&mut self) -> Result { 312 | let frame = self.mi.execute(MiCommand::stack_info_depth())?; 313 | response::get_u64_obj(&frame.results, "depth") 314 | } 315 | } 316 | 317 | // Various helper for getting stuff out of gdb response values 318 | pub mod response { 319 | use super::*; 320 | 321 | #[derive(Clone, Debug, PartialEq)] 322 | pub enum GDBResponseError { 323 | MissingField(&'static str, JsonValue), 324 | MalformedAddress(String), 325 | Other(String), 326 | Execution(ExecuteError), 327 | } 328 | 329 | impl From<(::std::num::ParseIntError, String)> for GDBResponseError { 330 | fn from((_, s): (::std::num::ParseIntError, String)) -> Self { 331 | GDBResponseError::MalformedAddress(s) 332 | } 333 | } 334 | impl From for GDBResponseError { 335 | fn from(e: ExecuteError) -> Self { 336 | GDBResponseError::Execution(e) 337 | } 338 | } 339 | 340 | pub fn get_str<'a>(obj: &'a JsonValue, key: &'static str) -> Result<&'a str, GDBResponseError> { 341 | obj[key] 342 | .as_str() 343 | .ok_or_else(|| GDBResponseError::MissingField(key, obj.clone())) 344 | } 345 | 346 | pub fn get_str_obj<'a>( 347 | obj: &'a Object, 348 | key: &'static str, 349 | ) -> Result<&'a str, GDBResponseError> { 350 | obj[key] 351 | .as_str() 352 | .ok_or_else(|| GDBResponseError::MissingField(key, JsonValue::Object(obj.clone()))) 353 | } 354 | 355 | pub fn get_addr(obj: &JsonValue, key: &'static str) -> Result { 356 | let s = get_str(obj, key)?; 357 | Ok(Address::parse(s)?) 358 | } 359 | 360 | pub fn get_addr_obj(obj: &Object, key: &'static str) -> Result { 361 | let s = get_str_obj(obj, key)?; 362 | Ok(Address::parse(s)?) 363 | } 364 | 365 | pub fn get_u64(obj: &JsonValue, key: &'static str) -> Result { 366 | let s = get_str(obj, key)?; 367 | s.parse::() 368 | .map_err(|e| GDBResponseError::Other(format!("Malformed frame description: {:?}", e))) 369 | } 370 | 371 | pub fn get_u64_obj(obj: &Object, key: &'static str) -> Result { 372 | let s = get_str_obj(obj, key)?; 373 | s.parse::() 374 | .map_err(|e| GDBResponseError::Other(format!("Malformed frame description: {:?}", e))) 375 | } 376 | } 377 | -------------------------------------------------------------------------------- /src/gdb_expression_parsing/ast.rs: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/gdb_expression_parsing/lexer.rs: -------------------------------------------------------------------------------- 1 | use std::str::CharIndices; 2 | 3 | pub type Location = usize; 4 | pub type TokenWithLocation = (Location, Token, Location); 5 | 6 | #[derive(Copy, Clone, Debug, PartialEq, Eq)] 7 | pub enum Token { 8 | LBracket, 9 | RBracket, 10 | LBrace, 11 | RBrace, 12 | LSquareBracket, 13 | RSquareBracket, 14 | LPointyBracket, 15 | RPointyBracket, 16 | Comma, 17 | Equals, 18 | String, 19 | Text, 20 | Newline, 21 | } 22 | 23 | #[derive(Copy, Clone)] 24 | enum LexerState { 25 | Free, 26 | PendingOutput(TokenWithLocation), 27 | InString(Location), 28 | InStringEscapedChar(Location), 29 | InText(Location, Location), 30 | } 31 | 32 | #[derive(Clone, Debug, PartialEq, Eq)] 33 | pub enum LexicalError { 34 | UnfinishedString { begin_index: Location }, 35 | } 36 | 37 | pub struct Lexer<'input> { 38 | chars: CharIndices<'input>, 39 | state: LexerState, 40 | } 41 | 42 | impl<'input> Lexer<'input> { 43 | pub fn new(input: &'input str) -> Self { 44 | Lexer { 45 | chars: input.char_indices(), 46 | state: LexerState::Free, 47 | } 48 | } 49 | } 50 | 51 | impl<'input> Iterator for Lexer<'input> { 52 | type Item = Result; 53 | fn next(&mut self) -> Option { 54 | for (i, c) in self.chars.by_ref() { 55 | let (output, new_state) = match self.state { 56 | LexerState::Free => match c { 57 | '"' => (None, LexerState::InString(i)), 58 | '{' => (Some((i, Token::LBrace, i + 1)), LexerState::Free), 59 | '}' => (Some((i, Token::RBrace, i + 1)), LexerState::Free), 60 | '[' => (Some((i, Token::LSquareBracket, i + 1)), LexerState::Free), 61 | ']' => (Some((i, Token::RSquareBracket, i + 1)), LexerState::Free), 62 | '(' => (Some((i, Token::LBracket, i + 1)), LexerState::Free), 63 | ')' => (Some((i, Token::RBracket, i + 1)), LexerState::Free), 64 | '<' => (Some((i, Token::LPointyBracket, i + 1)), LexerState::Free), 65 | '>' => (Some((i, Token::RPointyBracket, i + 1)), LexerState::Free), 66 | ',' => (Some((i, Token::Comma, i + 1)), LexerState::Free), 67 | '=' => (Some((i, Token::Equals, i + 1)), LexerState::Free), 68 | '\n' => (Some((i, Token::Newline, i + 1)), LexerState::Free), 69 | ' ' | '\t' => (None, LexerState::Free), 70 | _ => (None, LexerState::InText(i, i + 1)), 71 | }, 72 | LexerState::PendingOutput(output) => match c { 73 | '"' => (Some(output), LexerState::InString(i)), 74 | '{' => ( 75 | Some(output), 76 | LexerState::PendingOutput((i, Token::LBrace, i + 1)), 77 | ), 78 | '}' => ( 79 | Some(output), 80 | LexerState::PendingOutput((i, Token::RBrace, i + 1)), 81 | ), 82 | '[' => ( 83 | Some(output), 84 | LexerState::PendingOutput((i, Token::LSquareBracket, i + 1)), 85 | ), 86 | ']' => ( 87 | Some(output), 88 | LexerState::PendingOutput((i, Token::RSquareBracket, i + 1)), 89 | ), 90 | '(' => ( 91 | Some(output), 92 | LexerState::PendingOutput((i, Token::LBracket, i + 1)), 93 | ), 94 | ')' => ( 95 | Some(output), 96 | LexerState::PendingOutput((i, Token::RBracket, i + 1)), 97 | ), 98 | '<' => ( 99 | Some(output), 100 | LexerState::PendingOutput((i, Token::LPointyBracket, i + 1)), 101 | ), 102 | '>' => ( 103 | Some(output), 104 | LexerState::PendingOutput((i, Token::RPointyBracket, i + 1)), 105 | ), 106 | ',' => ( 107 | Some(output), 108 | LexerState::PendingOutput((i, Token::Comma, i + 1)), 109 | ), 110 | '=' => ( 111 | Some(output), 112 | LexerState::PendingOutput((i, Token::Equals, i + 1)), 113 | ), 114 | '\n' => ( 115 | Some(output), 116 | LexerState::PendingOutput((i, Token::Newline, i + 1)), 117 | ), 118 | ' ' | '\t' => (Some(output), LexerState::Free), 119 | _ => (Some(output), LexerState::InText(i, i + 1)), 120 | }, 121 | LexerState::InString(begin) => match c { 122 | '"' => (Some((begin, Token::String, i + 1)), LexerState::Free), 123 | '\\' => (None, LexerState::InStringEscapedChar(begin)), 124 | _ => (None, LexerState::InString(begin)), 125 | }, 126 | LexerState::InStringEscapedChar(begin) => (None, LexerState::InString(begin)), 127 | LexerState::InText(begin, end) => match c { 128 | '"' => (Some((begin, Token::Text, end)), LexerState::InString(i)), 129 | '{' => ( 130 | Some((begin, Token::Text, end)), 131 | LexerState::PendingOutput((i, Token::LBrace, i + 1)), 132 | ), 133 | '}' => ( 134 | Some((begin, Token::Text, end)), 135 | LexerState::PendingOutput((i, Token::RBrace, i + 1)), 136 | ), 137 | '[' => ( 138 | Some((begin, Token::Text, end)), 139 | LexerState::PendingOutput((i, Token::LSquareBracket, i + 1)), 140 | ), 141 | ']' => ( 142 | Some((begin, Token::Text, end)), 143 | LexerState::PendingOutput((i, Token::RSquareBracket, i + 1)), 144 | ), 145 | '(' => ( 146 | Some((begin, Token::Text, end)), 147 | LexerState::PendingOutput((i, Token::LBracket, i + 1)), 148 | ), 149 | ')' => ( 150 | Some((begin, Token::Text, end)), 151 | LexerState::PendingOutput((i, Token::RBracket, i + 1)), 152 | ), 153 | '<' => ( 154 | Some((begin, Token::Text, end)), 155 | LexerState::PendingOutput((i, Token::LPointyBracket, i + 1)), 156 | ), 157 | '>' => ( 158 | Some((begin, Token::Text, end)), 159 | LexerState::PendingOutput((i, Token::RPointyBracket, i + 1)), 160 | ), 161 | ',' => ( 162 | Some((begin, Token::Text, end)), 163 | LexerState::PendingOutput((i, Token::Comma, i + 1)), 164 | ), 165 | '=' => ( 166 | Some((begin, Token::Text, end)), 167 | LexerState::PendingOutput((i, Token::Equals, i + 1)), 168 | ), 169 | '\n' => ( 170 | Some((begin, Token::Text, end)), 171 | LexerState::PendingOutput((i, Token::Newline, i + 1)), 172 | ), 173 | ' ' | '\t' => (None, LexerState::InText(begin, end)), 174 | _ => (None, LexerState::InText(begin, i + 1)), 175 | }, 176 | }; 177 | self.state = new_state; 178 | if let Some(output) = output { 179 | return Some(Ok(output)); 180 | } 181 | } 182 | match self.state { 183 | LexerState::Free => None, 184 | LexerState::PendingOutput(output) => { 185 | self.state = LexerState::Free; 186 | Some(Ok(output)) 187 | } 188 | LexerState::InString(begin) | LexerState::InStringEscapedChar(begin) => { 189 | self.state = LexerState::Free; 190 | Some(Err(LexicalError::UnfinishedString { begin_index: begin })) 191 | } 192 | LexerState::InText(begin, end) => { 193 | self.state = LexerState::Free; 194 | Some(Ok((begin, Token::Text, end))) 195 | } 196 | } 197 | } 198 | } 199 | 200 | #[cfg(test)] 201 | mod test { 202 | use super::*; 203 | 204 | fn assert_eq_lexer_tokens(s: &'static str, expected_tokens: &[Token]) { 205 | let tokens = Lexer::new(s) 206 | .map(|t| t.map(|val| val.1)) 207 | .collect::, LexicalError>>() 208 | .unwrap(); 209 | assert_eq!(tokens.as_slice(), expected_tokens); 210 | } 211 | 212 | #[test] 213 | fn test_lexer_basic_success() { 214 | assert_eq_lexer_tokens("", &[]); 215 | assert_eq_lexer_tokens( 216 | "lj \"dlfj}[{}]=, \\t \\\"\" dfdf sadfad\n {{ []}, =\t\n\t", 217 | &[ 218 | Token::Text, 219 | Token::String, 220 | Token::Text, 221 | Token::Newline, 222 | Token::LBrace, 223 | Token::LBrace, 224 | Token::LSquareBracket, 225 | Token::RSquareBracket, 226 | Token::RBrace, 227 | Token::Comma, 228 | Token::Equals, 229 | Token::Newline, 230 | ], 231 | ); 232 | } 233 | 234 | fn assert_eq_lexer_error(s: &'static str, expected_error: LexicalError) { 235 | match Lexer::new(s) 236 | .map(|t| t.map(|val| val.1)) 237 | .collect::, LexicalError>>() 238 | { 239 | Ok(tokens) => panic!("Unexpected successful lexing of \"{}\": {:?}", s, tokens), 240 | Err(err) => assert_eq!(err, expected_error), 241 | } 242 | } 243 | 244 | #[test] 245 | fn test_lexer_error() { 246 | assert_eq_lexer_error("\"", LexicalError::UnfinishedString { begin_index: 0 }); 247 | assert_eq_lexer_error( 248 | "\"{,..\"\"", 249 | LexicalError::UnfinishedString { begin_index: 6 }, 250 | ); 251 | } 252 | } 253 | -------------------------------------------------------------------------------- /src/gdb_expression_parsing/mod.rs: -------------------------------------------------------------------------------- 1 | mod ast; 2 | mod lexer; 3 | mod parser; 4 | 5 | const ANON_KEY: &str = "*anon*"; 6 | 7 | pub type ParseError = parser::Error; 8 | 9 | pub fn parse_gdb_value(result_string: &str) -> Result { 10 | let lexer = lexer::Lexer::new(result_string); 11 | parser::parse(lexer, result_string) 12 | } 13 | 14 | #[derive(Debug, PartialEq)] 15 | pub enum Node<'a> { 16 | Leaf(&'a str), 17 | Array(Option<&'a str>, Vec>), 18 | Map(Option<&'a str>, Vec<(&'a str, Node<'a>)>), 19 | } 20 | 21 | #[derive(Clone, Copy)] 22 | pub enum Format { 23 | Decimal, 24 | Hex, 25 | Octal, 26 | Binary, 27 | } 28 | 29 | #[derive(Clone)] 30 | pub struct Value<'s> { 31 | pub node: &'s Node<'s>, 32 | pub format: Option, 33 | } 34 | 35 | impl<'n> unsegen_jsonviewer::Value for Value<'n> { 36 | fn visit<'s>(self) -> unsegen_jsonviewer::ValueVariant<'s, Self> { 37 | match self.node { 38 | Node::Leaf(s) => { 39 | let res = if let Some(format) = self.format { 40 | match parse_int::parse::(s) { 41 | Err(_) => s.to_string(), 42 | Ok(i) => match format { 43 | Format::Decimal => i.to_string(), 44 | Format::Hex => format!("{:#x}", i), 45 | Format::Octal => format!("{:#o}", i), 46 | Format::Binary => format!("{:#b}", i), 47 | }, 48 | } 49 | } else { 50 | s.to_string() 51 | }; 52 | unsegen_jsonviewer::ValueVariant::Scalar(res) 53 | } 54 | Node::Map(description, items) => unsegen_jsonviewer::ValueVariant::Map( 55 | description.map(|s| s.to_owned()), 56 | Box::new(items.iter().map(move |(s, v)| { 57 | ( 58 | s.to_string(), 59 | Value { 60 | node: v, 61 | format: self.format, 62 | }, 63 | ) 64 | })), 65 | ), 66 | Node::Array(description, items) => unsegen_jsonviewer::ValueVariant::Array( 67 | description.map(|s| s.to_owned()), 68 | Box::new(items.iter().map(move |v| Value { 69 | node: v, 70 | format: self.format, 71 | })), 72 | ), 73 | } 74 | } 75 | } 76 | 77 | #[cfg(test)] 78 | mod test { 79 | use super::*; 80 | 81 | #[test] 82 | fn test_parse_basic() { 83 | assert_eq!(parse_gdb_value("true").unwrap(), Node::Leaf("true")); 84 | assert_eq!(parse_gdb_value("false").unwrap(), Node::Leaf("false")); 85 | assert_eq!(parse_gdb_value("27").unwrap(), Node::Leaf("27")); //This is probably sufficient for us 86 | assert_eq!(parse_gdb_value("27.1").unwrap(), Node::Leaf("27.1")); 87 | assert_eq!(parse_gdb_value("\"dfd\"").unwrap(), Node::Leaf("\"dfd\"")); 88 | assert_eq!(parse_gdb_value(" l r ").unwrap(), Node::Leaf("l r")); 89 | assert_eq!(parse_gdb_value(" 0x123").unwrap(), Node::Leaf("0x123")); 90 | assert_eq!(parse_gdb_value(" -123").unwrap(), Node::Leaf("-123")); 91 | assert_eq!( 92 | parse_gdb_value("{}").unwrap(), 93 | Node::Array(None, Vec::new()) 94 | ); 95 | assert_eq!( 96 | parse_gdb_value("{...}").unwrap(), 97 | Node::Array(None, vec! { Node::Leaf("...") }) 98 | ); 99 | assert_eq!(parse_gdb_value("foo,bar").unwrap(), Node::Leaf("foo,bar")); 100 | assert_eq!(parse_gdb_value("foo}bar").unwrap(), Node::Leaf("foo}bar")); 101 | assert_eq!( 102 | parse_gdb_value("\nfoo\nbar").unwrap(), 103 | Node::Leaf("foo\nbar") 104 | ); 105 | } 106 | 107 | #[test] 108 | fn test_complex() { 109 | let testcase = "{ 110 | boolean = 128, 111 | x = 0, 112 | y = \"kdf\\\\j}{\\\"\", 113 | { 114 | z = 5.88163081e-39, 115 | w = 0 116 | }, 117 | named_inner = { 118 | v = 1.40129846e-45, 119 | w = 0 120 | }, 121 | bar = { 122 | x = 4295032831, 123 | y = 140737488347120, 124 | z = 4197294 125 | }, 126 | {...}, 127 | vec_empty = std::vector of length 0, capacity 0, 128 | map_empty = std::map with 0 elements, 129 | vec = std::vector of length 1, capacity 1 = {{ 130 | x = 1, 131 | y = 2, 132 | z = 3 133 | }}, 134 | map = std::map with 2 elements = { 135 | [\"blub\"] = 2, 136 | [\"foo\"] = 123 137 | }, 138 | uni_extern = {...}, 139 | uni_intern = {...}, 140 | const_array = {0, 0}, 141 | ptr = 0x400bf0 <__libc_csu_init>, 142 | array = 0x7fffffffe018 143 | }"; 144 | let result_obj = Node::Map( 145 | None, 146 | vec![ 147 | ("boolean", Node::Leaf("128")), 148 | ("x", Node::Leaf("0")), 149 | ("y", Node::Leaf("\"kdf\\\\j}{\\\"\"")), 150 | ( 151 | "named_inner", 152 | Node::Map( 153 | None, 154 | vec![("v", Node::Leaf("1.40129846e-45")), ("w", Node::Leaf("0"))], 155 | ), 156 | ), 157 | ( 158 | "bar", 159 | Node::Map( 160 | None, 161 | vec![ 162 | ("x", Node::Leaf("4295032831")), 163 | ("y", Node::Leaf("140737488347120")), 164 | ("z", Node::Leaf("4197294")), 165 | ], 166 | ), 167 | ), 168 | ( 169 | "vec_empty", 170 | Node::Leaf("std::vector of length 0, capacity 0"), 171 | ), 172 | ("map_empty", Node::Leaf("std::map with 0 elements")), 173 | ( 174 | "vec", 175 | Node::Array( 176 | Some("std::vector of length 1, capacity 1 ="), 177 | vec![Node::Map( 178 | None, 179 | vec![ 180 | ("x", Node::Leaf("1")), 181 | ("y", Node::Leaf("2")), 182 | ("z", Node::Leaf("3")), 183 | ], 184 | )], 185 | ), 186 | ), 187 | ( 188 | "map", 189 | Node::Map( 190 | Some("std::map with 2 elements ="), 191 | vec![ 192 | ("[\"blub\"]", Node::Leaf("2")), 193 | ("[\"foo\"]", Node::Leaf("123")), 194 | ], 195 | ), 196 | ), 197 | ("uni_extern", Node::Array(None, vec![Node::Leaf("...")])), 198 | ("uni_intern", Node::Array(None, vec![Node::Leaf("...")])), 199 | ( 200 | "const_array", 201 | Node::Array(None, vec![Node::Leaf("0"), Node::Leaf("0")]), 202 | ), 203 | ("ptr", Node::Leaf("0x400bf0 <__libc_csu_init>")), 204 | ("array", Node::Leaf("0x7fffffffe018")), 205 | ( 206 | ANON_KEY, 207 | Node::Array( 208 | None, 209 | vec![ 210 | Node::Map( 211 | None, 212 | vec![("z", Node::Leaf("5.88163081e-39")), ("w", Node::Leaf("0"))], 213 | ), 214 | Node::Array(None, vec![Node::Leaf("...")]), 215 | ], 216 | ), 217 | ), 218 | ], 219 | ); 220 | let r = parse_gdb_value(testcase).unwrap(); 221 | //println!("{}", r.pretty(2)); 222 | assert_eq!(r, result_obj); 223 | } 224 | 225 | #[test] 226 | fn test_parse_string() { 227 | //assert_eq!(parse_gdb_value("\"foo{]}]]}]<>,\\\\\""), Node::Leaf("\"foo{]}]]}]<>,\\\"".to_string())); 228 | assert_eq!(parse_gdb_value("\"foo\"").unwrap(), Node::Leaf("\"foo\"")); 229 | assert_eq!( 230 | parse_gdb_value("\"foo\\\"\"").unwrap(), 231 | Node::Leaf("\"foo\\\"\"") 232 | ); 233 | assert_eq!( 234 | parse_gdb_value("\"\\\\}{\\\"\"").unwrap(), 235 | Node::Leaf("\"\\\\}{\\\"\"") 236 | ); 237 | assert_eq!(parse_gdb_value("\"\\t\"").unwrap(), Node::Leaf("\"\\t\"")); 238 | assert_eq!(parse_gdb_value("\"\\n\"").unwrap(), Node::Leaf("\"\\n\"")); 239 | assert_eq!(parse_gdb_value("\"\\r\"").unwrap(), Node::Leaf("\"\\r\"")); 240 | assert_eq!( 241 | parse_gdb_value("\"kdf\\\\j}{\\\"\"").unwrap(), 242 | Node::Leaf("\"kdf\\\\j}{\\\"\"") 243 | ); 244 | } 245 | 246 | #[test] 247 | fn test_parse_something_else() { 248 | assert_eq!(parse_gdb_value("l r").unwrap(), Node::Leaf("l r")); 249 | assert_eq!(parse_gdb_value(" l r").unwrap(), Node::Leaf("l r")); 250 | assert_eq!(parse_gdb_value("l r ").unwrap(), Node::Leaf("l r")); 251 | assert_eq!(parse_gdb_value(" l r ").unwrap(), Node::Leaf("l r")); 252 | 253 | assert_eq!( 254 | parse_gdb_value("{ l r, l r}").unwrap(), 255 | Node::Array(None, vec![Node::Leaf("l r"), Node::Leaf("l r")]) 256 | ); 257 | assert_eq!( 258 | parse_gdb_value("{l r,l r}").unwrap(), 259 | Node::Array(None, vec![Node::Leaf("l r"), Node::Leaf("l r")]) 260 | ); 261 | assert_eq!( 262 | parse_gdb_value("{ l r ,l r }").unwrap(), 263 | Node::Array(None, vec![Node::Leaf("l r"), Node::Leaf("l r")]) 264 | ); 265 | assert_eq!( 266 | parse_gdb_value("{ l r , l r }").unwrap(), 267 | Node::Array(None, vec![Node::Leaf("l r"), Node::Leaf("l r")]) 268 | ); 269 | 270 | assert_eq!( 271 | parse_gdb_value("{foo =l r,bar =l r}").unwrap(), 272 | Node::Map( 273 | None, 274 | vec![("foo", Node::Leaf("l r")), ("bar", Node::Leaf("l r"))] 275 | ) 276 | ); 277 | assert_eq!( 278 | parse_gdb_value("{foo = l r ,bar =l r}").unwrap(), 279 | Node::Map( 280 | None, 281 | vec![("foo", Node::Leaf("l r")), ("bar", Node::Leaf("l r"))] 282 | ) 283 | ); 284 | assert_eq!( 285 | parse_gdb_value("{foo =l r,bar = l r }").unwrap(), 286 | Node::Map( 287 | None, 288 | vec![("foo", Node::Leaf("l r")), ("bar", Node::Leaf("l r"))] 289 | ) 290 | ); 291 | assert_eq!( 292 | parse_gdb_value("{foo = l r ,bar = l r }").unwrap(), 293 | Node::Map( 294 | None, 295 | vec![("foo", Node::Leaf("l r")), ("bar", Node::Leaf("l r"))] 296 | ) 297 | ); 298 | 299 | // GDB really does not make it easy for us... 300 | assert_eq!( 301 | parse_gdb_value("{int (int, int)} 0x400a76 ").unwrap(), 302 | Node::Leaf("{int (int, int)} 0x400a76 ") 303 | ); 304 | 305 | assert_eq!( 306 | parse_gdb_value("{ {int (int, int)} 0x400a76 }").unwrap(), 307 | Node::Array( 308 | None, 309 | vec![Node::Leaf("{int (int, int)} 0x400a76 ")] 310 | ) 311 | ); 312 | } 313 | 314 | #[test] 315 | fn test_parse_objects() { 316 | assert_eq!( 317 | parse_gdb_value(" { foo = 27}").unwrap(), 318 | Node::Map(None, vec![("foo", Node::Leaf("27"))]) 319 | ); 320 | assert_eq!( 321 | parse_gdb_value("{ foo = 27, bar = 37}").unwrap(), 322 | Node::Map( 323 | None, 324 | vec![("foo", Node::Leaf("27")), ("bar", Node::Leaf("37"))] 325 | ) 326 | ); 327 | assert_eq!( 328 | parse_gdb_value("{\n foo = 27,\n bar = 37\n }").unwrap(), 329 | Node::Map( 330 | None, 331 | vec![("foo", Node::Leaf("27")), ("bar", Node::Leaf("37"))] 332 | ) 333 | ); 334 | assert_eq!( 335 | parse_gdb_value("{{...}}").unwrap(), 336 | Node::Array(None, vec![Node::Array(None, vec![Node::Leaf("...")])]) 337 | ); 338 | assert_eq!( 339 | parse_gdb_value("{{}}").unwrap(), 340 | Node::Array(None, vec![Node::Array(None, vec![])]) 341 | ); 342 | assert_eq!( 343 | parse_gdb_value("{foo = 27, { bar=37}}").unwrap(), 344 | Node::Map( 345 | None, 346 | vec![ 347 | ("foo", Node::Leaf("27")), 348 | (ANON_KEY, Node::Map(None, vec![("bar", Node::Leaf("37"))])) 349 | ] 350 | ) 351 | ); 352 | assert_eq!( 353 | parse_gdb_value("{{ bar=37}, foo = 27}").unwrap(), 354 | Node::Map( 355 | None, 356 | vec![ 357 | ("foo", Node::Leaf("27")), 358 | (ANON_KEY, Node::Map(None, vec![("bar", Node::Leaf("37"))])) 359 | ] 360 | ) 361 | ); 362 | } 363 | 364 | #[test] 365 | fn test_parse_arrays() { 366 | assert_eq!( 367 | parse_gdb_value("{27}").unwrap(), 368 | Node::Array(None, vec![Node::Leaf("27")]) 369 | ); 370 | assert_eq!( 371 | parse_gdb_value("{ 27, 37}").unwrap(), 372 | Node::Array(None, vec![Node::Leaf("27"), Node::Leaf("37")]) 373 | ); 374 | assert_eq!( 375 | parse_gdb_value("{\n 27,\n 37\n}").unwrap(), 376 | Node::Array(None, vec![Node::Leaf("27"), Node::Leaf("37")]) 377 | ); 378 | } 379 | } 380 | -------------------------------------------------------------------------------- /src/gdb_expression_parsing/parser.rs: -------------------------------------------------------------------------------- 1 | use super::lexer::{Lexer, LexicalError, Token}; 2 | use super::Node; 3 | 4 | /// These are "stage 2 tokens", which only distinguish between the parts of the expression relevant 5 | /// for the parser below. See collapse_tokens for the conversion. 6 | #[derive(Debug, PartialEq, Copy, Clone)] 7 | pub enum TokenS2 { 8 | Text, 9 | LBrace, 10 | RBrace, 11 | Equals, 12 | Newline, 13 | Comma, 14 | } 15 | 16 | /// Create stage 2 tokens from tokens generated by the lexer. Here, all structured expressions 17 | /// (using `()`, `[]` or `<>`) will be collapsed into a single text token that can be handled much 18 | /// more easilty by the parser. 19 | fn collapse_tokens( 20 | tokens: impl Iterator, 21 | ) -> Vec<(usize, TokenS2, usize)> { 22 | enum State { 23 | Free, 24 | SimpleText(usize, usize), 25 | StructuredText(usize, usize, Token, usize), 26 | } 27 | 28 | let mut ret = Vec::new(); 29 | let mut state = State::Free; 30 | 31 | for (begin, tok, end) in tokens { 32 | state = match state { 33 | State::Free => match tok { 34 | Token::String 35 | | Token::Text 36 | | Token::RSquareBracket 37 | | Token::RBracket 38 | | Token::RPointyBracket => State::SimpleText(begin, end), 39 | Token::LSquareBracket => { 40 | State::StructuredText(begin, end, Token::RSquareBracket, 1) 41 | } 42 | Token::LBracket => State::StructuredText(begin, end, Token::RBracket, 1), 43 | Token::LPointyBracket => { 44 | State::StructuredText(begin, end, Token::RPointyBracket, 1) 45 | } 46 | Token::LBrace => { 47 | ret.push((begin, TokenS2::LBrace, end)); 48 | State::Free 49 | } 50 | Token::RBrace => { 51 | ret.push((begin, TokenS2::RBrace, end)); 52 | State::Free 53 | } 54 | Token::Equals => { 55 | ret.push((begin, TokenS2::Equals, end)); 56 | State::Free 57 | } 58 | Token::Newline => { 59 | ret.push((begin, TokenS2::Newline, end)); 60 | State::Free 61 | } 62 | Token::Comma => { 63 | ret.push((begin, TokenS2::Comma, end)); 64 | State::Free 65 | } 66 | }, 67 | State::SimpleText(b, e) => match tok { 68 | Token::String 69 | | Token::Text 70 | | Token::RSquareBracket 71 | | Token::RBracket 72 | | Token::RPointyBracket => State::SimpleText(b, end), 73 | Token::LSquareBracket => State::StructuredText(b, end, Token::RSquareBracket, 1), 74 | Token::LBracket => State::StructuredText(b, end, Token::RBracket, 1), 75 | Token::LPointyBracket => State::StructuredText(b, end, Token::RPointyBracket, 1), 76 | Token::LBrace => { 77 | ret.push((b, TokenS2::Text, e)); 78 | ret.push((begin, TokenS2::LBrace, end)); 79 | State::Free 80 | } 81 | Token::RBrace => { 82 | ret.push((b, TokenS2::Text, e)); 83 | ret.push((begin, TokenS2::RBrace, end)); 84 | State::Free 85 | } 86 | Token::Equals => { 87 | ret.push((b, TokenS2::Text, e)); 88 | ret.push((begin, TokenS2::Equals, end)); 89 | State::Free 90 | } 91 | Token::Newline => { 92 | ret.push((b, TokenS2::Text, e)); 93 | ret.push((begin, TokenS2::Newline, end)); 94 | State::Free 95 | } 96 | Token::Comma => { 97 | ret.push((b, TokenS2::Text, e)); 98 | ret.push((begin, TokenS2::Comma, end)); 99 | State::Free 100 | } 101 | }, 102 | State::StructuredText(b, _, endtoken, depth) => { 103 | if tok == endtoken { 104 | if depth == 1 { 105 | State::SimpleText(b, end) 106 | } else { 107 | State::StructuredText(b, end, endtoken, depth - 1) 108 | } 109 | } else { 110 | State::StructuredText(b, end, endtoken, depth) 111 | } 112 | } 113 | } 114 | } 115 | match state { 116 | State::Free => {} 117 | State::SimpleText(b, e) | State::StructuredText(b, e, _, _) => { 118 | ret.push((b, TokenS2::Text, e)); 119 | } 120 | } 121 | ret 122 | } 123 | 124 | enum Struct<'s> { 125 | Array(Vec>), 126 | Map(Vec<(&'s str, Node<'s>)>), 127 | } 128 | 129 | fn parse_structure<'s>(tokens: &mut Tokens, string: &'s str) -> Result, ()> { 130 | let mut kv = Vec::new(); 131 | let mut single = Vec::new(); 132 | let multiline = if let [(_, TokenS2::Newline, _), ..] = tokens.tokens { 133 | tokens.consume(1); 134 | true 135 | } else { 136 | false 137 | }; 138 | if let [(_, TokenS2::RBrace, _), ..] = tokens.tokens { 139 | tokens.consume(1); 140 | return Ok(Struct::Array(Vec::new())); 141 | } 142 | loop { 143 | let key = match *tokens.tokens { 144 | [(b, TokenS2::Text, e), (_, TokenS2::Equals, _), ..] => { 145 | let res = Some((b, e)); 146 | tokens.consume(2); 147 | res 148 | } 149 | _ => None, 150 | }; 151 | let val = parse_value( 152 | tokens, 153 | string, 154 | if multiline { 155 | ValueContext::MultiLineStruct 156 | } else { 157 | ValueContext::SingleLineStruct 158 | }, 159 | ); 160 | if let Some((b, e)) = key { 161 | kv.push((&string[b..e], val)); 162 | } else { 163 | single.push(val); 164 | } 165 | match (multiline, tokens.tokens) { 166 | (true, &[(_, TokenS2::Comma, _), (_, TokenS2::Newline, _), ..]) => { 167 | tokens.consume(2); 168 | } 169 | (false, &[(_, TokenS2::Comma, _), ..]) => { 170 | tokens.consume(1); 171 | } 172 | (false, &[(_, TokenS2::RBrace, _), ..]) 173 | | (true, &[(_, TokenS2::Newline, _), (_, TokenS2::RBrace, _), ..]) => { 174 | tokens.consume(if multiline { 2 } else { 1 }); 175 | return Ok(match (kv.is_empty(), single.len()) { 176 | (true, 0) => Struct::Array(Vec::new()), 177 | (true, _) => Struct::Array(single), 178 | (false, 0) => Struct::Map(kv), 179 | (false, 1) => { 180 | kv.push((super::ANON_KEY, single.drain(..).next().unwrap())); 181 | Struct::Map(kv) 182 | } 183 | (false, _) => { 184 | kv.push((super::ANON_KEY, Node::Array(None, single))); 185 | Struct::Map(kv) 186 | } 187 | }); 188 | } 189 | _ => { 190 | return Err(()); 191 | } 192 | } 193 | } 194 | } 195 | 196 | struct Tokens<'a> { 197 | tokens: &'a [(usize, TokenS2, usize)], 198 | } 199 | 200 | impl Tokens<'_> { 201 | fn consume(&mut self, n: usize) { 202 | self.tokens = &self.tokens[n..]; 203 | } 204 | } 205 | 206 | #[derive(Copy, Clone)] 207 | enum ValueContext { 208 | Free, 209 | SingleLineStruct, 210 | MultiLineStruct, 211 | } 212 | 213 | pub type Error = LexicalError; 214 | 215 | // TODO: Use or-patterns starting from 1.53? 216 | fn parse_value<'s>(tokens: &mut Tokens, string: &'s str, context: ValueContext) -> Node<'s> { 217 | let mut text_begin = None; 218 | let mut text_end = 0; 219 | let update_range = 220 | |text_begin: &mut Option, text_end: &mut usize, b: usize, e: usize| { 221 | *text_begin = Some(text_begin.unwrap_or(b)); 222 | *text_end = (*text_end).max(e); 223 | }; 224 | let mut current_struct = None; 225 | loop { 226 | match (context, tokens.tokens) { 227 | (ValueContext::SingleLineStruct, &[(_, TokenS2::Comma, _), ..]) 228 | | ( 229 | ValueContext::MultiLineStruct, 230 | &[(_, TokenS2::Comma, _), (_, TokenS2::Newline, _), ..], 231 | ) 232 | | (ValueContext::SingleLineStruct, &[(_, TokenS2::RBrace, _), ..]) 233 | | (ValueContext::MultiLineStruct, &[(_, TokenS2::RBrace, _), ..]) 234 | | ( 235 | ValueContext::MultiLineStruct, 236 | &[(_, TokenS2::Newline, _), (_, TokenS2::RBrace, _), ..], 237 | ) 238 | | (_, &[]) => { 239 | let text_begin = text_begin.unwrap_or(text_end); 240 | let text = &string[text_begin..text_end]; 241 | let description = if text.is_empty() { None } else { Some(text) }; 242 | return match current_struct { 243 | None => Node::Leaf(text), 244 | Some(Struct::Map(v)) => Node::Map(description, v), 245 | Some(Struct::Array(v)) => Node::Array(description, v), 246 | }; 247 | } 248 | (_, &[(b, TokenS2::Text, e), ..]) 249 | | (_, &[(b, TokenS2::Equals, e), ..]) 250 | | (ValueContext::MultiLineStruct, &[(b, TokenS2::Comma, e), ..]) 251 | | (ValueContext::Free, &[(b, TokenS2::Comma, e), ..]) 252 | | (ValueContext::Free, &[(b, TokenS2::RBrace, e), ..]) => { 253 | update_range(&mut text_begin, &mut text_end, b, e); 254 | current_struct = None; 255 | tokens.consume(1); 256 | } 257 | (_, &[(_, TokenS2::Newline, _), ..]) => { 258 | tokens.consume(1); 259 | } 260 | (_, &[(b, TokenS2::LBrace, _), ..]) => { 261 | if text_begin.is_none() { 262 | text_begin = Some(b); 263 | text_end = b; 264 | } 265 | tokens.consume(1); 266 | if let Ok(strct) = parse_structure(tokens, string) { 267 | current_struct = Some(strct); 268 | } 269 | } 270 | } 271 | } 272 | } 273 | 274 | pub fn parse<'s>(lexer: Lexer, string: &'s str) -> Result, Error> { 275 | let tokens = lexer.into_iter().collect::, _>>()?; 276 | let tokens = collapse_tokens(tokens.into_iter()); 277 | let mut tokens = Tokens { 278 | tokens: &tokens[..], 279 | }; 280 | Ok(parse_value(&mut tokens, string, ValueContext::Free)) 281 | } 282 | 283 | #[cfg(test)] 284 | mod test { 285 | use super::super::lexer::Token; 286 | use super::*; 287 | 288 | #[test] 289 | fn collapse_tokens_simple() { 290 | assert_eq!( 291 | collapse_tokens( 292 | vec![ 293 | (0, Token::Text, 1), 294 | (1, Token::String, 4), 295 | (4, Token::Text, 5) 296 | ] 297 | .into_iter() 298 | ), 299 | vec![(0, TokenS2::Text, 5)] 300 | ); 301 | 302 | assert_eq!( 303 | collapse_tokens( 304 | vec![ 305 | (0, Token::Text, 1), 306 | (1, Token::String, 4), 307 | (4, Token::Comma, 5) 308 | ] 309 | .into_iter() 310 | ), 311 | vec![(0, TokenS2::Text, 4), (4, TokenS2::Comma, 5)] 312 | ); 313 | } 314 | 315 | #[test] 316 | fn collapse_tokens_structured() { 317 | assert_eq!( 318 | collapse_tokens( 319 | vec![ 320 | (0, Token::Text, 1), 321 | (1, Token::LSquareBracket, 4), 322 | (4, Token::Text, 5), 323 | (5, Token::RBrace, 6), 324 | (6, Token::String, 7), 325 | (9, Token::RSquareBracket, 10), 326 | (10, Token::String, 11), 327 | ] 328 | .into_iter() 329 | ), 330 | vec![(0, TokenS2::Text, 11)] 331 | ); 332 | } 333 | } 334 | -------------------------------------------------------------------------------- /src/gdbmi/commands.rs: -------------------------------------------------------------------------------- 1 | use std::ffi::OsString; 2 | use std::fmt; 3 | use std::io::{Error, Write}; 4 | use std::path::Path; 5 | 6 | #[derive(Debug, Clone)] 7 | pub struct MiCommand { 8 | operation: &'static str, 9 | options: Vec, 10 | parameters: Vec, 11 | } 12 | 13 | pub enum DisassembleMode { 14 | DisassemblyOnly = 0, 15 | DisassemblyWithRawOpcodes = 2, 16 | MixedSourceAndDisassembly = 1, // deprecated and 4 would be preferred, but might not be available in older gdb(mi) versions 17 | MixedSourceAndDisassemblyWithRawOpcodes = 3, // deprecated and 5 would be preferred, same as above 18 | } 19 | 20 | pub enum WatchMode { 21 | Read, 22 | Write, 23 | Access, 24 | } 25 | 26 | pub enum BreakPointLocation<'a> { 27 | Address(usize), 28 | Function(&'a Path, &'a str), 29 | Line(&'a Path, usize), 30 | } 31 | 32 | #[derive(Copy, Clone, Hash, PartialEq, Eq)] 33 | pub struct BreakPointNumber { 34 | pub major: usize, 35 | pub minor: Option, 36 | } 37 | 38 | impl std::str::FromStr for BreakPointNumber { 39 | type Err = String; 40 | fn from_str(s: &str) -> Result { 41 | if let Some(dot_pos) = s.find('.') { 42 | let major = s[..dot_pos].parse::().map_err(|e| e.to_string())?; 43 | let minor = s[dot_pos + 1..] 44 | .parse::() 45 | .map_err(|e| e.to_string())?; 46 | Ok(BreakPointNumber { 47 | major, 48 | minor: Some(minor), 49 | }) 50 | } else { 51 | match s.parse::() { 52 | Ok(val) => Ok(BreakPointNumber { 53 | major: val, 54 | minor: None, 55 | }), 56 | Err(e) => Err(e.to_string()), 57 | } 58 | } 59 | } 60 | } 61 | 62 | impl fmt::Display for BreakPointNumber { 63 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 64 | if let Some(minor) = self.minor { 65 | write!(f, "{}.{}", self.major, minor) 66 | } else { 67 | write!(f, "{}", self.major) 68 | } 69 | } 70 | } 71 | 72 | fn escape_command(input: &str) -> String { 73 | let mut output = String::new(); 74 | output.push('\"'); 75 | for c in input.chars() { 76 | match c { 77 | '\\' => output.push_str("\\\\"), 78 | '\"' => output.push_str("\\\""), 79 | '\r' => output.push_str("\\\r"), 80 | '\n' => output.push_str("\\\n"), 81 | other => output.push(other), 82 | } 83 | } 84 | output.push('\"'); 85 | output 86 | } 87 | 88 | impl MiCommand { 89 | pub fn write_interpreter_string( 90 | &self, 91 | sink: &mut S, 92 | token: super::Token, 93 | ) -> Result<(), Error> { 94 | use std::os::unix::ffi::OsStrExt; 95 | write!(sink, "{}-{}", token, self.operation)?; 96 | for option in &self.options { 97 | write!(sink, " ")?; 98 | sink.write_all(option.as_bytes())?; 99 | } 100 | if !self.parameters.is_empty() && !self.options.is_empty() { 101 | write!(sink, " --")?; 102 | } 103 | for parameter in &self.parameters { 104 | write!(sink, " ")?; 105 | sink.write_all(parameter.as_bytes())?; 106 | } 107 | writeln!(sink)?; 108 | Ok(()) 109 | } 110 | pub fn interpreter_exec, S2: Into>( 111 | interpreter: S1, 112 | command: S2, 113 | ) -> MiCommand { 114 | MiCommand { 115 | operation: "interpreter-exec", 116 | options: vec![interpreter.into(), command.into()], 117 | parameters: Vec::new(), 118 | } 119 | } 120 | 121 | pub fn cli_exec(command: &str) -> MiCommand { 122 | Self::interpreter_exec("console".to_owned(), escape_command(command)) 123 | } 124 | 125 | pub fn data_disassemble_file>( 126 | file: P, 127 | linenum: usize, 128 | lines: Option, 129 | mode: DisassembleMode, 130 | ) -> MiCommand { 131 | MiCommand { 132 | operation: "data-disassemble", 133 | options: vec![ 134 | OsString::from("-f"), 135 | OsString::from(file.as_ref()), 136 | OsString::from("-l"), 137 | OsString::from(linenum.to_string()), 138 | OsString::from("-n"), 139 | OsString::from(lines.map(|l| l as isize).unwrap_or(-1).to_string()), 140 | ], 141 | parameters: vec![OsString::from((mode as u8).to_string())], 142 | } 143 | } 144 | 145 | pub fn data_disassemble_address( 146 | start_addr: usize, 147 | end_addr: usize, 148 | mode: DisassembleMode, 149 | ) -> MiCommand { 150 | MiCommand { 151 | operation: "data-disassemble", 152 | options: vec![ 153 | OsString::from("-s"), 154 | OsString::from(start_addr.to_string()), 155 | OsString::from("-e"), 156 | OsString::from(end_addr.to_string()), 157 | ], 158 | parameters: vec![OsString::from((mode as u8).to_string())], 159 | } 160 | } 161 | 162 | pub fn data_evaluate_expression(expression: String) -> MiCommand { 163 | MiCommand { 164 | operation: "data-evaluate-expression", 165 | options: vec![OsString::from(format!("\"{}\"", expression))], //TODO: maybe we need to quote existing " in expression. Is this even possible? 166 | parameters: vec![], 167 | } 168 | } 169 | 170 | pub fn insert_breakpoint(location: BreakPointLocation) -> MiCommand { 171 | MiCommand { 172 | operation: "break-insert", 173 | options: match location { 174 | BreakPointLocation::Address(addr) => vec![OsString::from(format!("*0x{:x}", addr))], 175 | BreakPointLocation::Function(path, func_name) => { 176 | let mut ret = OsString::from(path); 177 | ret.push(":"); 178 | ret.push(func_name); 179 | vec![ret] 180 | 181 | // Not available in old gdb(mi) versions 182 | //vec![ 183 | // OsString::from("--source"), 184 | // OsString::from(path), 185 | // OsString::from("--function"), 186 | // OsString::from(func_name), 187 | //] 188 | } 189 | BreakPointLocation::Line(path, line_number) => { 190 | let mut ret = OsString::from(path); 191 | ret.push(":"); 192 | ret.push(line_number.to_string()); 193 | vec![ret] 194 | 195 | // Not available in old gdb(mi) versions 196 | //vec![ 197 | //OsString::from("--source"), 198 | //OsString::from(path), 199 | //OsString::from("--line"), 200 | //OsString::from(format!("{}", line_number)), 201 | //], 202 | } 203 | }, 204 | parameters: Vec::new(), 205 | } 206 | } 207 | 208 | pub fn delete_breakpoints>( 209 | breakpoint_numbers: I, 210 | ) -> MiCommand { 211 | //let options = options: breakpoint_numbers.map(|n| format!("{} ", n)).collect(), 212 | //GDB is broken: see http://sourceware-org.1504.n7.nabble.com/Bug-breakpoints-20133-New-unable-to-delete-a-sub-breakpoint-td396197.html 213 | let mut options = breakpoint_numbers 214 | .map(|n| format!("{} ", n.major).into()) 215 | .collect::>(); 216 | options.sort(); 217 | options.dedup(); 218 | MiCommand { 219 | operation: "break-delete", 220 | options, 221 | parameters: Vec::new(), 222 | } 223 | } 224 | 225 | pub fn insert_watchpoing(expression: &str, mode: WatchMode) -> MiCommand { 226 | let options = match mode { 227 | WatchMode::Write => Vec::new(), 228 | WatchMode::Read => vec!["-r".into()], 229 | WatchMode::Access => vec!["-a".into()], 230 | }; 231 | MiCommand { 232 | operation: "break-watch", 233 | options, 234 | parameters: vec![expression.into()], 235 | } 236 | } 237 | 238 | pub fn environment_pwd() -> MiCommand { 239 | MiCommand { 240 | operation: "environment-pwd", 241 | options: Vec::new(), 242 | parameters: Vec::new(), 243 | } 244 | } 245 | 246 | // Be aware: This does not seem to always interrupt execution. 247 | // Use gdb.interrupt_execution instead. 248 | pub fn exec_interrupt() -> MiCommand { 249 | MiCommand { 250 | operation: "exec-interrupt", 251 | options: Vec::new(), 252 | parameters: Vec::new(), 253 | } 254 | } 255 | 256 | // Warning: This cannot be used to pass special characters like \n to gdb because 257 | // (unlike it is said in the spec) there is apparently no way to pass \n unescaped 258 | // to gdb, and for "exec-arguments" gdb somehow does not unescape these chars... 259 | pub fn exec_arguments(args: Vec) -> MiCommand { 260 | MiCommand { 261 | operation: "exec-arguments", 262 | options: args, 263 | parameters: Vec::new(), 264 | } 265 | } 266 | 267 | pub fn exit() -> MiCommand { 268 | MiCommand { 269 | operation: "gdb-exit", 270 | options: Vec::new(), 271 | parameters: Vec::new(), 272 | } 273 | } 274 | 275 | pub fn select_frame(frame_number: u64) -> MiCommand { 276 | MiCommand { 277 | operation: "stack-select-frame", 278 | options: vec![frame_number.to_string().into()], 279 | parameters: Vec::new(), 280 | } 281 | } 282 | 283 | pub fn stack_info_frame(frame_number: Option) -> MiCommand { 284 | MiCommand { 285 | operation: "stack-info-frame", 286 | options: if let Some(frame_number) = frame_number { 287 | vec![frame_number.to_string().into()] 288 | } else { 289 | vec![] 290 | }, 291 | parameters: Vec::new(), 292 | } 293 | } 294 | 295 | pub fn stack_info_depth() -> MiCommand { 296 | MiCommand { 297 | operation: "stack-info-depth", 298 | options: Vec::new(), 299 | parameters: Vec::new(), 300 | } 301 | } 302 | 303 | pub fn stack_list_variables( 304 | thread_number: Option, 305 | frame_number: Option, 306 | ) -> MiCommand { 307 | let mut parameters = vec![]; 308 | if let Some(thread_number) = thread_number { 309 | parameters.push("--thread".into()); 310 | parameters.push(thread_number.to_string().into()); 311 | } 312 | if let Some(frame_number) = frame_number { 313 | parameters.push("--frame".into()); 314 | parameters.push(frame_number.to_string().into()); 315 | } 316 | parameters.push("--simple-values".into()); //TODO: make configurable if required. 317 | MiCommand { 318 | operation: "stack-list-variables", 319 | options: Vec::new(), 320 | parameters, 321 | } 322 | } 323 | 324 | pub fn thread_info(thread_id: Option) -> MiCommand { 325 | MiCommand { 326 | operation: "thread-info", 327 | options: if let Some(id) = thread_id { 328 | vec![id.to_string().into()] 329 | } else { 330 | vec![] 331 | }, 332 | parameters: Vec::new(), 333 | } 334 | } 335 | 336 | pub fn file_exec_and_symbols(file: &Path) -> MiCommand { 337 | MiCommand { 338 | operation: "file-exec-and-symbols", 339 | options: vec![file.into()], 340 | parameters: Vec::new(), 341 | } 342 | } 343 | 344 | pub fn file_symbol_file(file: Option<&Path>) -> MiCommand { 345 | MiCommand { 346 | operation: "file-symbol-file", 347 | options: if let Some(file) = file { 348 | vec![file.into()] 349 | } else { 350 | vec![] 351 | }, 352 | parameters: Vec::new(), 353 | } 354 | } 355 | 356 | pub fn list_thread_groups(list_all_available: bool, thread_group_ids: &[u32]) -> MiCommand { 357 | MiCommand { 358 | operation: "list-thread-groups", 359 | options: if list_all_available { 360 | vec![OsString::from("--available")] 361 | } else { 362 | vec![] 363 | }, 364 | parameters: thread_group_ids 365 | .iter() 366 | .map(|id| id.to_string().into()) 367 | .collect(), 368 | } 369 | } 370 | 371 | pub fn var_create( 372 | name: Option, /*none: generate name*/ 373 | expression: &str, 374 | frame_addr: Option, /*none: current frame*/ 375 | ) -> MiCommand { 376 | MiCommand { 377 | operation: "var-create", 378 | options: vec![], 379 | parameters: vec![ 380 | name.unwrap_or_else(|| "\"-\"".into()), 381 | frame_addr 382 | .map(|s| s.to_string()) 383 | .unwrap_or_else(|| "\"*\"".to_string()) 384 | .into(), 385 | escape_command(expression).into(), 386 | ], 387 | } 388 | } 389 | pub fn var_delete(name: impl Into, delete_children: bool) -> MiCommand { 390 | let mut parameters = vec![]; 391 | if delete_children { 392 | parameters.push("-c".into()); 393 | } 394 | parameters.push(name.into()); 395 | MiCommand { 396 | operation: "var-delete", 397 | options: Vec::new(), 398 | parameters, 399 | } 400 | } 401 | pub fn var_list_children( 402 | name: impl Into, 403 | print_values: bool, 404 | from_to: Option>, 405 | ) -> MiCommand { 406 | let mut com = MiCommand { 407 | operation: "var-list-children", 408 | options: vec![], 409 | parameters: vec![ 410 | if print_values { 411 | "--all-values" 412 | } else { 413 | "--no-values" 414 | } 415 | .into(), 416 | name.into(), 417 | ], 418 | }; 419 | if let Some(from_to) = from_to { 420 | com.parameters 421 | .push(OsString::from(from_to.start.to_string())); 422 | com.parameters.push(OsString::from(from_to.end.to_string())); 423 | } 424 | com 425 | } 426 | } 427 | -------------------------------------------------------------------------------- /src/gdbmi/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod commands; 2 | pub mod output; 3 | 4 | use log::info; 5 | use std::ffi::OsString; 6 | use std::path::Path; 7 | use std::path::PathBuf; 8 | use std::process::{Child, ChildStdin, Command, Stdio}; 9 | use std::sync::atomic::{AtomicBool, Ordering}; 10 | use std::sync::{mpsc, Arc}; 11 | use std::thread; 12 | 13 | type Token = u64; 14 | 15 | #[allow(clippy::upper_case_acronyms)] 16 | pub struct GDB { 17 | pub process: Child, 18 | stdin: ChildStdin, 19 | is_running: Arc, 20 | result_output: mpsc::Receiver, 21 | current_command_token: Token, 22 | binary_path: PathBuf, 23 | init_options: Vec, 24 | //outputThread: thread::Thread, 25 | } 26 | 27 | pub trait OutOfBandRecordSink: std::marker::Send { 28 | fn send(&self, record: output::OutOfBandRecord); 29 | } 30 | 31 | #[derive(Clone, Debug, PartialEq)] 32 | pub enum ExecuteError { 33 | Busy, 34 | Quit, 35 | } 36 | 37 | pub struct GDBBuilder { 38 | gdb_path: PathBuf, 39 | opt_nh: bool, 40 | opt_nx: bool, 41 | opt_quiet: bool, 42 | opt_cd: Option, 43 | opt_bps: Option, 44 | opt_symbol_file: Option, 45 | opt_core_file: Option, 46 | opt_proc_id: Option, 47 | opt_command: Option, 48 | opt_source_dir: Option, 49 | opt_args: Vec, 50 | opt_program: Option, 51 | opt_tty: Option, 52 | rr_args: Option<(PathBuf, Vec)>, 53 | } 54 | impl GDBBuilder { 55 | pub fn new(gdb: PathBuf) -> Self { 56 | GDBBuilder { 57 | gdb_path: gdb, 58 | opt_nh: false, 59 | opt_nx: false, 60 | opt_quiet: false, 61 | opt_cd: None, 62 | opt_bps: None, 63 | opt_symbol_file: None, 64 | opt_core_file: None, 65 | opt_proc_id: None, 66 | opt_command: None, 67 | opt_source_dir: None, 68 | opt_args: Vec::new(), 69 | opt_program: None, 70 | opt_tty: None, 71 | rr_args: None, 72 | } 73 | } 74 | 75 | pub fn nh(mut self) -> Self { 76 | self.opt_nh = true; 77 | self 78 | } 79 | pub fn nx(mut self) -> Self { 80 | self.opt_nx = true; 81 | self 82 | } 83 | pub fn rr_args(mut self, binary: PathBuf, args: Vec) -> Self { 84 | self.rr_args = Some((binary, args)); 85 | self 86 | } 87 | pub fn quiet(mut self) -> Self { 88 | self.opt_quiet = true; 89 | self 90 | } 91 | pub fn working_dir(mut self, dir: PathBuf) -> Self { 92 | self.opt_cd = Some(dir); 93 | self 94 | } 95 | pub fn bps(mut self, bps: u32) -> Self { 96 | self.opt_bps = Some(bps); 97 | self 98 | } 99 | pub fn symbol_file(mut self, file: PathBuf) -> Self { 100 | self.opt_symbol_file = Some(file); 101 | self 102 | } 103 | pub fn core_file(mut self, file: PathBuf) -> Self { 104 | self.opt_core_file = Some(file); 105 | self 106 | } 107 | pub fn proc_id(mut self, pid: u32) -> Self { 108 | self.opt_proc_id = Some(pid); 109 | self 110 | } 111 | pub fn command_file(mut self, command_file: PathBuf) -> Self { 112 | self.opt_command = Some(command_file); 113 | self 114 | } 115 | pub fn source_dir(mut self, dir: PathBuf) -> Self { 116 | self.opt_source_dir = Some(dir); 117 | self 118 | } 119 | pub fn args(mut self, args: &[OsString]) -> Self { 120 | self.opt_args.extend_from_slice(args); 121 | self 122 | } 123 | pub fn program(mut self, program: PathBuf) -> Self { 124 | self.opt_program = Some(program); 125 | self 126 | } 127 | pub fn tty(mut self, tty: PathBuf) -> Self { 128 | self.opt_tty = Some(tty); 129 | self 130 | } 131 | pub fn try_spawn(self, oob_sink: S) -> Result 132 | where 133 | S: OutOfBandRecordSink + 'static, 134 | { 135 | let mut gdb_args = Vec::::new(); 136 | let mut init_options = Vec::::new(); 137 | if self.opt_nh { 138 | gdb_args.push("--nh".into()); 139 | init_options.push("--nh".into()); 140 | } 141 | if self.opt_nx { 142 | gdb_args.push("--nx".into()); 143 | init_options.push("--nx".into()); 144 | } 145 | if self.opt_quiet { 146 | gdb_args.push("--quiet".into()); 147 | } 148 | if let Some(cd) = self.opt_cd { 149 | gdb_args.push("--cd=".into()); 150 | gdb_args.last_mut().unwrap().push(&cd); 151 | } 152 | if let Some(bps) = self.opt_bps { 153 | gdb_args.push("-b".into()); 154 | gdb_args.push(bps.to_string().into()); 155 | } 156 | if let Some(symbol_file) = self.opt_symbol_file { 157 | gdb_args.push("--symbols=".into()); 158 | gdb_args.last_mut().unwrap().push(&symbol_file); 159 | } 160 | if let Some(core_file) = self.opt_core_file { 161 | gdb_args.push("--core=".into()); 162 | gdb_args.last_mut().unwrap().push(&core_file); 163 | } 164 | if let Some(proc_id) = self.opt_proc_id { 165 | gdb_args.push("--pid=".into()); 166 | gdb_args.last_mut().unwrap().push(proc_id.to_string()); 167 | } 168 | if let Some(command) = self.opt_command { 169 | gdb_args.push("--command=".into()); 170 | gdb_args.last_mut().unwrap().push(&command); 171 | } 172 | if let Some(source_dir) = self.opt_source_dir { 173 | gdb_args.push("--directory=".into()); 174 | gdb_args.last_mut().unwrap().push(&source_dir); 175 | } 176 | if let Some(tty) = self.opt_tty { 177 | gdb_args.push("--tty=".into()); 178 | gdb_args.last_mut().unwrap().push(&tty); 179 | } 180 | if !self.opt_args.is_empty() { 181 | gdb_args.push("--args".into()); 182 | gdb_args.push(self.opt_program.unwrap().into()); 183 | for arg in self.opt_args { 184 | gdb_args.push(arg); 185 | } 186 | } else if let Some(program) = self.opt_program { 187 | gdb_args.push(program.into()); 188 | } 189 | 190 | let mut child = if let Some(rr_args) = self.rr_args { 191 | // It looks like rr acts as a remote target for gdb and "runs" (or simulates) the 192 | // binary itself. Consequently, it also is responsible for stdin/stdout handling. 193 | // Without "-q" it appears to pass all stdout to the terminal/output that gdb is 194 | // connected to as well, which may confuse our gdbmi parser. For this reason we disable 195 | // output using the "-q" flag. 196 | // 197 | // This also means that the --tty flag that we pass to gdb is useless. In order to get 198 | // proper output in the ugdb's terminal emulator we would need a "tty" option in rr 199 | // itself, I think. 200 | let args = gdb_args 201 | .into_iter() 202 | .filter(|v| !v.to_string_lossy().contains("--tty")) 203 | .flat_map(|arg| vec!["-o".into(), arg]); 204 | 205 | let silence_arg = "-q"; 206 | 207 | Command::new(rr_args.0) 208 | .arg("replay") 209 | .arg("--interpreter=mi") 210 | .arg(silence_arg) 211 | .arg("-d") 212 | .arg(self.gdb_path.clone()) 213 | .args(args) 214 | .args(rr_args.1) 215 | .stdin(Stdio::piped()) 216 | .stdout(Stdio::piped()) 217 | .spawn()? 218 | } else { 219 | Command::new(self.gdb_path.clone()) 220 | .arg("--interpreter=mi") 221 | .args(gdb_args) 222 | .stdin(Stdio::piped()) 223 | .stdout(Stdio::piped()) 224 | .spawn()? 225 | }; 226 | 227 | let stdin = child.stdin.take().expect("take stdin"); 228 | let stdout = child.stdout.take().expect("take stdout"); 229 | let is_running = Arc::new(AtomicBool::new(false)); 230 | let is_running_for_thread = is_running.clone(); 231 | let (result_input, result_output) = mpsc::channel(); 232 | /*let outputThread = */ 233 | thread::Builder::new() 234 | .name("gdbmi parser".to_owned()) 235 | .spawn(move || { 236 | output::process_output(stdout, result_input, oob_sink, is_running_for_thread); 237 | })?; 238 | let gdb = GDB { 239 | process: child, 240 | stdin, 241 | is_running, 242 | result_output, 243 | current_command_token: 0, 244 | binary_path: self.gdb_path, 245 | init_options, 246 | //outputThread: outputThread, 247 | }; 248 | Ok(gdb) 249 | } 250 | } 251 | 252 | impl GDB { 253 | pub fn interrupt_execution(&self) -> Result<(), nix::Error> { 254 | use nix::sys::signal; 255 | use nix::unistd::Pid; 256 | signal::kill(Pid::from_raw(self.process.id() as i32), signal::SIGINT) 257 | } 258 | 259 | pub fn binary_path(&self) -> &Path { 260 | &self.binary_path 261 | } 262 | pub fn init_options(&self) -> &[OsString] { 263 | &self.init_options 264 | } 265 | 266 | pub fn is_running(&self) -> bool { 267 | self.is_running.load(Ordering::SeqCst) 268 | } 269 | pub fn get_usable_token(&mut self) -> Token { 270 | self.current_command_token = self.current_command_token.wrapping_add(1); 271 | self.current_command_token 272 | } 273 | 274 | pub fn execute>( 275 | &mut self, 276 | command: C, 277 | ) -> Result { 278 | if self.is_running() { 279 | return Err(ExecuteError::Busy); 280 | } 281 | let command_token = self.get_usable_token(); 282 | 283 | let mut bytes = Vec::new(); 284 | command 285 | .borrow() 286 | .write_interpreter_string(&mut bytes, command_token) 287 | .expect("write interpreter command"); 288 | 289 | info!("Writing msg {}", String::from_utf8_lossy(&bytes),); 290 | command 291 | .borrow() 292 | .write_interpreter_string(&mut self.stdin, command_token) 293 | .expect("write interpreter command"); 294 | loop { 295 | match self.result_output.recv() { 296 | Ok(record) => match record.token { 297 | Some(token) if token == command_token => return Ok(record), 298 | _ => info!( 299 | "Record does not match expected token ({}) and will be dropped: {:?}", 300 | command_token, record 301 | ), 302 | }, 303 | Err(_) => return Err(ExecuteError::Quit), 304 | } 305 | } 306 | } 307 | 308 | pub fn execute_later>(&mut self, command: C) { 309 | let command_token = self.get_usable_token(); 310 | command 311 | .borrow() 312 | .write_interpreter_string(&mut self.stdin, command_token) 313 | .expect("write interpreter command"); 314 | let _ = self.result_output.recv(); 315 | } 316 | 317 | pub fn is_session_active(&mut self) -> Result { 318 | let res = self.execute(commands::MiCommand::thread_info(None))?; 319 | Ok(!res.results["threads"].is_empty()) 320 | } 321 | } 322 | -------------------------------------------------------------------------------- /src/gdbmi/output.rs: -------------------------------------------------------------------------------- 1 | use super::Token; 2 | pub use json::object::Object; 3 | pub use json::JsonValue; 4 | use nom::{ 5 | alt, call, do_parse, error_position, is_not, many0, map, named, opt, separated_list, tag, value, 6 | }; 7 | 8 | use log::{error, info}; 9 | 10 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 11 | pub enum ResultClass { 12 | Done, 13 | Running, 14 | Connected, 15 | Error, 16 | Exit, 17 | } 18 | 19 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 20 | pub enum BreakPointEvent { 21 | Created, 22 | Deleted, 23 | Modified, 24 | } 25 | 26 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 27 | pub enum ThreadEvent { 28 | Created, 29 | GroupStarted, 30 | Exited, 31 | GroupExited, 32 | Selected, 33 | } 34 | 35 | #[derive(Debug, Clone, PartialEq, Eq)] 36 | pub enum AsyncClass { 37 | Stopped, 38 | CmdParamChanged, 39 | LibraryLoaded, 40 | Thread(ThreadEvent), 41 | BreakPoint(BreakPointEvent), 42 | Other(String), //? 43 | } 44 | 45 | #[derive(Debug)] 46 | pub enum AsyncKind { 47 | Exec, 48 | Status, 49 | Notify, 50 | } 51 | 52 | #[derive(Debug)] 53 | pub enum StreamKind { 54 | Console, 55 | Target, 56 | Log, 57 | } 58 | 59 | #[derive(Debug)] 60 | pub struct ResultRecord { 61 | pub(crate) token: Option, 62 | pub class: ResultClass, 63 | pub results: Object, 64 | } 65 | 66 | #[derive(Debug)] 67 | pub enum OutOfBandRecord { 68 | AsyncRecord { 69 | token: Option, 70 | kind: AsyncKind, 71 | class: AsyncClass, 72 | results: Object, 73 | }, 74 | StreamRecord { 75 | kind: StreamKind, 76 | data: String, 77 | }, 78 | } 79 | 80 | #[derive(Debug)] 81 | enum Output { 82 | Result(ResultRecord), 83 | OutOfBand(OutOfBandRecord), 84 | GDBLine, 85 | SomethingElse(String), /* Debug */ 86 | } 87 | 88 | use crate::OutOfBandRecordSink; 89 | use nom::IResult; 90 | use std::io::{BufRead, BufReader, Read}; 91 | use std::sync::atomic::{AtomicBool, Ordering}; 92 | use std::sync::mpsc::Sender; 93 | use std::sync::Arc; 94 | 95 | pub fn process_output( 96 | output: T, 97 | result_pipe: Sender, 98 | out_of_band_pipe: S, 99 | is_running: Arc, 100 | ) { 101 | let mut reader = BufReader::new(output); 102 | 103 | loop { 104 | let mut buffer = String::new(); 105 | match reader.read_line(&mut buffer) { 106 | Ok(0) => { 107 | return; 108 | } 109 | Ok(_) => { 110 | info!("{}", buffer.trim_end()); 111 | 112 | let parse_result = match Output::parse(&buffer) { 113 | Ok(r) => r, 114 | Err(e) => { 115 | error!("PARSING ERROR: {}", e); 116 | continue; 117 | } 118 | }; 119 | match parse_result { 120 | Output::Result(record) => { 121 | match record.class { 122 | ResultClass::Running => is_running.store(true, Ordering::SeqCst), 123 | //Apparently sometimes gdb first claims to be running, only to then stop again (without notifying the user)... 124 | ResultClass::Error => is_running.store(false, Ordering::SeqCst), 125 | _ => {} 126 | } 127 | result_pipe.send(record).expect("send result to pipe"); 128 | } 129 | Output::OutOfBand(record) => { 130 | if let OutOfBandRecord::AsyncRecord { 131 | class: AsyncClass::Stopped, 132 | .. 133 | } = record 134 | { 135 | is_running.store(false, Ordering::SeqCst); 136 | } 137 | out_of_band_pipe.send(record); 138 | } 139 | Output::GDBLine => {} 140 | //Output::SomethingElse(_) => { /*println!("SOMETHING ELSE: {}", str);*/ } 141 | Output::SomethingElse(text) => { 142 | out_of_band_pipe.send(OutOfBandRecord::StreamRecord { 143 | kind: StreamKind::Target, 144 | data: text, 145 | }); 146 | } 147 | } 148 | } 149 | Err(e) => { 150 | panic!("{}", e); 151 | } 152 | } 153 | } 154 | } 155 | 156 | impl Output { 157 | fn parse(line: &str) -> Result { 158 | match output(line.as_bytes()) { 159 | IResult::Done(_, c) => Ok(c), 160 | IResult::Incomplete(e) => Err(format!("parsing line: incomplete {:?}", e)), //Is it okay to read the next bytes then? 161 | IResult::Error(e) => Err(format!("parse error: {}", e)), 162 | } 163 | } 164 | } 165 | 166 | named!( 167 | result_class, 168 | alt!( 169 | value!(ResultClass::Done, tag!("done")) 170 | | value!(ResultClass::Running, tag!("running")) 171 | | value!(ResultClass::Connected, tag!("connected")) 172 | | value!(ResultClass::Error, tag!("error")) 173 | | value!(ResultClass::Exit, tag!("exit")) 174 | ) 175 | ); 176 | 177 | fn non_quote_byte(input: &[u8]) -> IResult<&[u8], u8> { 178 | let byte = input[0]; 179 | if byte == b'\"' { 180 | IResult::Error(::nom::ErrorKind::Custom(1)) //what are we supposed to return here?? 181 | } else { 182 | IResult::Done(&input[1..], byte) 183 | } 184 | } 185 | 186 | named!( 187 | escaped_character, 188 | alt!( 189 | value!(b'\n', tag!("\\n")) 190 | | value!(b'\r', tag!("\\r")) 191 | | value!(b'\t', tag!("\\t")) 192 | | value!(b'\"', tag!("\\\"")) 193 | | value!(b'\\', tag!("\\\\")) 194 | | non_quote_byte 195 | ) 196 | ); 197 | 198 | named!( 199 | string, 200 | do_parse!( 201 | tag!("\"") 202 | >> s: many0!(escaped_character) 203 | >> tag!("\"") 204 | >> (String::from_utf8_lossy(s.as_slice()).into_owned()) 205 | ) 206 | ); 207 | 208 | fn to_map(v: Vec<(String, JsonValue)>) -> Object { 209 | //TODO: fix this and parse the map directly 210 | let mut obj = Object::new(); 211 | for (name, value) in v { 212 | debug_assert!(obj.get(&name).is_none(), "Duplicate object member!"); 213 | obj.insert(&name, value); 214 | } 215 | obj 216 | } 217 | 218 | fn to_list(v: Vec<(String, JsonValue)>) -> Vec { 219 | //The gdbmi-grammar is really weird... 220 | //TODO: fix this and parse the map directly 221 | v.into_iter().map(|(_, value)| value).collect() 222 | } 223 | 224 | named!( 225 | value, 226 | alt!( 227 | map!(string, JsonValue::String) 228 | | do_parse!( 229 | tag!("{") 230 | >> results: separated_list!(tag!(","), result) 231 | >> tag!("}") 232 | >> (JsonValue::Object(to_map(results))) 233 | ) 234 | | do_parse!( 235 | tag!("[") 236 | >> values: separated_list!(tag!(","), value) 237 | >> tag!("]") 238 | >> (JsonValue::Array(values)) 239 | ) 240 | | do_parse!( 241 | tag!("[") 242 | >> results: separated_list!(tag!(","), result) 243 | >> tag!("]") 244 | >> (JsonValue::Array(to_list(results))) 245 | ) 246 | ) 247 | ); 248 | 249 | // Don't even ask... Against its spec, gdb(mi) sometimes emits multiple values for a single tuple 250 | // in a comma separated list. 251 | named!( 252 | buggy_gdb_list_in_result, 253 | map!(separated_list!(tag!(","), value), |values: Vec< 254 | JsonValue, 255 | >| { 256 | if values.len() == 1 { 257 | values 258 | .into_iter() 259 | .next() 260 | .expect("len == 1 => first element is guaranteed") 261 | } else { 262 | JsonValue::Array(values) 263 | } 264 | }) 265 | ); 266 | 267 | named!( 268 | result<(String, JsonValue)>, 269 | do_parse!( 270 | var: is_not!("={}" /* Do not allow =, {, nor } */) 271 | >> tag!("=") 272 | >> val: buggy_gdb_list_in_result 273 | >> (String::from_utf8_lossy(var).into_owned(), val) 274 | ) 275 | ); 276 | 277 | named!( 278 | token, 279 | map!(::nom::digit, |values: &[u8]| values 280 | .iter() 281 | .fold(0, |acc, &ascii_digit| 10 * acc 282 | + (ascii_digit - b'0') as u64)) 283 | ); 284 | 285 | named!( 286 | result_record, 287 | do_parse!( 288 | t: opt!(token) 289 | >> tag!("^") 290 | >> c: result_class 291 | >> res: many0!(do_parse!(tag!(",") >> r: result >> (r))) 292 | >> (Output::Result(ResultRecord { 293 | token: t, 294 | class: c, 295 | results: to_map(res), 296 | })) 297 | ) 298 | ); 299 | 300 | named!( 301 | async_kind, 302 | alt!( 303 | value!(AsyncKind::Exec, tag!("*")) 304 | | value!(AsyncKind::Status, tag!("+")) 305 | | value!(AsyncKind::Notify, tag!("=")) 306 | ) 307 | ); 308 | 309 | named!( 310 | async_class, 311 | alt!( 312 | value!(AsyncClass::Stopped, tag!("stopped")) 313 | | value!( 314 | AsyncClass::Thread(ThreadEvent::Created), 315 | tag!("thread-created") 316 | ) 317 | | value!( 318 | AsyncClass::Thread(ThreadEvent::GroupStarted), 319 | tag!("thread-group-started") 320 | ) 321 | | value!( 322 | AsyncClass::Thread(ThreadEvent::Exited), 323 | tag!("thread-exited") 324 | ) 325 | | value!( 326 | AsyncClass::Thread(ThreadEvent::GroupExited), 327 | tag!("thread-group-exited") 328 | ) 329 | | value!( 330 | AsyncClass::Thread(ThreadEvent::Selected), 331 | tag!("thread-selected") 332 | ) 333 | | value!(AsyncClass::CmdParamChanged, tag!("cmd-param-changed")) 334 | | value!(AsyncClass::LibraryLoaded, tag!("library-loaded")) 335 | | value!( 336 | AsyncClass::BreakPoint(BreakPointEvent::Created), 337 | tag!("breakpoint-created") 338 | ) 339 | | value!( 340 | AsyncClass::BreakPoint(BreakPointEvent::Deleted), 341 | tag!("breakpoint-deleted") 342 | ) 343 | | value!( 344 | AsyncClass::BreakPoint(BreakPointEvent::Modified), 345 | tag!("breakpoint-modified") 346 | ) 347 | | map!(is_not!(","), |msg| AsyncClass::Other( 348 | String::from_utf8_lossy(msg).into_owned() 349 | )) 350 | ) 351 | ); 352 | 353 | named!( 354 | async_record, 355 | do_parse!( 356 | t: opt!(token) 357 | >> kind: async_kind 358 | >> class: async_class 359 | >> results: many0!(do_parse!(tag!(",") >> r: result >> (r))) 360 | >> (OutOfBandRecord::AsyncRecord { 361 | token: t, 362 | kind, 363 | class, 364 | results: to_map(results), 365 | }) 366 | ) 367 | ); 368 | 369 | named!( 370 | stream_kind, 371 | alt!( 372 | value!(StreamKind::Console, tag!("~")) 373 | | value!(StreamKind::Target, tag!("@")) 374 | | value!(StreamKind::Log, tag!("&")) 375 | ) 376 | ); 377 | 378 | named!( 379 | stream_record, 380 | do_parse!( 381 | kind: stream_kind >> msg: string >> (OutOfBandRecord::StreamRecord { kind, data: msg }) 382 | ) 383 | ); 384 | 385 | named!( 386 | out_of_band_record, 387 | map!(alt!(stream_record | async_record), |record| { 388 | Output::OutOfBand(record) 389 | }) 390 | ); 391 | 392 | named!( 393 | gdb_line, 394 | value!(Output::GDBLine, tag!("(gdb) ")) //TODO proper matching 395 | ); 396 | 397 | fn debug_line(i: &[u8]) -> IResult<&[u8], Output> { 398 | IResult::Done( 399 | i, 400 | Output::SomethingElse(String::from_utf8_lossy(i).into_owned()), 401 | ) 402 | } 403 | 404 | // Ends all records, but can probably ignored 405 | named!(nl, alt!(tag!("\n") | tag!("\r\n"))); 406 | 407 | named!( 408 | output, 409 | do_parse!( 410 | output: alt!(result_record | out_of_band_record | gdb_line | debug_line) >> nl >> (output) 411 | ) 412 | ); 413 | 414 | #[cfg(test)] 415 | mod test { 416 | use super::*; 417 | 418 | #[test] 419 | fn test_output() { 420 | let _ = Output::parse("=library-loaded,ranges=[{}]\n"); 421 | } 422 | } 423 | -------------------------------------------------------------------------------- /src/ipc.rs: -------------------------------------------------------------------------------- 1 | use json::object; 2 | use unix_socket::{UnixListener, UnixStream}; 3 | 4 | use crate::gdb::BreakpointOperationError; 5 | use crate::gdbmi::{ 6 | commands::{BreakPointLocation, MiCommand}, 7 | ExecuteError, 8 | }; 9 | use crate::{Context, Event}; 10 | use std::fs; 11 | use std::io::Read; 12 | use std::io::Write; 13 | use std::path::{Path, PathBuf}; 14 | use std::thread; 15 | 16 | struct IPCError { 17 | reason: &'static str, 18 | details: String, 19 | } 20 | impl IPCError { 21 | fn new>(reason: &'static str, details: S) -> Self { 22 | IPCError { 23 | reason, 24 | details: details.into(), 25 | } 26 | } 27 | fn into_json(self) -> json::JsonValue { 28 | object! { 29 | "type" => "error", 30 | "reason" => self.reason, 31 | "details" => self.details 32 | } 33 | } 34 | } 35 | 36 | #[derive(Debug)] 37 | pub struct IPCRequest { 38 | raw_request: Vec, 39 | response_channel: UnixStream, 40 | } 41 | 42 | impl IPCRequest { 43 | pub fn respond(mut self, p: &mut Context) { 44 | let reply = match Self::handle(p, self.raw_request) { 45 | Ok(reply_success) => reply_success, 46 | Err(reply_fail) => reply_fail.into_json(), 47 | }; 48 | // Client may just close the channel, so we ignore any errors. 49 | // If they mess up it's on them. 50 | let _ = write_ipc_response(&mut self.response_channel, reply.dump().as_bytes()); 51 | } 52 | 53 | fn handle(p: &mut Context, raw_request: Vec) -> Result { 54 | let str_request = std::str::from_utf8(raw_request.as_slice()) 55 | .map_err(|_| IPCError::new("Malformed utf8.", ""))?; 56 | let json_request = 57 | json::parse(str_request).map_err(|_| IPCError::new("Malformed json.", str_request))?; 58 | 59 | let (function_name, parameters) = match json_request { 60 | json::JsonValue::Object(ref obj) => { 61 | let function_name = obj 62 | .get("function") 63 | .and_then(|o| o.as_str()) 64 | .ok_or_else(|| IPCError::new("Missing function name", json_request.dump()))?; 65 | 66 | let parameters = &obj["parameters"]; 67 | 68 | (function_name, parameters) 69 | } 70 | _ => { 71 | return Err(IPCError::new( 72 | "Malformed (non-object) request", 73 | json_request.dump(), 74 | )); 75 | } 76 | }; 77 | let result = Self::dispatch(function_name)?(p, parameters)?; 78 | 79 | Ok(object! { 80 | "type" => "success", 81 | "result" => result 82 | }) 83 | } 84 | 85 | #[allow(clippy::type_complexity)] 86 | fn dispatch( 87 | function_name: &str, 88 | ) -> Result Result, IPCError> 89 | { 90 | match function_name { 91 | "set_breakpoint" => Ok(Self::set_breakpoint), 92 | "show_file" => Ok(Self::show_file), 93 | "get_instance_info" => Ok(Self::get_instance_info), 94 | _ => Err(IPCError::new("unknown function", function_name)), 95 | } 96 | } 97 | 98 | fn set_breakpoint( 99 | p: &mut Context, 100 | parameters: &json::JsonValue, 101 | ) -> Result { 102 | let parameters_obj = if let json::JsonValue::Object(parameters_obj) = parameters { 103 | parameters_obj 104 | } else { 105 | return Err(IPCError::new( 106 | "Parameters is not an object", 107 | parameters.dump(), 108 | )); 109 | }; 110 | let file = parameters_obj 111 | .get("file") 112 | .and_then(|o| o.as_str()) 113 | .ok_or_else(|| IPCError::new("Missing file name", parameters.dump()))?; 114 | let line = parameters_obj 115 | .get("line") 116 | .and_then(|o| o.as_u32()) 117 | .ok_or_else(|| IPCError::new("Missing integer line number", parameters.dump()))?; 118 | match p 119 | .gdb 120 | .insert_breakpoint(BreakPointLocation::Line(Path::new(file), line as usize)) 121 | { 122 | Ok(()) => Ok(json::JsonValue::String(format!( 123 | "Inserted breakpoint at {}:{}", 124 | file, line 125 | ))), 126 | Err(BreakpointOperationError::Busy) => { 127 | //TODO: we may want to investigate if we can interrupt execution, insert 128 | //breakpoint, and resume execution thereafter. 129 | Err(IPCError::new("Could not insert breakpoint", "GDB is busy")) 130 | } 131 | Err(BreakpointOperationError::ExecutionError(msg)) => { 132 | //TODO: we may want to investigate if we can interrupt execution, insert 133 | //breakpoint, and resume execution thereafter. 134 | Err(IPCError::new("Could not insert breakpoint:", msg)) 135 | } 136 | } 137 | } 138 | 139 | fn show_file( 140 | p: &mut Context, 141 | parameters: &json::JsonValue, 142 | ) -> Result { 143 | let parameters_obj = if let json::JsonValue::Object(parameters_obj) = parameters { 144 | parameters_obj 145 | } else { 146 | return Err(IPCError::new( 147 | "Parameters is not an object", 148 | parameters.dump(), 149 | )); 150 | }; 151 | let file = parameters_obj 152 | .get("file") 153 | .and_then(|o| o.as_str()) 154 | .ok_or_else(|| IPCError::new("Missing file name", parameters.dump()))?; 155 | let line = parameters_obj 156 | .get("line") 157 | .and_then(|o| o.as_u32()) 158 | .ok_or_else(|| IPCError::new("Missing integer line number", parameters.dump()))?; 159 | 160 | if line < 1 { 161 | return Err(IPCError::new( 162 | "Invalid integer line number, must be > 0", 163 | parameters.dump(), 164 | )); 165 | } 166 | p.show_file(file.to_owned(), unsegen::base::LineNumber::new(line as _)); 167 | Ok(json::JsonValue::String(format!( 168 | "Showing file {}:{}", 169 | file, line 170 | ))) 171 | } 172 | 173 | fn get_instance_info( 174 | p: &mut Context, 175 | _: &json::JsonValue, 176 | ) -> Result { 177 | let result = p 178 | .gdb 179 | .mi 180 | .execute(MiCommand::environment_pwd()) 181 | .map_err(|e| match e { 182 | ExecuteError::Busy => { 183 | //TODO: we may want to investigate if we can interrupt execution, get information 184 | //and resume execution thereafter. 185 | IPCError::new("Could not get working directory", "GDB is busy") 186 | } 187 | ExecuteError::Quit => IPCError::new("Could not get working directory", "GDB quit"), 188 | })?; 189 | let working_directory = result.results["cwd"].as_str().ok_or_else(|| { 190 | IPCError::new("Could not get working directory", "Malformed GDB response") 191 | })?; 192 | Ok(object! { 193 | "working_directory" => working_directory 194 | }) 195 | } 196 | } 197 | 198 | const FALLBACK_RUNTIME_DIR: &str = "/tmp/"; 199 | const RUNTIME_SUBDIR: &str = "ugdb"; 200 | const SOCKET_IDENTIFIER_LENGTH: usize = 64; 201 | const IPC_MSG_IDENTIFIER: &[u8] = b"ugdb-ipc"; 202 | const HEADER_LENGTH: usize = 12; 203 | 204 | #[allow(clippy::upper_case_acronyms)] 205 | pub struct IPC { 206 | socket_path: PathBuf, 207 | } 208 | 209 | fn write_ipc_header(w: &mut W, msg_len: u32) -> std::io::Result<()> { 210 | let msg_len = msg_len.to_le(); 211 | let msg_len_buf = [ 212 | msg_len as u8, 213 | (msg_len >> 8) as u8, 214 | (msg_len >> 16) as u8, 215 | (msg_len >> 24) as u8, 216 | ]; 217 | w.write_all(IPC_MSG_IDENTIFIER)?; 218 | w.write_all(&msg_len_buf)?; 219 | Ok(()) 220 | } 221 | 222 | fn write_ipc_response(w: &mut W, msg: &[u8]) -> std::io::Result<()> { 223 | write_ipc_header(w, msg.len() as u32)?; 224 | w.write_all(msg)?; 225 | Ok(()) 226 | } 227 | 228 | fn try_read_ipc_header(r: &mut R) -> Result { 229 | // The header has to look like this with four bytes (****) for the message length in little endian: "ugdb-ipc****" 230 | let mut buf = vec![0u8; HEADER_LENGTH]; 231 | r.read_exact(&mut buf).map_err(|_| {})?; 232 | if &buf[0..8] == IPC_MSG_IDENTIFIER { 233 | let mut len = 0; 234 | len += buf[8] as u32; 235 | len += (buf[9] as u32) << 8; 236 | len += (buf[10] as u32) << 16; 237 | len += (buf[11] as u32) << 24; 238 | Ok(u32::from_le(len)) 239 | } else { 240 | Err(()) 241 | } 242 | } 243 | 244 | fn try_read_ipc_request(connection: &mut UnixStream) -> Result { 245 | let msg_len = try_read_ipc_header(connection)?; 246 | 247 | let mut msg_buf = vec![0u8; msg_len as usize]; 248 | connection.read_exact(&mut msg_buf).map_err(|_| {})?; 249 | Ok(IPCRequest { 250 | raw_request: msg_buf, 251 | response_channel: connection.try_clone().expect("clone handle"), 252 | }) 253 | } 254 | 255 | fn start_connection(mut connection: UnixStream, request_sink: std::sync::mpsc::Sender) { 256 | let _ = thread::Builder::new() 257 | .name("IPC Connection".to_owned()) 258 | .spawn(move || { 259 | connection.set_nonblocking(false).expect("set blocking"); 260 | 261 | // Try to respond to requests as long as they are well formed. If the other side is 262 | // (for example) specifying an incorrect message length, we just terminate the 263 | // connection. 264 | while let Ok(request) = try_read_ipc_request(&mut connection) { 265 | request_sink.send(Event::Ipc(request)).unwrap(); 266 | } 267 | }); 268 | } 269 | 270 | impl IPC { 271 | pub fn setup(request_sink: std::sync::mpsc::Sender) -> std::io::Result { 272 | let runtime_dir = 273 | std::env::var_os("XDG_RUNTIME_DIR").unwrap_or_else(|| FALLBACK_RUNTIME_DIR.into()); 274 | let ugdb_dir = Path::join(runtime_dir.as_ref(), RUNTIME_SUBDIR); 275 | let _ = fs::create_dir(&ugdb_dir); //Ignore error if dir exists, we check if we can access it soon. 276 | 277 | use rand::Rng; 278 | let socket_name = rand::thread_rng() 279 | .gen_ascii_chars() 280 | .take(SOCKET_IDENTIFIER_LENGTH) 281 | .collect::(); 282 | let socket_path = ugdb_dir.join(socket_name); 283 | 284 | let listener = UnixListener::bind(&socket_path)?; 285 | 286 | let _ = thread::Builder::new() 287 | .name("IPC Connection Listener".to_owned()) 288 | .spawn(move || { 289 | for connection in listener.incoming().flatten() { 290 | start_connection(connection, request_sink.clone()); 291 | } 292 | }); 293 | 294 | Ok(IPC { socket_path }) 295 | } 296 | } 297 | 298 | impl Drop for IPC { 299 | fn drop(&mut self) { 300 | // We at least try to remove the socket. If it fails we cannot really do about it here. 301 | let _ = fs::remove_file(&self.socket_path); 302 | } 303 | } 304 | -------------------------------------------------------------------------------- /src/layout.rs: -------------------------------------------------------------------------------- 1 | use crate::tui::{Tui, TuiContainerType}; 2 | use std::str::CharIndices; 3 | use unsegen::container::{HSplit, Layout, Leaf, VSplit}; 4 | 5 | #[derive(Debug, PartialEq)] 6 | pub enum LayoutParseErrorKind { 7 | TooShortExpected(&'static [char]), 8 | ExpectedGotMany(usize, &'static [char], char), 9 | SplitTypeChangeFromTo(usize, char, char), 10 | NoConsole, 11 | } 12 | 13 | #[derive(Debug, PartialEq)] 14 | pub struct LayoutParseError { 15 | layout: String, 16 | kind: LayoutParseErrorKind, 17 | } 18 | 19 | impl std::fmt::Display for LayoutParseError { 20 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 21 | writeln!(f, "Failed to parse layout string: ")?; 22 | 23 | let format_expected = |expected: &'static [char]| match expected { 24 | &[l] => format!("'{}'", l), 25 | o => format!("One of {:?}", o), 26 | }; 27 | let layout = &self.layout; 28 | match self.kind { 29 | LayoutParseErrorKind::ExpectedGotMany(at, expected, got) => { 30 | writeln!( 31 | f, 32 | "Expected {}, but got {}!\n{}☛{} ", 33 | format_expected(expected), 34 | got, 35 | &layout[..at], 36 | &layout[at..] 37 | ) 38 | } 39 | LayoutParseErrorKind::TooShortExpected(expected) => { 40 | writeln!( 41 | f, 42 | "Too short! Expected at least {}.\n{}☚", 43 | format_expected(expected), 44 | &layout 45 | ) 46 | } 47 | LayoutParseErrorKind::SplitTypeChangeFromTo(at, from, to) => { 48 | writeln!(f, "Split type cannot change from '{}' to '{}' within a node. Try to use brackets.\n{}☛{}", from, to, &layout[..at], &layout[at..]) 49 | } 50 | LayoutParseErrorKind::NoConsole => { 51 | writeln!( 52 | f, 53 | "Layout MUST contain gdb console. Insert 'c' somewhere in the layout." 54 | ) 55 | } 56 | } 57 | } 58 | } 59 | 60 | #[derive(Copy, Clone)] 61 | enum SplitType { 62 | H, 63 | V, 64 | None, 65 | } 66 | struct Input<'a>(std::iter::Peekable>); 67 | 68 | const NODE_START_CHARS: &[char] = &['c', 't', 's', 'e', '(']; 69 | const CLOSING_BRACKET_CHARS: &[char] = &[')']; 70 | 71 | impl<'a> Input<'a> { 72 | fn new(s: &'a str) -> Result { 73 | let mut ret = Self(s.char_indices().peekable()); 74 | let _ = ret 75 | .0 76 | .peek() 77 | .ok_or(LayoutParseErrorKind::TooShortExpected(NODE_START_CHARS))?; 78 | Ok(ret) 79 | } 80 | fn current(&mut self) -> Option { 81 | self.0.peek().map(|v| v.1) 82 | } 83 | fn current_index(&mut self) -> usize { 84 | self.0.peek().unwrap().0 85 | } 86 | fn advance(&mut self) { 87 | self.0.next(); 88 | } 89 | } 90 | 91 | fn try_parse_weight(i: &mut Input) -> f64 { 92 | if !i.current().map(|v| v.is_digit(10)).unwrap_or(false) { 93 | return 1.0; 94 | } 95 | let mut w = 0; 96 | loop { 97 | if let Some(i) = i.current() { 98 | w = match i.to_digit(10) { 99 | Some(d) => w * 10 + d, 100 | None => return w as _, 101 | }; 102 | } else { 103 | return w as _; 104 | } 105 | i.advance(); 106 | } 107 | } 108 | fn try_parse_leaf<'a, 'b>(i: &mut Input<'a>) -> Option> + 'b>> { 109 | let ret = match i.current()? { 110 | 'c' => Box::new(Leaf::new(TuiContainerType::Console)), 111 | 't' => Box::new(Leaf::new(TuiContainerType::Terminal)), 112 | 's' => Box::new(Leaf::new(TuiContainerType::SrcView)), 113 | 'e' => Box::new(Leaf::new(TuiContainerType::ExpressionTable)), 114 | _ => return None, 115 | }; 116 | i.advance(); 117 | Some(ret) 118 | } 119 | 120 | fn parse_node<'a, 'b>( 121 | i: &mut Input<'a>, 122 | ) -> Result> + 'b>, LayoutParseErrorKind> { 123 | let mut nodes = Vec::new(); 124 | let mut split_type = SplitType::None; 125 | loop { 126 | let weight = try_parse_weight(i); 127 | if let Some(l) = try_parse_leaf(i) { 128 | nodes.push((l, weight)); 129 | } else { 130 | match i.current() { 131 | Some('(') => { 132 | i.advance(); 133 | nodes.push((parse_node(i)?, weight)); 134 | match i.current() { 135 | Some(')') => { 136 | i.advance(); 137 | } 138 | Some(o) => { 139 | return Err(LayoutParseErrorKind::ExpectedGotMany( 140 | i.current_index(), 141 | CLOSING_BRACKET_CHARS, 142 | o, 143 | )); 144 | } 145 | None => { 146 | return Err(LayoutParseErrorKind::TooShortExpected( 147 | CLOSING_BRACKET_CHARS, 148 | )) 149 | } 150 | } 151 | } 152 | Some(o) => { 153 | return Err(LayoutParseErrorKind::ExpectedGotMany( 154 | i.current_index(), 155 | NODE_START_CHARS, 156 | o, 157 | )); 158 | } 159 | None => { 160 | return Err(LayoutParseErrorKind::TooShortExpected(NODE_START_CHARS)); 161 | } 162 | } 163 | } 164 | 165 | let c = if let Some(c) = i.current() { 166 | c 167 | } else { 168 | break; 169 | }; 170 | split_type = match (split_type, c) { 171 | (SplitType::None, '|') => SplitType::H, 172 | (SplitType::H, '|') => SplitType::H, 173 | (SplitType::V, '|') => { 174 | return Err(LayoutParseErrorKind::SplitTypeChangeFromTo( 175 | i.current_index(), 176 | '-', 177 | '|', 178 | )) 179 | } 180 | (SplitType::None, '-') => SplitType::V, 181 | (SplitType::V, '-') => SplitType::V, 182 | (SplitType::H, '-') => { 183 | return Err(LayoutParseErrorKind::SplitTypeChangeFromTo( 184 | i.current_index(), 185 | '|', 186 | '-', 187 | )) 188 | } 189 | (_, _) => break, 190 | }; 191 | i.advance(); 192 | } 193 | Ok(match split_type { 194 | SplitType::H => Box::new(HSplit::new(nodes)), 195 | SplitType::V => Box::new(VSplit::new(nodes)), 196 | SplitType::None => { 197 | assert!(nodes.len() == 1); 198 | nodes.pop().unwrap().0 199 | } 200 | }) 201 | } 202 | 203 | pub fn parse<'a>(s: String) -> Result> + 'a>, LayoutParseError> { 204 | if !s.contains('c') { 205 | return Err(LayoutParseError { 206 | kind: LayoutParseErrorKind::NoConsole, 207 | layout: s, 208 | }); 209 | } 210 | let mut i = Input::new(&s).map_err(|kind| LayoutParseError { 211 | kind, 212 | layout: s.to_owned(), 213 | })?; 214 | parse_node(&mut i).map_err(|kind| LayoutParseError { 215 | kind, 216 | layout: s.to_owned(), 217 | }) 218 | } 219 | 220 | #[cfg(test)] 221 | mod test { 222 | use super::*; 223 | 224 | fn stringify(l: &dyn Layout>) -> String { 225 | format!("{:?}", l) 226 | } 227 | #[track_caller] 228 | fn expect_equal(input: &str, expected: &str) { 229 | let parsed = parse(input.to_owned()).unwrap(); 230 | assert_eq!(&stringify(&*parsed), expected); 231 | } 232 | #[track_caller] 233 | fn expect_error(input: &str, e: LayoutParseErrorKind) { 234 | let pe = parse(input.to_owned()).unwrap_err(); 235 | assert_eq!(pe.kind, e); 236 | } 237 | #[test] 238 | fn parse_default() { 239 | expect_equal( 240 | "(1s-1c)|(1e-1t)", 241 | "(1(1SrcView-1Console)|1(1ExpressionTable-1Terminal))", 242 | ); 243 | } 244 | #[test] 245 | fn parse_triple_weighted() { 246 | expect_equal( 247 | "(s|2t|c)-99e", 248 | "(1(1SrcView|2Terminal|1Console)-99ExpressionTable)", 249 | ); 250 | } 251 | #[test] 252 | fn parse_empty() { 253 | expect_error("", LayoutParseErrorKind::NoConsole); 254 | } 255 | #[test] 256 | fn parse_unclosed() { 257 | expect_error( 258 | "(c-e", 259 | LayoutParseErrorKind::TooShortExpected(CLOSING_BRACKET_CHARS), 260 | ); 261 | } 262 | #[test] 263 | fn parse_unexpected() { 264 | expect_error( 265 | "fc", 266 | LayoutParseErrorKind::ExpectedGotMany(0, NODE_START_CHARS, 'f'), 267 | ); 268 | } 269 | #[test] 270 | fn parse_change_split() { 271 | expect_error( 272 | "c-e|t", 273 | LayoutParseErrorKind::SplitTypeChangeFromTo(3, '-', '|'), 274 | ); 275 | } 276 | } 277 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | mod completion; 2 | mod gdb; 3 | mod gdb_expression_parsing; 4 | mod gdbmi; 5 | mod ipc; 6 | mod layout; 7 | mod tui; 8 | 9 | use ipc::IPCRequest; 10 | use std::ffi::OsString; 11 | use std::time::Duration; 12 | 13 | use std::sync::mpsc::Sender; 14 | 15 | use gdb::GDB; 16 | use gdbmi::output::OutOfBandRecord; 17 | use gdbmi::{GDBBuilder, OutOfBandRecordSink}; 18 | use log::{debug, warn}; 19 | use nix::sys::signal::Signal; 20 | use nix::sys::termios; 21 | use std::path::PathBuf; 22 | use structopt::StructOpt; 23 | use tui::{Tui, TuiContainerType}; 24 | use unsegen::base::{Color, StyleModifier, Terminal}; 25 | use unsegen::container::ContainerManager; 26 | use unsegen::input::{Input, Key, NavigateBehavior, ToEvent}; 27 | use unsegen::widget::{Blink, RenderingHints}; 28 | 29 | const EVENT_BUFFER_DURATION_MS: u64 = 10; 30 | const FOCUS_ESCAPE_MAX_DURATION_MS: u64 = 200; 31 | const CURSOR_BLINK_PERIOD_MS: u64 = 500; 32 | const CURSOR_BLINK_TIMES: u8 = 20; 33 | 34 | #[derive(StructOpt)] 35 | #[structopt()] 36 | struct Options { 37 | #[structopt( 38 | long = "gdb", 39 | help = "Path to alternative gdb binary.", 40 | default_value = "gdb", 41 | parse(from_os_str) 42 | )] 43 | gdb_path: PathBuf, 44 | #[structopt(long = "nh", help = "Do not execute commands from ~/.gdbinit.")] 45 | nh: bool, 46 | #[structopt( 47 | short = "n", 48 | long = "nx", 49 | help = "Do not execute commands from any .gdbinit initialization files." 50 | )] 51 | nx: bool, 52 | #[structopt( 53 | short = "q", 54 | long = "quiet", 55 | help = "\"Quiet\". Do not print the introductory and copyright messages. These messages are also suppressed in batch mode." 56 | )] 57 | quiet: bool, 58 | #[structopt( 59 | long = "rr", 60 | help = "Start ugdb as an interface for rr. Trailing ugdb arguments will be passed to rr replay instead." 61 | )] 62 | rr: bool, 63 | #[structopt( 64 | long = "rr-path", 65 | help = "Path to alternative rr binary.", 66 | default_value = "rr", 67 | parse(from_os_str) 68 | )] 69 | rr_path: PathBuf, 70 | #[structopt( 71 | long = "cd", 72 | help = "Run GDB using directory as its working directory, instead of the current directory.", 73 | parse(from_os_str) 74 | )] 75 | cd: Option, 76 | #[structopt( 77 | short = "b", 78 | help = "Set the line speed (baud rate or bits per second) of any serial interface used by GDB for remote debugging." 79 | )] 80 | bps: Option, 81 | #[structopt( 82 | short = "s", 83 | long = "symbols", 84 | help = "Read symbols from the given file.", 85 | parse(from_os_str) 86 | )] 87 | symbol_file: Option, 88 | #[structopt( 89 | short = "c", 90 | long = "core", 91 | help = "Use file file as a core dump to examine.", 92 | parse(from_os_str) 93 | )] 94 | core_file: Option, 95 | #[structopt(short = "p", long = "pid", help = "Attach to process with given id.")] 96 | proc_id: Option, 97 | #[structopt( 98 | short = "x", 99 | long = "command", 100 | help = "Execute GDB commands from file.", 101 | parse(from_os_str) 102 | )] 103 | command_file: Option, 104 | #[structopt( 105 | short = "d", 106 | long = "directory", 107 | help = "Add directory to the path to search for source files.", 108 | parse(from_os_str) 109 | )] 110 | source_dir: Option, 111 | #[structopt( 112 | long = "log_dir", 113 | help = "Directory in which the log file will be stored.", 114 | parse(from_os_str), 115 | default_value = "/tmp" 116 | )] 117 | log_dir: PathBuf, 118 | #[structopt( 119 | short = "e", 120 | long = "initial-expression", 121 | help = "Define initial entries for the expression table." 122 | )] 123 | initial_expression_table_entries: Vec, 124 | #[structopt( 125 | long = "layout", 126 | help = "Define the initial tui layout via a format string.", 127 | default_value = "(1s-1c)|(1e-1t)" 128 | )] 129 | layout: String, 130 | #[structopt( 131 | help = "Path to program to debug (with arguments).", 132 | parse(from_os_str) 133 | )] 134 | program: Vec, 135 | // Not sure how to mimic gdbs cmdline behavior for the positional arguments... 136 | //#[structopt(help="Attach to process with given id.")] 137 | //proc_id: Option, 138 | //#[structopt(help="Use file file as a core dump to examine.", parse(from_os_str))] 139 | //core_file: Option, 140 | } 141 | 142 | impl Options { 143 | fn create_gdb_builder(self) -> GDBBuilder { 144 | let mut gdb_builder = GDBBuilder::new(self.gdb_path); 145 | if self.nh { 146 | gdb_builder = gdb_builder.nh(); 147 | } 148 | if self.nx { 149 | gdb_builder = gdb_builder.nx(); 150 | } 151 | if self.quiet { 152 | gdb_builder = gdb_builder.quiet(); 153 | } 154 | if let Some(cd) = self.cd { 155 | gdb_builder = gdb_builder.working_dir(cd); 156 | } 157 | if let Some(bps) = self.bps { 158 | gdb_builder = gdb_builder.bps(bps); 159 | } 160 | if let Some(symbol_file) = self.symbol_file { 161 | gdb_builder = gdb_builder.symbol_file(symbol_file); 162 | } 163 | if let Some(core_file) = self.core_file { 164 | gdb_builder = gdb_builder.core_file(core_file); 165 | } 166 | if let Some(proc_id) = self.proc_id { 167 | gdb_builder = gdb_builder.proc_id(proc_id); 168 | } 169 | if let Some(command_file) = self.command_file { 170 | gdb_builder = gdb_builder.command_file(command_file); 171 | } 172 | if let Some(src_dir) = self.source_dir { 173 | gdb_builder = gdb_builder.source_dir(src_dir); 174 | } 175 | if self.rr { 176 | gdb_builder = gdb_builder.rr_args(self.rr_path, self.program); 177 | } else { 178 | let (program, args) = self 179 | .program 180 | .split_first() 181 | .map(|(p, a)| (Some(p), a)) 182 | .unwrap_or_else(|| (None, &[])); 183 | gdb_builder = gdb_builder.args(args); 184 | if let Some(program) = program { 185 | gdb_builder = gdb_builder.program(PathBuf::from(program)); 186 | } 187 | } 188 | 189 | gdb_builder 190 | } 191 | } 192 | 193 | struct MpscOobRecordSink(Sender); 194 | 195 | impl OutOfBandRecordSink for MpscOobRecordSink { 196 | fn send(&self, data: OutOfBandRecord) { 197 | self.0.send(Event::OutOfBandRecord(data)).unwrap(); 198 | } 199 | } 200 | 201 | impl Drop for MpscOobRecordSink { 202 | fn drop(&mut self) { 203 | self.0.send(Event::GdbShutdown).unwrap(); 204 | } 205 | } 206 | 207 | struct MpscSlaveInputSink(Sender); 208 | 209 | impl unsegen_terminal::SlaveInputSink for MpscSlaveInputSink { 210 | fn receive_bytes_from_pty(&mut self, data: Box<[u8]>) { 211 | self.0.send(Event::Pty(data)).unwrap(); 212 | } 213 | } 214 | 215 | pub struct MessageSink { 216 | messages: Vec, 217 | } 218 | 219 | impl MessageSink { 220 | pub fn send>(&mut self, msg: S) { 221 | self.messages.push(msg.into()); 222 | } 223 | pub fn drain_messages(&mut self) -> Vec { 224 | let mut alt_buffer = Vec::new(); 225 | std::mem::swap(&mut self.messages, &mut alt_buffer); 226 | alt_buffer 227 | } 228 | } 229 | 230 | pub struct Context { 231 | pub gdb: GDB, 232 | event_sink: Sender, 233 | } 234 | 235 | impl Context { 236 | fn log(&mut self, msg: impl AsRef) { 237 | self.event_sink 238 | .send(Event::Log(format!("{}\n", msg.as_ref()))) 239 | .unwrap(); 240 | } 241 | 242 | fn try_change_layout(&mut self, layout_str: String) { 243 | self.event_sink 244 | .send(Event::ChangeLayout(layout_str)) 245 | .unwrap(); 246 | } 247 | 248 | fn show_file(&mut self, file: String, line: unsegen::base::LineNumber) { 249 | self.event_sink.send(Event::ShowFile(file, line)).unwrap(); 250 | } 251 | } 252 | 253 | // A timer that can be used to receive an event at any time, 254 | // but will never send until started via try_start_ms. 255 | struct MpscTimer { 256 | next_sender: Option>, 257 | sender: Sender, 258 | evt_fn: Box Event>, 259 | counter: std::sync::Arc, 260 | } 261 | 262 | impl MpscTimer { 263 | fn new(sender: Sender, evt_fn: Box Event>) -> Self { 264 | MpscTimer { 265 | next_sender: Some(sender.clone()), 266 | sender, 267 | evt_fn, 268 | counter: std::sync::Arc::new(std::sync::atomic::AtomicU64::new(0)), 269 | } 270 | } 271 | 272 | // Try to start the timer if it has not been started already. 273 | fn try_start(&mut self, duration: Duration) { 274 | if let Some(sender) = self.next_sender.take() { 275 | let start_number = self.counter.load(std::sync::atomic::Ordering::SeqCst); 276 | let counter = self.counter.clone(); 277 | let evt = (self.evt_fn)(); 278 | let _ = std::thread::spawn(move || { 279 | std::thread::sleep(duration); 280 | let current = counter.load(std::sync::atomic::Ordering::SeqCst); 281 | if current == start_number { 282 | sender.send(evt).unwrap(); 283 | } 284 | }); 285 | } 286 | } 287 | 288 | fn has_been_started(&self) -> bool { 289 | self.next_sender.is_none() 290 | } 291 | 292 | fn reset(&mut self) { 293 | self.next_sender = Some(self.sender.clone()); 294 | self.counter 295 | .fetch_add(1, std::sync::atomic::Ordering::SeqCst); 296 | } 297 | } 298 | 299 | impl Drop for MpscTimer { 300 | fn drop(&mut self) { 301 | self.counter 302 | .fetch_add(1, std::sync::atomic::Ordering::SeqCst); 303 | } 304 | } 305 | 306 | #[derive(Clone, Copy, Debug)] 307 | enum InputMode { 308 | Normal, 309 | Focused, 310 | ContainerSelect, 311 | } 312 | 313 | impl InputMode { 314 | fn associated_border_style(self) -> StyleModifier { 315 | match self { 316 | InputMode::Normal => StyleModifier::new(), 317 | InputMode::Focused => StyleModifier::new().fg_color(Color::Red), 318 | InputMode::ContainerSelect => StyleModifier::new().fg_color(Color::LightYellow), 319 | } 320 | } 321 | } 322 | 323 | #[derive(Debug)] 324 | pub enum Event { 325 | Signal(nix::sys::signal::Signal), 326 | Input(Input), 327 | Pty(Box<[u8]>), 328 | CursorTimer, 329 | RenderTimer, 330 | FocusEscTimer, 331 | OutOfBandRecord(OutOfBandRecord), 332 | Log(String), 333 | ChangeLayout(String), 334 | ShowFile(String, unsegen::base::LineNumber), 335 | GdbShutdown, 336 | Ipc(IPCRequest), 337 | } 338 | 339 | fn run() -> i32 { 340 | // Setup signal piping: 341 | let mut signals_to_wait = nix::sys::signal::SigSet::empty(); 342 | signals_to_wait.add(Signal::SIGWINCH); 343 | signals_to_wait.add(Signal::SIGTSTP); 344 | signals_to_wait.add(Signal::SIGTERM); 345 | let mut signals_to_block = signals_to_wait; 346 | signals_to_block.add(Signal::SIGCONT); 347 | 348 | // We block the signals for the current (and so far only thread). This mask will be inherited 349 | // by all other threads spawned subsequently, so that we retrieve signals using sigwait. 350 | signals_to_block.thread_block().unwrap(); 351 | 352 | let (event_sink, event_source) = std::sync::mpsc::channel(); 353 | 354 | let signal_sink = event_sink.clone(); 355 | std::thread::spawn(move || loop { 356 | if let Ok(signal) = signals_to_wait.wait() { 357 | signal_sink.send(Event::Signal(signal)).unwrap(); 358 | } 359 | }); 360 | 361 | // Set up a panic hook that ALWAYS displays panic information (including stack) to the main 362 | // terminal screen. 363 | const STDOUT: std::os::unix::io::RawFd = 0; 364 | let orig_attr = std::sync::Mutex::new( 365 | termios::tcgetattr(STDOUT).expect("Failed to get terminal attributes"), 366 | ); 367 | 368 | let options = Options::from_args(); 369 | let log_dir = options.log_dir.to_owned(); 370 | let initial_expression_table_entries = options.initial_expression_table_entries.clone(); 371 | let layout = options.layout.clone(); 372 | 373 | std::panic::set_hook(Box::new(move |info| { 374 | // Switch back to main screen 375 | println!("{}{}", termion::screen::ToMainScreen, termion::cursor::Show); 376 | // Restore old terminal behavior (will be restored later automatically, but we want to be 377 | // able to properly print the panic info) 378 | let _ = termios::tcsetattr(STDOUT, termios::SetArg::TCSANOW, &orig_attr.lock().unwrap()); 379 | 380 | println!("Oh no! ugdb crashed!"); 381 | println!( 382 | "Consider filing an issue including the log file located in {} and the following backtrace at {}:\n", 383 | log_dir.to_string_lossy(), 384 | env!("CARGO_PKG_REPOSITORY"), 385 | ); 386 | 387 | println!("{}", info); 388 | println!("{:?}", backtrace::Backtrace::new()); 389 | })); 390 | 391 | if let Err(e) = flexi_logger::Logger::with_env_or_str("info") 392 | .log_to_file() 393 | .directory(options.log_dir.to_owned()) 394 | .start() 395 | { 396 | eprintln!("Unable to initialize Logger: {}", e); 397 | return 0xfe; 398 | } 399 | 400 | // Create terminal and setup slave input piping 401 | let tui_terminal = unsegen_terminal::Terminal::new(MpscSlaveInputSink(event_sink.clone())) 402 | .expect("Create PTY"); 403 | 404 | // Setup ipc 405 | let _ipc = ipc::IPC::setup(event_sink.clone()).expect("Setup ipc"); 406 | 407 | // Start gdb and setup output event piping 408 | let gdb_path = options.gdb_path.to_string_lossy().to_string(); 409 | let mut gdb_builder = options.create_gdb_builder(); 410 | gdb_builder = gdb_builder.tty(tui_terminal.slave_name().into()); 411 | let gdb = GDB::new( 412 | match gdb_builder.try_spawn(MpscOobRecordSink(event_sink.clone())) { 413 | Ok(gdb) => gdb, 414 | Err(e) => { 415 | eprintln!("Failed to spawn gdb process (\"{}\"): {}", gdb_path, e); 416 | return 0xfc; 417 | } 418 | }, 419 | ); 420 | 421 | let stdout = std::io::stdout(); 422 | 423 | let theme_set = unsegen_pager::ThemeSet::load_defaults(); 424 | 425 | let layout = match layout::parse(layout) { 426 | Ok(l) => l, 427 | Err(e) => { 428 | eprintln!("{}", e); 429 | return 0xfb; 430 | } 431 | }; 432 | 433 | let mut context = Context { 434 | gdb, 435 | event_sink: event_sink.clone(), 436 | }; 437 | 438 | { 439 | let mut terminal = match Terminal::new(stdout.lock()) { 440 | Ok(t) => t, 441 | Err(e) => { 442 | eprintln!("Unable to setup Terminal: {}", e); 443 | return 0xfd; 444 | } 445 | }; 446 | let mut tui = Tui::new(tui_terminal, &theme_set.themes["base16-ocean.dark"]); 447 | for entry in initial_expression_table_entries { 448 | tui.expression_table.add_entry(entry); 449 | } 450 | 451 | // Start stdin thread _after_ building terminal (and setting the actual terminal to raw 452 | // mode to avoid race condition where the first 'set of input' is buffered 453 | /* let keyboard_input = */ 454 | let keyboard_sink = event_sink.clone(); 455 | std::thread::spawn(move || { 456 | let stdin = std::io::stdin(); 457 | let stdin = stdin.lock(); 458 | for e in Input::read_all(stdin) { 459 | keyboard_sink.send(Event::Input(e.expect("event"))).unwrap(); 460 | } 461 | }); 462 | 463 | let mut app = ContainerManager::::from_layout(layout); 464 | let mut input_mode = InputMode::Normal; 465 | let mut focus_esc_timer = 466 | MpscTimer::new(event_sink.clone(), Box::new(|| Event::FocusEscTimer)); 467 | let mut cursor_status = Blink::On; 468 | let mut cursor_blinks_since_last_input = 0; 469 | 470 | 'runloop: loop { 471 | let mut cursor_update_timer = 472 | MpscTimer::new(event_sink.clone(), Box::new(|| Event::CursorTimer)); 473 | if cursor_blinks_since_last_input < CURSOR_BLINK_TIMES { 474 | cursor_update_timer.try_start(Duration::from_millis(CURSOR_BLINK_PERIOD_MS)); 475 | } 476 | 477 | let mut render_delay_timer = 478 | MpscTimer::new(event_sink.clone(), Box::new(|| Event::RenderTimer)); 479 | let mut esc_timer_needs_reset = false; 480 | 'displayloop: loop { 481 | let mut esc_in_focused_context_pressed = false; 482 | match event_source.recv().unwrap() { 483 | Event::CursorTimer => { 484 | cursor_status.toggle(); 485 | cursor_blinks_since_last_input += 1; 486 | break 'displayloop; 487 | } 488 | Event::RenderTimer => { 489 | cursor_status = Blink::On; 490 | cursor_blinks_since_last_input = 0; 491 | break 'displayloop; 492 | } 493 | Event::FocusEscTimer => { 494 | Input { 495 | event: Key::Esc.to_event(), 496 | raw: vec![0x1bu8], 497 | } 498 | .chain(app.active_container_behavior(&mut tui, &mut context)); 499 | esc_timer_needs_reset = true; 500 | break 'displayloop; 501 | } 502 | Event::Input(input) => { 503 | let sig_behavior = unsegen_signals::SignalBehavior::new() 504 | .on_default::(); 505 | let input = input.chain(sig_behavior); 506 | match input_mode { 507 | InputMode::ContainerSelect => input 508 | .chain( 509 | NavigateBehavior::new(&mut app.navigatable(&mut tui)) 510 | .up_on(Key::Char('k')) 511 | .up_on(Key::Up) 512 | .down_on(Key::Char('j')) 513 | .down_on(Key::Down) 514 | .left_on(Key::Char('h')) 515 | .left_on(Key::Left) 516 | .right_on(Key::Char('l')) 517 | .right_on(Key::Right), 518 | ) 519 | .chain((Key::Char('i'), || { 520 | input_mode = InputMode::Normal; 521 | app.set_active(TuiContainerType::Console); 522 | })) 523 | .chain((Key::Char('e'), || { 524 | input_mode = InputMode::Normal; 525 | app.set_active(TuiContainerType::ExpressionTable); 526 | })) 527 | .chain((Key::Char('s'), || { 528 | input_mode = InputMode::Normal; 529 | app.set_active(TuiContainerType::SrcView); 530 | })) 531 | .chain((Key::Char('t'), || { 532 | input_mode = InputMode::Normal; 533 | app.set_active(TuiContainerType::Terminal); 534 | })) 535 | .chain((Key::Char('T'), || { 536 | input_mode = InputMode::Focused; 537 | app.set_active(TuiContainerType::Terminal); 538 | })) 539 | .chain((Key::Char('\n'), || input_mode = InputMode::Normal)), 540 | InputMode::Normal => input 541 | .chain((Key::Esc, || input_mode = InputMode::ContainerSelect)) 542 | .chain(app.active_container_behavior(&mut tui, &mut context)), 543 | InputMode::Focused => input 544 | .chain((Key::Esc, || esc_in_focused_context_pressed = true)) 545 | .chain(app.active_container_behavior(&mut tui, &mut context)), 546 | } 547 | .finish(); 548 | } 549 | Event::OutOfBandRecord(record) => { 550 | tui.add_out_of_band_record(record, &mut context); 551 | } 552 | Event::Log(msg) => { 553 | tui.console.write_to_gdb_log(msg); 554 | } 555 | Event::ShowFile(file, line) => { 556 | tui.src_view.show_file(file, line, &mut context); 557 | } 558 | Event::ChangeLayout(layout) => { 559 | match layout::parse(layout) { 560 | Ok(layout) => { 561 | app.set_layout(layout); 562 | } 563 | Err(e) => { 564 | tui.console.write_to_gdb_log(e.to_string()); 565 | } 566 | }; 567 | } 568 | Event::GdbShutdown => { 569 | break 'runloop; 570 | } 571 | Event::Ipc(request) => { 572 | request.respond(&mut context); 573 | } 574 | Event::Pty(pty_output) => { 575 | tui.add_pty_input(&pty_output); 576 | } 577 | Event::Signal(signal_event) => { 578 | let sig = signal_event; 579 | match sig { 580 | Signal::SIGWINCH => { /* Ignore, we just want to redraw */ } 581 | Signal::SIGTSTP => { 582 | if let Err(e) = terminal.handle_sigtstp() { 583 | warn!("Unable to handle SIGTSTP: {}", e); 584 | } 585 | } 586 | Signal::SIGTERM => context.gdb.kill(), 587 | _ => {} 588 | } 589 | debug!("received signal {:?}", sig); 590 | } 591 | } 592 | if esc_in_focused_context_pressed { 593 | if focus_esc_timer.has_been_started() { 594 | input_mode = InputMode::ContainerSelect; 595 | } else { 596 | focus_esc_timer 597 | .try_start(Duration::from_millis(FOCUS_ESCAPE_MAX_DURATION_MS)); 598 | } 599 | } 600 | tui.update_after_event(&mut context); 601 | render_delay_timer.try_start(Duration::from_millis(EVENT_BUFFER_DURATION_MS)); 602 | } 603 | if esc_timer_needs_reset { 604 | focus_esc_timer.reset(); 605 | } 606 | app.draw( 607 | terminal.create_root_window(), 608 | &mut tui, 609 | input_mode.associated_border_style(), 610 | RenderingHints::default().blink(cursor_status), 611 | ); 612 | terminal.present(); 613 | } 614 | } 615 | 616 | let mut join_retry_counter = 0; 617 | let join_retry_duration = Duration::from_millis(100); 618 | let child_exit_status = loop { 619 | if let Some(ret) = context.gdb.mi.process.try_wait().expect("gdb exited") { 620 | break ret; 621 | } 622 | std::thread::sleep(join_retry_duration); 623 | if join_retry_counter == 10 { 624 | println!("Waiting for GDB to exit..."); 625 | } 626 | join_retry_counter += 1; 627 | }; 628 | if child_exit_status.success() { 629 | 0 630 | } else { 631 | println!("GDB exited with status {}.", child_exit_status); 632 | child_exit_status.code().unwrap_or(0xff) 633 | } 634 | } 635 | 636 | fn main() { 637 | let exit_code = run(); 638 | std::process::exit(exit_code); 639 | } 640 | -------------------------------------------------------------------------------- /src/tui/commands.rs: -------------------------------------------------------------------------------- 1 | use crate::gdbmi::{ 2 | commands::MiCommand, 3 | output::{ResultClass, ResultRecord}, 4 | ExecuteError, 5 | }; 6 | use crate::Context; 7 | 8 | use log::error; 9 | 10 | pub struct Command { 11 | cmd: Box Result<(), ExecuteError>>, 12 | } 13 | 14 | impl Command { 15 | fn new(cmd: Box Result<(), ExecuteError>>) -> Command { 16 | Command { cmd } 17 | } 18 | fn from_mi_with_msg(cmd: MiCommand, success_msg: &'static str) -> Command { 19 | Command::new(Box::new(move |p: &mut Context| { 20 | let res = p.gdb.mi.execute(cmd.clone()).map(|_| ()); 21 | if res.is_ok() { 22 | p.log(success_msg); 23 | } 24 | res 25 | })) 26 | } 27 | fn from_mi(cmd: MiCommand) -> Command { 28 | Command::new(Box::new(move |p: &mut Context| { 29 | p.gdb.mi.execute(cmd.clone()).map(|_| ()) 30 | })) 31 | } 32 | } 33 | 34 | pub enum CommandState { 35 | Idle, 36 | WaitingForConfirmation(Command), 37 | } 38 | 39 | impl CommandState { 40 | pub fn handle_input_line(&mut self, line: &str, p: &mut Context) { 41 | let mut tmp_state = CommandState::Idle; 42 | std::mem::swap(&mut tmp_state, self); 43 | *self = match tmp_state { 44 | CommandState::Idle => Self::dispatch_command(line, p), 45 | CommandState::WaitingForConfirmation(cmd) => Self::execute_if_confirmed(line, cmd, p), 46 | } 47 | } 48 | 49 | fn execute_if_confirmed(line: &str, cmd: Command, p: &mut Context) -> Self { 50 | match line { 51 | "y" | "Y" | "yes" => { 52 | Self::try_execute(cmd, p); 53 | CommandState::Idle 54 | } 55 | "n" | "N" | "no" => CommandState::Idle, 56 | _ => { 57 | p.log("Please type 'y' or 'n'."); 58 | CommandState::WaitingForConfirmation(cmd) 59 | } 60 | } 61 | } 62 | 63 | fn print_execute_error(e: ExecuteError, p: &mut Context) { 64 | match e { 65 | ExecuteError::Quit => p.log("quit"), 66 | ExecuteError::Busy => p.log("GDB is running!"), 67 | } 68 | } 69 | 70 | fn try_execute(mut cmd: Command, p: &mut Context) { 71 | match (cmd.cmd)(p) { 72 | Ok(_) => {} 73 | Err(e) => Self::print_execute_error(e, p), 74 | } 75 | } 76 | 77 | fn ask_if_session_active( 78 | cmd: Command, 79 | confirmation_question: &'static str, 80 | p: &mut Context, 81 | ) -> Self { 82 | match p.gdb.mi.is_session_active() { 83 | Ok(true) => { 84 | p.log(format!( 85 | "A debugging session is active. {} (y or n)", 86 | confirmation_question 87 | )); 88 | CommandState::WaitingForConfirmation(cmd) 89 | } 90 | Ok(false) => { 91 | Self::try_execute(cmd, p); 92 | CommandState::Idle 93 | } 94 | Err(e) => { 95 | Self::print_execute_error(e, p); 96 | CommandState::Idle 97 | } 98 | } 99 | } 100 | 101 | fn dispatch_command(line: &str, p: &mut Context) -> Self { 102 | let line = line.trim(); 103 | let cmd_end = line.find(' ').unwrap_or(line.len()); 104 | let cmd = &line[..cmd_end]; 105 | let args_begin = (cmd_end + 1).min(line.len()); 106 | let args_str = &line[args_begin..]; 107 | match cmd { 108 | "!stop" => { 109 | p.gdb.mi.interrupt_execution().expect("interrupted gdb"); 110 | // This does not always seem to unblock gdb, but only hang it 111 | //gdb.execute(&MiCommand::exec_interrupt()).expect("Interrupt"); 112 | 113 | CommandState::Idle 114 | } 115 | "!layout" => { 116 | p.try_change_layout(args_str.to_owned()); 117 | 118 | CommandState::Idle 119 | } 120 | "!show" => { 121 | p.show_file(args_str.to_owned(), unsegen::base::LineNumber::new(1)); 122 | 123 | CommandState::Idle 124 | } 125 | "!reload" => match p.gdb.get_target() { 126 | Ok(Some(target)) => Self::ask_if_session_active( 127 | Command::from_mi_with_msg( 128 | MiCommand::file_exec_and_symbols(&target), 129 | "Reloaded target.", 130 | ), 131 | "Reload anyway?", 132 | p, 133 | ), 134 | Ok(None) => { 135 | p.log("No target. Use the 'file' command to specify one."); 136 | CommandState::Idle 137 | } 138 | Err(e) => { 139 | Self::print_execute_error(e, p); 140 | CommandState::Idle 141 | } 142 | }, 143 | "shell" => { 144 | // This command does not work, because gdb breaks the gdbmi protocol (because it 145 | // likely just gives up stdout to the shell process until it terminates). This 146 | // cannot work in the curren architecture and actually just freezes ugdb. As a 147 | // workaround we just block this command. 148 | p.log("The `shell` command is not supported in ugdb. Consider suspending the process with Ctrl-z instead."); 149 | CommandState::Idle 150 | } 151 | "q" => { 152 | Self::ask_if_session_active(Command::from_mi(MiCommand::exit()), "Quit anyway?", p) 153 | } 154 | // Gdb commands 155 | _ => { 156 | match p.gdb.mi.execute(MiCommand::cli_exec(line)) { 157 | Ok(ResultRecord { 158 | class: ResultClass::Error, 159 | results, 160 | .. 161 | }) => { 162 | // Most of the time gdb seems to also write error messages to the console. 163 | // We therefore (only) write the error message to debug log to avoid duplicates. 164 | error!("{}", results["msg"].as_str().unwrap_or(&results.pretty(2))); 165 | } 166 | Ok(_) => {} 167 | Err(e) => Self::print_execute_error(e, p), 168 | } 169 | CommandState::Idle 170 | } 171 | } 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /src/tui/console.rs: -------------------------------------------------------------------------------- 1 | use crate::tui::commands::CommandState; 2 | use crate::Context; 3 | 4 | use unsegen::{ 5 | base::GraphemeCluster, 6 | container::Container, 7 | input::{EditBehavior, Input, Key, ScrollBehavior}, 8 | widget::{ 9 | builtin::{LogViewer, PromptLine}, 10 | VLayout, Widget, 11 | }, 12 | }; 13 | 14 | use crate::completion::{CmdlineCompleter, Completer, CompletionState}; 15 | 16 | #[derive(Debug, Copy, Clone, PartialEq, Eq)] 17 | enum GDBState { 18 | Running, 19 | Stopped, 20 | } 21 | 22 | pub struct Console { 23 | gdb_log: LogViewer, 24 | prompt_line: PromptLine, 25 | last_gdb_state: GDBState, 26 | command_state: CommandState, 27 | completion_state: Option, 28 | } 29 | 30 | static STOPPED_PROMPT: &str = "(gdb) "; 31 | static RUNNING_PROMPT: &str = "(↻↻↻) "; 32 | static SCROLL_PROMPT: &str = "(↑↓) "; 33 | static SEARCH_PROMPT: &str = "(🔍) "; 34 | 35 | impl Console { 36 | pub fn new() -> Self { 37 | let mut prompt_line = PromptLine::with_prompt(STOPPED_PROMPT.into()); 38 | prompt_line.set_search_prompt(SEARCH_PROMPT.to_owned()); 39 | prompt_line.set_scroll_prompt(SCROLL_PROMPT.to_owned()); 40 | Console { 41 | gdb_log: LogViewer::new(), 42 | prompt_line, 43 | last_gdb_state: GDBState::Stopped, 44 | command_state: CommandState::Idle, 45 | completion_state: None, 46 | } 47 | } 48 | 49 | pub fn write_to_gdb_log>(&mut self, msg: S) { 50 | use std::fmt::Write; 51 | write!(self.gdb_log, "{}", msg.as_ref()).expect("Write Message"); 52 | } 53 | 54 | fn handle_newline(&mut self, p: &mut Context) { 55 | let line = if self.prompt_line.active_line().is_empty() { 56 | self.prompt_line.previous_line(1).unwrap_or("").to_owned() 57 | } else { 58 | self.prompt_line.finish_line().to_owned() 59 | }; 60 | self.write_to_gdb_log(format!("{}{}\n", STOPPED_PROMPT, line)); 61 | self.command_state.handle_input_line(&line, p); 62 | } 63 | pub fn update_after_event(&mut self, p: &mut Context) { 64 | if p.gdb.mi.is_running() { 65 | if self.last_gdb_state != GDBState::Running { 66 | self.last_gdb_state = GDBState::Running; 67 | self.prompt_line.set_edit_prompt(RUNNING_PROMPT.to_owned()); 68 | } 69 | } else if self.last_gdb_state != GDBState::Stopped { 70 | self.last_gdb_state = GDBState::Stopped; 71 | self.prompt_line.set_edit_prompt(STOPPED_PROMPT.to_owned()); 72 | } 73 | } 74 | } 75 | 76 | impl Container for Console { 77 | fn input(&mut self, input: Input, p: &mut Context) -> Option { 78 | let set_completion = |completion_state: &Option, 79 | prompt_line: &mut PromptLine| { 80 | let completion = completion_state.as_ref().unwrap(); 81 | let (begin, option, after) = completion.current_line_parts(); 82 | prompt_line.set(&format!("{}{}{}", begin, option, after)); 83 | prompt_line 84 | .set_cursor_pos(begin.len() + option.len()) 85 | .unwrap(); 86 | }; 87 | let after_completion = input 88 | .chain((&[Key::Ctrl('p'), Key::Char('\t')][..], || { 89 | if let Some(s) = &mut self.completion_state { 90 | s.select_next_option(); 91 | } else { 92 | self.completion_state = Some(CmdlineCompleter(p).complete( 93 | self.prompt_line.active_line(), 94 | self.prompt_line.cursor_pos(), 95 | )); 96 | } 97 | set_completion(&self.completion_state, &mut self.prompt_line); 98 | })) 99 | .chain((Key::Ctrl('n'), || { 100 | if let Some(s) = &mut self.completion_state { 101 | s.select_prev_option(); 102 | } else { 103 | self.completion_state = Some(CmdlineCompleter(p).complete( 104 | self.prompt_line.active_line(), 105 | self.prompt_line.cursor_pos(), 106 | )); 107 | } 108 | set_completion(&self.completion_state, &mut self.prompt_line); 109 | })) 110 | .finish(); 111 | if let Some(input) = after_completion { 112 | self.completion_state = None; 113 | input 114 | .chain((Key::Char('\n'), || self.handle_newline(p))) 115 | .chain((Key::Ctrl('r'), || self.prompt_line.enter_search())) 116 | .chain( 117 | EditBehavior::new(&mut self.prompt_line) 118 | .left_on(Key::Left) 119 | .right_on(Key::Right) 120 | .up_on(Key::Up) 121 | .down_on(Key::Down) 122 | .delete_forwards_on(Key::Delete) 123 | .delete_backwards_on(Key::Backspace) 124 | .go_to_beginning_of_line_on(Key::Home) 125 | .go_to_end_of_line_on(Key::End) 126 | .clear_on(Key::Ctrl('c')), 127 | ) 128 | .chain(ScrollBehavior::new(&mut self.prompt_line).to_end_on(Key::Ctrl('r'))) 129 | .chain((Key::Ctrl('c'), || { 130 | p.gdb.mi.interrupt_execution().expect("interrupted gdb") 131 | })) 132 | .chain( 133 | ScrollBehavior::new(&mut self.gdb_log) 134 | .forwards_on(Key::PageDown) 135 | .backwards_on(Key::PageUp) 136 | .to_beginning_on(Key::Ctrl('b')) 137 | .to_end_on(Key::Ctrl('e')), 138 | ) 139 | .finish() 140 | } else { 141 | None 142 | } 143 | } 144 | fn as_widget<'a>(&'a self) -> Box { 145 | Box::new( 146 | VLayout::new() 147 | .separator(GraphemeCluster::try_from('=').unwrap()) 148 | .widget(self.gdb_log.as_widget()) 149 | .widget(self.prompt_line.as_widget()), 150 | ) 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /src/tui/expression_table.rs: -------------------------------------------------------------------------------- 1 | use crate::gdb_expression_parsing::Format; 2 | use crate::gdbmi::{commands::MiCommand, output::ResultClass, ExecuteError}; 3 | use crate::Context; 4 | use unsegen::{ 5 | base::{Color, GraphemeCluster, StyleModifier}, 6 | container::Container, 7 | input::{EditBehavior, Input, Key, NavigateBehavior, ScrollBehavior}, 8 | widget::{ 9 | builtin::{Column, LineEdit, Table, TableRow}, 10 | SeparatingStyle, Widget, 11 | }, 12 | }; 13 | use unsegen_jsonviewer::JsonViewer; 14 | 15 | use crate::completion::{Completer, CompletionState, IdentifierCompleter}; 16 | 17 | pub struct ExpressionRow { 18 | expression: LineEdit, 19 | completion_state: Option, 20 | result: JsonViewer, 21 | format: Option, 22 | } 23 | 24 | fn next_format(f: Option) -> Option { 25 | match f { 26 | None => Some(Format::Hex), 27 | Some(Format::Hex) => Some(Format::Decimal), 28 | Some(Format::Decimal) => Some(Format::Octal), 29 | Some(Format::Octal) => Some(Format::Binary), 30 | Some(Format::Binary) => None, 31 | } 32 | } 33 | 34 | impl ExpressionRow { 35 | fn new() -> Self { 36 | ExpressionRow { 37 | expression: LineEdit::new(), 38 | completion_state: None, 39 | result: JsonViewer::new(" "), 40 | format: None, 41 | } 42 | } 43 | 44 | fn is_empty(&self) -> bool { 45 | self.expression.get().is_empty() 46 | } 47 | fn update_result(&mut self, p: &mut Context) { 48 | let expr = self.expression.get().to_owned(); 49 | if expr.is_empty() { 50 | self.result.update(" "); 51 | } else { 52 | match p.gdb.mi.execute(MiCommand::data_evaluate_expression(expr)) { 53 | Ok(res) => match res.class { 54 | ResultClass::Error => { 55 | self.result.update(&res.results["msg"]); 56 | } 57 | ResultClass::Done => { 58 | let to_parse = res.results["value"].as_str().expect("value present"); 59 | match crate::gdb_expression_parsing::parse_gdb_value(to_parse) { 60 | Ok(n) => { 61 | let v = crate::gdb_expression_parsing::Value { 62 | node: &n, 63 | format: self.format, 64 | }; 65 | self.result.update(v); 66 | } 67 | Err(_) => { 68 | self.result 69 | .update(format!("*Error parsing*: {}", to_parse).as_str()); 70 | } 71 | } 72 | } 73 | other => panic!("unexpected result class: {:?}", other), 74 | }, 75 | Err(ExecuteError::Busy) => {} 76 | Err(ExecuteError::Quit) => { 77 | panic!("GDB quit!"); 78 | } 79 | } 80 | } 81 | } 82 | } 83 | impl TableRow for ExpressionRow { 84 | type BehaviorContext = Context; 85 | const COLUMNS: &'static [Column] = &[ 86 | Column { 87 | access: |r| Box::new(r.expression.as_widget()), 88 | behavior: |r, input, p| { 89 | let mut format_changed = false; 90 | let prev_content = r.expression.get().to_owned(); 91 | let set_completion = 92 | |completion_state: &Option, expression: &mut LineEdit| { 93 | let completion = completion_state.as_ref().unwrap(); 94 | let (begin, option, after) = completion.current_line_parts(); 95 | expression.set(&format!("{}{}{}", begin, option, after)); 96 | expression 97 | .set_cursor_pos(begin.len() + option.len()) 98 | .unwrap(); 99 | }; 100 | let res = input 101 | .chain((&[Key::Ctrl('n'), Key::Char('\t')][..], || { 102 | if let Some(s) = &mut r.completion_state { 103 | s.select_next_option(); 104 | } else { 105 | r.completion_state = Some( 106 | IdentifierCompleter(p) 107 | .complete(r.expression.get(), r.expression.cursor_pos()), 108 | ); 109 | } 110 | set_completion(&r.completion_state, &mut r.expression); 111 | })) 112 | .chain((Key::Ctrl('p'), || { 113 | if let Some(s) = &mut r.completion_state { 114 | s.select_prev_option(); 115 | } else { 116 | r.completion_state = Some( 117 | IdentifierCompleter(p) 118 | .complete(r.expression.get(), r.expression.cursor_pos()), 119 | ); 120 | } 121 | set_completion(&r.completion_state, &mut r.expression); 122 | })) 123 | .chain((Key::Ctrl('f'), || { 124 | r.format = next_format(r.format); 125 | format_changed = true; 126 | })) 127 | .if_not_consumed(|| r.completion_state = None) 128 | .chain((Key::Ctrl('w'), || { 129 | match p.gdb.mi.execute(MiCommand::insert_watchpoing( 130 | r.expression.get(), 131 | crate::gdbmi::commands::WatchMode::Access, 132 | )) { 133 | Ok(o) => match o.class { 134 | ResultClass::Done => { 135 | p.log(format!( 136 | "Inserted watchpoint for expression \"{}\"", 137 | r.expression.get() 138 | )); 139 | } 140 | ResultClass::Error => { 141 | p.log(format!( 142 | "Failed to set watchpoint: {}", 143 | o.results["msg"].as_str().unwrap(), 144 | )); 145 | } 146 | other => panic!("unexpected result class: {:?}", other), 147 | }, 148 | Err(e) => { 149 | p.log(format!("Failed to set watchpoint: {:?}", e)); 150 | } 151 | } 152 | })) 153 | .chain( 154 | EditBehavior::new(&mut r.expression) 155 | .left_on(Key::Left) 156 | .right_on(Key::Right) 157 | .up_on(Key::Up) 158 | .down_on(Key::Down) 159 | .delete_forwards_on(Key::Delete) 160 | .delete_backwards_on(Key::Backspace) 161 | .go_to_beginning_of_line_on(Key::Home) 162 | .go_to_end_of_line_on(Key::End) 163 | .clear_on(Key::Ctrl('c')), 164 | ) 165 | .finish(); 166 | 167 | if r.expression.get() != prev_content || format_changed { 168 | r.update_result(p); 169 | } 170 | res 171 | }, 172 | }, 173 | Column { 174 | access: |r| Box::new(r.result.as_widget()), 175 | behavior: |r, input, _| { 176 | input 177 | .chain( 178 | ScrollBehavior::new(&mut r.result) 179 | .forwards_on(Key::PageDown) 180 | .backwards_on(Key::PageUp) 181 | .forwards_on(Key::Down) 182 | .backwards_on(Key::Up) 183 | .to_beginning_on(Key::Home) 184 | .to_end_on(Key::End), 185 | ) 186 | .chain(|evt: Input| { 187 | if evt.matches(Key::Char(' ')) { 188 | if r.result.toggle_active_element().is_ok() { 189 | None 190 | } else { 191 | Some(evt) 192 | } 193 | } else { 194 | Some(evt) 195 | } 196 | }) 197 | .finish() 198 | }, 199 | }, 200 | ]; 201 | } 202 | 203 | pub struct ExpressionTable { 204 | table: Table, 205 | } 206 | 207 | impl ExpressionTable { 208 | pub fn new() -> Self { 209 | let mut table = Table::new(); 210 | table.rows_mut().push(ExpressionRow::new()); //Invariant: always at least one line 211 | ExpressionTable { table } 212 | } 213 | pub fn add_entry(&mut self, entry: String) { 214 | { 215 | let mut rows = self.table.rows_mut(); 216 | match rows.last_mut() { 217 | Some(row) if row.is_empty() => { 218 | row.expression.set(entry); 219 | } 220 | _ => { 221 | let mut row = ExpressionRow::new(); 222 | row.expression.set(entry); 223 | rows.push(row); 224 | } 225 | } 226 | } 227 | self.shrink_to_fit(); 228 | } 229 | fn shrink_to_fit(&mut self) { 230 | let begin_of_empty_range = { 231 | let iter = self.table.rows().iter().enumerate().rev(); 232 | let mut without_trailing_empty_rows = iter.skip_while(|&(_, r)| r.is_empty()); 233 | if let Some((i, _)) = without_trailing_empty_rows.next() { 234 | i + 1 235 | } else { 236 | 0 237 | } 238 | }; 239 | let mut rows = self.table.rows_mut(); 240 | rows.drain(begin_of_empty_range..); 241 | rows.push(ExpressionRow::new()); 242 | } 243 | 244 | pub fn update_results(&mut self, p: &mut Context) { 245 | for row in self.table.rows_mut().iter_mut() { 246 | row.update_result(p); 247 | } 248 | } 249 | } 250 | 251 | impl Container for ExpressionTable { 252 | fn input(&mut self, input: Input, p: &mut Context) -> Option { 253 | let res = input 254 | .chain( 255 | NavigateBehavior::new(&mut self.table) //TODO: Fix this properly in lineedit 256 | .down_on(Key::Char('\n')), 257 | ) 258 | .chain(self.table.current_cell_behavior(p)) 259 | .chain( 260 | NavigateBehavior::new(&mut self.table) 261 | .up_on(Key::Up) 262 | .down_on(Key::Down) 263 | .left_on(Key::Left) 264 | .right_on(Key::Right), 265 | ) 266 | .finish(); 267 | self.shrink_to_fit(); 268 | res 269 | } 270 | 271 | fn as_widget<'a>(&'a self) -> Box { 272 | Box::new( 273 | self.table 274 | .as_widget() 275 | .row_separation(SeparatingStyle::AlternatingStyle( 276 | StyleModifier::new().bg_color(Color::Black), 277 | )) 278 | .col_separation(SeparatingStyle::Draw( 279 | GraphemeCluster::try_from('│').unwrap(), 280 | )) 281 | .focused(StyleModifier::new().bold(true)), 282 | ) 283 | } 284 | } 285 | -------------------------------------------------------------------------------- /src/tui/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod commands; 2 | pub mod console; 3 | pub mod expression_table; 4 | pub mod srcview; 5 | #[allow(clippy::module_inception)] 6 | pub mod tui; 7 | 8 | pub use self::tui::*; 9 | -------------------------------------------------------------------------------- /src/tui/tui.rs: -------------------------------------------------------------------------------- 1 | use crate::Context; 2 | use unsegen_pager::Theme; 3 | 4 | use crate::gdbmi::output::{ 5 | AsyncClass, AsyncKind, JsonValue, Object, OutOfBandRecord, ThreadEvent, 6 | }; 7 | 8 | use super::console::Console; 9 | use super::expression_table::ExpressionTable; 10 | use super::srcview::CodeWindow; 11 | use log::{debug, info}; 12 | use unsegen::container::{Container, ContainerProvider}; 13 | use unsegen_terminal::Terminal; 14 | 15 | pub struct Tui<'a> { 16 | pub console: Console, 17 | pub expression_table: ExpressionTable, 18 | process_pty: Terminal, 19 | pub src_view: CodeWindow<'a>, 20 | } 21 | 22 | const WELCOME_MSG: &str = concat!( 23 | r#" Welcome to 24 | _ _ __ _ __| | |__ 25 | | | | |/ _` |/ _` | '_ \ 26 | | |_| | (_| | (_| | |_) | 27 | \__,_|\__, |\__,_|_.__/ 28 | |___/ 29 | version "#, 30 | env!("CRATE_VERSION"), 31 | r#" 32 | revision "#, 33 | env!("REVISION") 34 | ); 35 | 36 | impl<'a> Tui<'a> { 37 | pub fn new(terminal: Terminal, highlighting_theme: &'a Theme) -> Self { 38 | Tui { 39 | console: Console::new(), 40 | expression_table: ExpressionTable::new(), 41 | process_pty: terminal, 42 | src_view: CodeWindow::new(highlighting_theme, WELCOME_MSG), 43 | } 44 | } 45 | 46 | fn handle_async_record( 47 | &mut self, 48 | kind: AsyncKind, 49 | class: AsyncClass, 50 | results: &Object, 51 | p: &mut Context, 52 | ) { 53 | match (kind, class) { 54 | (AsyncKind::Exec, AsyncClass::Stopped) 55 | | (AsyncKind::Notify, AsyncClass::Thread(ThreadEvent::Selected)) => { 56 | debug!("stopped: {}", JsonValue::Object(results.clone()).pretty(2)); 57 | if let JsonValue::Object(ref frame) = results["frame"] { 58 | self.src_view.show_frame(frame, p); 59 | } 60 | self.expression_table.update_results(p); 61 | } 62 | (AsyncKind::Notify, AsyncClass::BreakPoint(event)) => { 63 | debug!( 64 | "bkpoint {:?}: {}", 65 | event, 66 | JsonValue::Object(results.clone()).pretty(2) 67 | ); 68 | p.gdb.handle_breakpoint_event(event, results); 69 | } 70 | (kind, class) => { 71 | info!( 72 | "unhandled async_record: [{:?}, {:?}] {}", 73 | kind, 74 | class, 75 | JsonValue::Object(results.clone()).pretty(2) 76 | ); 77 | } 78 | } 79 | } 80 | 81 | pub fn add_out_of_band_record(&mut self, record: OutOfBandRecord, p: &mut Context) { 82 | match record { 83 | OutOfBandRecord::StreamRecord { kind: _, data } => { 84 | self.console.write_to_gdb_log(data); 85 | } 86 | OutOfBandRecord::AsyncRecord { 87 | token: _, 88 | kind, 89 | class, 90 | results, 91 | } => { 92 | self.handle_async_record(kind, class, &results, p); 93 | } 94 | } 95 | } 96 | 97 | pub fn add_pty_input(&mut self, input: &[u8]) { 98 | self.process_pty.add_byte_input(input); 99 | } 100 | 101 | pub fn update_after_event(&mut self, p: &mut Context) { 102 | self.src_view.update_after_event(p); 103 | self.console.update_after_event(p); 104 | } 105 | } 106 | 107 | #[derive(Clone, PartialEq, Debug)] 108 | pub enum TuiContainerType { 109 | SrcView, 110 | Console, 111 | ExpressionTable, 112 | Terminal, 113 | } 114 | 115 | impl<'t> ContainerProvider for Tui<'t> { 116 | type Context = Context; 117 | type Index = TuiContainerType; 118 | fn get<'a, 'b: 'a>(&'b self, index: &'a Self::Index) -> &'b dyn Container { 119 | match index { 120 | TuiContainerType::SrcView => &self.src_view, 121 | TuiContainerType::Console => &self.console, 122 | TuiContainerType::ExpressionTable => &self.expression_table, 123 | TuiContainerType::Terminal => &self.process_pty, 124 | } 125 | } 126 | fn get_mut<'a, 'b: 'a>( 127 | &'b mut self, 128 | index: &'a Self::Index, 129 | ) -> &'b mut dyn Container { 130 | match index { 131 | TuiContainerType::SrcView => &mut self.src_view, 132 | TuiContainerType::Console => &mut self.console, 133 | TuiContainerType::ExpressionTable => &mut self.expression_table, 134 | TuiContainerType::Terminal => &mut self.process_pty, 135 | } 136 | } 137 | const DEFAULT_CONTAINER: TuiContainerType = TuiContainerType::Console; 138 | } 139 | -------------------------------------------------------------------------------- /ugdbception.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ftilde/ugdb/891106a1ba54a68faaf084ec63b94674256c62c0/ugdbception.png --------------------------------------------------------------------------------