debug_here/
internal.rs

1// Copyright 2018-2019 Ethan Pailes. See the COPYRIGHT
2// file at the top-level directory of this distribution.
3//
4// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
5// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
6// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
7// option. This file may not be copied, modified, or distributed
8// except according to those terms.
9
10/*!
11This module contains the internal implementation of debug-here. Nothing
12in this module is part of the public api of debug-here, even if it is
13marked `pub`.
14
15Certain functions must be marked `pub` in order for the `debug_here!()`
16macro to call them, but user code should never call them directly.
17*/
18
19use std::sync::Mutex;
20use std::process;
21
22#[cfg(target_os = "linux")]
23use std::{fs, env};
24
25#[cfg(target_os = "windows")]
26use winapi::um::debugapi;
27#[cfg(target_os = "windows")]
28use std::{path, thread};
29#[cfg(target_os = "windows")]
30use std::time::Duration;
31
32#[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
33compile_error!("debug-here: this crate currently only builds on linux, macos, and windows");
34
35fn already_entered() -> bool {
36    lazy_static! {
37        static ref GUARD: Mutex<bool> = Mutex::new(false);
38    }
39
40    // just propogate the thread poisoning with unwrap.
41    let mut entered = GUARD.lock().unwrap();
42
43    let ret = *entered;
44    *entered = true;
45    return ret;
46}
47
48/// The function responsible for actually launching the debugger.
49///
50/// If we have never launched a debugger before, we do so. Otherwise,
51/// we just don't do anything on the theory that if you are debugging
52/// something in a loop, you probably don't want a new debugger
53/// every time you step through your `debug_here!()`.
54///
55/// Before spawning the debugger we examine the execution environment
56/// a bit to try to help users through any configuration errors.
57///
58/// Don't use this directly.
59#[cfg(not(target_os = "windows"))]
60pub fn debug_here_unixy_impl(debugger: Option<&str>) {
61    if already_entered() {
62        return;
63    }
64
65
66    #[cfg(target_os = "linux")]
67    let sane_env = linux_check();
68    #[cfg(target_os = "macos")]
69    let sane_env = macos_check();
70
71    if let Err(e) = sane_env {
72        eprintln!("debug-here: {}", e);
73        return
74    }
75
76    #[cfg(target_os = "linux")]
77    let debugger = debugger.unwrap_or("rust-gdb");
78    #[cfg(target_os = "macos")]
79    let debugger = debugger.unwrap_or("rust-lldb");
80
81    if which::which(debugger).is_err() {
82        eprintln!("debug-here: can't find {} on your path. Bailing.", debugger);
83        return;
84    }
85
86    if which::which("debug-here-gdb-wrapper").is_err() {
87        eprintln!(r#"debug-here:
88            Can't find debug-here-gdb-wrapper on your path. To get it
89            you can run `cargo install debug-here-gdb-wrapper`
90            "#);
91        return;
92    }
93
94    // `looping` is a magic variable name that debug-here-gdb-wrapper knows to
95    // set to false in order to unstick us. We set it before launching the
96    // debugger to avoid a race condition.
97    let looping = true;
98
99    #[cfg(target_os = "linux")]
100    let launch_stat = linux_launch_term(debugger);
101    #[cfg(target_os = "macos")]
102    let launch_stat = macos_launch_term(debugger);
103
104    if let Err(e) = launch_stat {
105        eprintln!("debug-here: {}", e);
106        return;
107    }
108
109    // Now we enter an infinite loop and wait for the debugger to come to
110    // our rescue
111    while looping {}
112}
113
114/// Pop open a debugger on windows.
115///
116/// Windows has native just-in-time debugging capabilities via the debugapi
117/// winapi module, so we use that instead of manually popping open a termianl
118/// and launching the debugger in that.
119///
120/// We perform the same re-entry check as we do for non-windows platforms.
121///
122/// This approach pretty directly taken from:
123/// https://stackoverflow.com/questions/20337870/what-is-the-equivalent-of-system-diagnostics-debugger-launch-in-unmanaged-code
124///
125/// Don't use this directly.
126#[cfg(target_os = "windows")]
127pub fn debug_here_win_impl() {
128    if already_entered() {
129        return;
130    }
131
132    let jitdbg_exe = r#"c:\windows\system32\vsjitdebugger.exe"#;
133    if !path::Path::new(jitdbg_exe).exists() {
134        eprintln!("debug-here: could not find '{}'.", jitdbg_exe);
135        return;
136    }
137
138
139    let pid = process::id();
140
141    let mut cmd = process::Command::new(jitdbg_exe);
142    cmd.stdin(process::Stdio::null())
143       .stdout(process::Stdio::null())
144       .stderr(process::Stdio::null());
145    cmd.arg("-p").arg(pid.to_string());
146
147    if let Err(e) = cmd.spawn() {
148        eprintln!("debug-here: failed to launch '{}': {}", jitdbg_exe, e);
149        return;
150    }
151
152    // Argument for safty: this unsafe call doesn't manipulate memory
153    // in any way.
154    while unsafe { debugapi::IsDebuggerPresent() } == 0 {
155        thread::sleep(Duration::from_millis(100));
156    }
157
158    // Just mash F10 until you see your own code
159    unsafe { debugapi::DebugBreak(); }
160}
161
162
163/// The args required to launch the given debugger and attach to the
164/// current debugger.
165#[cfg(not(target_os = "windows"))]
166fn debugger_args(debugger: &str) -> Vec<String> {
167    if debugger == "rust-lldb" {
168        vec!["-p".to_string(),
169             process::id().to_string(),
170             "-o".to_string(),
171             "expression looping = 0".to_string(),
172             "-o".to_string(),
173             "finish".to_string()]
174    } else if debugger == "rust-gdb" {
175        vec!["-pid".to_string(),
176             process::id().to_string(),
177             "-ex".to_string(),
178             "set variable looping = 0".to_string(),
179             "-ex".to_string(),
180             "finish".to_string()]
181    } else {
182        panic!("unknown debugger: {}", debugger);
183    }
184}
185
186/// Perform sanity checks specific to a linux environment
187///
188/// Returns true on success, false if we should terminate early
189#[cfg(target_os = "linux")]
190fn linux_check() -> Result<(), String> {
191    let the_kids_are_ok =
192        fs::read("/proc/sys/kernel/yama/ptrace_scope")
193            .map(|contents|
194                 std::str::from_utf8(&contents[..1]).unwrap_or("1") == "0")
195            .unwrap_or(false);
196    if !the_kids_are_ok {
197        return Err(format!(r#"
198            ptrace_scope must be set to 0 for debug-here to work.
199            This will allow any process with a given uid to rummage around
200            in the memory of any other process with the same uid, so there
201            are some security risks. To set ptrace_scope for just this
202            session you can do:
203
204            echo 0 | sudo tee /proc/sys/kernel/yama/ptrace_scope
205
206            Giving up on debugging for now.
207            "#));
208    }
209
210    Ok(())
211}
212
213/// Launch a terminal in a linux environment
214#[cfg(target_os = "linux")]
215fn linux_launch_term(debugger: &str) -> Result<(), String> {
216    // Set up a magic environment variable telling debug-here-gdb-wrapper
217    // where to enter the program to be debugged.
218    //
219    // It is nicer to use an environment variable instead of a magic file
220    // or named pipe or something because that way only our kids will get
221    // to see it.
222    //
223    // The format here is `<format version no>,<pid>`.
224    //
225    // We have to do this in the linux-specific launch routine becuase
226    // macos is weird about how you can lauch a new terminal window,
227    // and doesn't just put new windows in a subprocess.
228    if debugger == "rust-gdb" {
229        // If we are being asked to launch rust-gdb, that can be handled with
230        // protocol version 1, so there is no need to pester users to upgrade.
231        env::set_var("RUST_DEBUG_HERE_LIFELINE",
232            format!("1,{}", process::id()));
233    } else {
234        env::set_var("RUST_DEBUG_HERE_LIFELINE",
235            format!("2,{},{}", process::id(), debugger));
236    }
237
238    let term = match which::which("alacritty").or(which::which("xterm")) {
239        Ok(t) => t,
240        Err(_) => {
241            return Err(format!(r#"
242                can't find alacritty or xterm on your path. Those are the
243                only terminal emulators currently supported on linux.
244                "#));
245        }
246    };
247    let term_cmd = term.clone();
248
249    let mut cmd = process::Command::new(term_cmd);
250    cmd.stdin(process::Stdio::null())
251       .stdout(process::Stdio::null())
252       .stderr(process::Stdio::null());
253
254    // Alacritty doesn't need the shim
255    if term.ends_with("alacritty") {
256        cmd.arg("-e");
257        cmd.arg(debugger);
258        cmd.args(debugger_args(debugger));
259    } else {
260        cmd.arg("debug-here-gdb-wrapper");
261    }
262
263    match cmd.spawn() {
264        Ok(_) => Ok(()),
265        Err(e) => Err(
266            format!("failed to launch rust-gdb in {:?}: {}", term, e))
267    }
268}
269
270/// sanity check the environment in a macos environment
271#[cfg(target_os = "macos")]
272fn macos_check() -> Result<(), String> {
273    if which::which("osascript").is_err() {
274        return Err(format!("debug-here: can't find osascript. Bailing."));
275    }
276
277    Ok(())
278}
279
280/// Launch a terminal in a macos environment
281#[cfg(target_os = "macos")]
282fn macos_launch_term(debugger: &str) -> Result<(), String> {
283    let launch_script =
284        format!(r#"tell app "Terminal"
285               do script "exec {} {}"
286           end tell"#, debugger,
287           debugger_args(debugger).into_iter()
288               .map(|a| if a.contains(" ") { format!("'{}'", a) } else { a } )
289               .collect::<Vec<_>>().join(" "));
290
291    let mut cmd = process::Command::new("osascript");
292    cmd.arg("-e")
293       .arg(launch_script)
294       .stdin(process::Stdio::null())
295       .stdout(process::Stdio::null())
296       .stderr(process::Stdio::null());
297
298    match cmd.spawn() {
299        Ok(_) => Ok(()),
300        Err(e) => Err(
301            format!("failed to launch {} in Terminal.app: {}", debugger, e))
302    }
303}