Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add node's --inspect debugger to port detection #2981

Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.d/2936.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Added debugger port detection type for the node `--inspect`, `--inspect-wait` and `--inspect-brk` flags
1 change: 0 additions & 1 deletion mirrord/config/src/util.rs
Original file line number Diff line number Diff line change
Expand Up @@ -250,7 +250,6 @@ where
deserializer.deserialize_any(StringOrStruct(PhantomData))
}

#[cfg(test)]
gememma marked this conversation as resolved.
Show resolved Hide resolved
pub mod testing {
use std::{
env,
Expand Down
255 changes: 178 additions & 77 deletions mirrord/layer/src/debugger_ports.rs
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,14 @@
/// @/var/folders/2h/fn_s1t8n0cqfc9x71yq845m40000gn/T/cp_dikq30ybalqwcehe333w2xxhd.argfile
/// com.example.demo.DemoApplication
JavaAgent,
/// Used in node applications, the flags `--inspect`, `--inspect-brk` and `--inspect-wait`
/// invoke the inspector. Invoking them as command line arguments is deprecated, but they are
/// set into the NODE_OPTIONS env var as, for example, `--inspect=9230`
///
/// the NODE_OPTIONS env var looks like this:
/// "NODE_OPTIONS": "--require=<path> --inspect-publish-uid=http --max-old-space-size=9216

Check failure on line 88 in mirrord/layer/src/debugger_ports.rs

View workflow job for this annotation

GitHub Actions / check-rust-docs

unclosed HTML tag `path`
/// --enable-source-maps --inspect=9994"
NodeInspector,
}

impl FromStr for DebuggerType {
Expand All @@ -91,81 +99,142 @@
"pydevd" => Ok(Self::PyDevD),
"resharper" => Ok(Self::ReSharper),
"javaagent" => Ok(Self::JavaAgent),
"nodeinspector" => Ok(Self::NodeInspector),
_ => Err(format!("invalid debugger type: {s}")),
}
}
}

impl DebuggerType {
/// Retrieves the port used by debugger of this type from the command.
fn get_port(self, args: &[String]) -> Option<u16> {
/// May return multiple ports when using the inspect flags with node
fn get_port(self, args: &[String]) -> Vec<u16> {
match self {
Self::DebugPy => {
let is_python = args.first()?.rsplit('/').next()?.starts_with("py");
let runs_debugpy = if args.get(1)?.starts_with("-X") {
args.get(3)?.ends_with("debugpy") // newer args layout
let is_python = args
.first()
.unwrap_or(&"".to_string())
gememma marked this conversation as resolved.
Show resolved Hide resolved
.rsplit('/')
.next()
.unwrap_or_default()
.starts_with("py");
let runs_debugpy = if args.get(1).unwrap_or(&"".to_string()).starts_with("-X") {
args.get(3).unwrap_or(&"".to_string()).ends_with("debugpy") // newer args layout
} else {
args.get(1)?.ends_with("debugpy") // older args layout
args.get(1).unwrap_or(&"".to_string()).ends_with("debugpy") // older args layout
};

if !is_python || !runs_debugpy {
None?
if is_python && runs_debugpy {
args.windows(2).find_map(|window| match window {
[opt, val] if opt == "--connect" => val.parse::<SocketAddr>().ok(),
_ => None,
})
} else {
None
}

args.windows(2).find_map(|window| match window {
[opt, val] if opt == "--connect" => val.parse::<SocketAddr>().ok(),
_ => None,
})
.into_iter()
.collect::<Vec<_>>()
}
Self::PyDevD => {
let is_python = args.first()?.rsplit('/').next()?.starts_with("py");
let runs_pydevd = args.get(1)?.rsplit('/').next()?.contains("pydevd");

if !is_python || !runs_pydevd {
None?
let is_python = args
.first()
.unwrap_or(&"".to_string())
.rsplit('/')
.next()
.unwrap_or_default()
.starts_with("py");
let runs_pydevd = args
.get(1)
.unwrap_or(&"".to_string())
.rsplit('/')
.next()
.unwrap_or_default()
.contains("pydevd");

if is_python && runs_pydevd {
let client = args.windows(2).find_map(|window| match window {
[opt, val] if opt == "--client" => val.parse::<IpAddr>().ok(),
_ => None,
});
let port = args.windows(2).find_map(|window| match window {
[opt, val] if opt == "--port" => val.parse::<u16>().ok(),
_ => None,
});

if let (Some(client), Some(port)) = (client, port) {
SocketAddr::new(client, port).into()
} else {
None
}
} else {
None
}

let client = args.windows(2).find_map(|window| match window {
[opt, val] if opt == "--client" => val.parse::<IpAddr>().ok(),
_ => None,
})?;
let port = args.windows(2).find_map(|window| match window {
[opt, val] if opt == "--port" => val.parse::<u16>().ok(),
_ => None,
})?;

SocketAddr::new(client, port).into()
.into_iter()
.collect::<Vec<_>>()
}
Self::ReSharper => {
let is_dotnet = args.first()?.ends_with("dotnet");
let runs_debugger = args.get(2)?.contains("Debugger");

if !is_dotnet || !runs_debugger {
None?
let is_dotnet = args.first().unwrap_or(&"".to_string()).ends_with("dotnet");
let runs_debugger = args.get(2).unwrap_or(&"".to_string()).contains("Debugger");

if is_dotnet && runs_debugger {
args.iter()
.find_map(|arg| arg.strip_prefix("--frontend-port="))
.and_then(|port| port.parse::<u16>().ok())
.map(|port| SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), port))
} else {
None
}

args.iter()
.find_map(|arg| arg.strip_prefix("--frontend-port="))
.and_then(|port| port.parse::<u16>().ok())
.map(|port| SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), port))
.into_iter()
.collect::<Vec<_>>()
}
Self::JavaAgent => {
let is_java = args.first()?.ends_with("java");

if !is_java {
None?
let is_java = args.first().unwrap_or(&"".to_string()).ends_with("java");

if is_java {
args.iter()
.find_map(|arg| arg.strip_prefix("-agentlib:jdwp=transport=dt_socket"))
.and_then(|agent_lib_args| {
agent_lib_args
.split(',')
.find_map(|arg| arg.strip_prefix("address="))
})
.and_then(|full_address| full_address.split(':').last())
.and_then(|port| port.parse::<u16>().ok())
.map(|port| SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), port))
} else {
None
}

args.iter()
.find_map(|arg| arg.strip_prefix("-agentlib:jdwp=transport=dt_socket"))
.and_then(|agent_lib_args| agent_lib_args.split(',').find_map(|arg| arg.strip_prefix("address=")))
.and_then(|full_address| full_address.split(':').last())
.and_then(|port| port.parse::<u16>().ok())
.map(|port| SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), port))

.into_iter()
.collect::<Vec<_>>()
}
}
.and_then(|addr| match addr.ip() {
Self::NodeInspector => {
let is_node = args.first().unwrap_or(&"".to_string()).ends_with("node");

if is_node && let Ok(value) = std::env::var("NODE_OPTIONS") {
// matching specific flags so we avoid matching on, for example,
// `--inspect-publish-uid=http`
let flags = value.split(" --").filter(|s| {
s.contains("inspect=")
|| s.contains("inspect-brk=")
|| s.contains("inspect-wait=")
});
flags
.filter_map(|flag| {
let vec = flag.split('=').collect::<Vec<_>>();
match vec.as_slice() {
// you can use --inspect, -wait and -brk all together at once -
// we need to ignore them all
&[_flag, port] => port.parse::<u16>().ok(),
_ => None,
}
})
.map(|port| SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), port))
.collect::<Vec<SocketAddr>>()
gememma marked this conversation as resolved.
Show resolved Hide resolved
} else {
vec![]
}
}
}.iter().filter_map(|addr| match addr.ip() {
IpAddr::V4(Ipv4Addr::LOCALHOST) | IpAddr::V6(Ipv6Addr::LOCALHOST) => Some(addr.port()),
other => {
warn!(
Expand All @@ -175,14 +244,15 @@
None
}
})
.collect::<Vec<_>>()
}
}

/// Local ports used by the debugger running the process.
/// These should be ignored by the layer.
#[derive(Debug)]
pub enum DebuggerPorts {
gememma marked this conversation as resolved.
Show resolved Hide resolved
Detected(u16),
Detected(Vec<u16>),
FixedRange(RangeInclusive<u16>),
None,
}
Expand All @@ -194,23 +264,28 @@
///
/// Log errors (like malformed env variables) but do not panic.
pub fn from_env() -> Self {
let detected = env::var(MIRRORD_DETECT_DEBUGGER_PORT_ENV)
.ok()
.and_then(|s| {
DebuggerType::from_str(&s)
.inspect_err(|e| {
error!(
"Failed to decode debugger type from {} env variable: {}",
MIRRORD_DETECT_DEBUGGER_PORT_ENV, e
)
})
.ok()
})
.and_then(|d| d.get_port(&std::env::args().collect::<Vec<_>>()));
if let Some(port) = detected {
env::set_var(MIRRORD_IGNORE_DEBUGGER_PORTS_ENV, port.to_string());
let detected: Vec<u16> =
match env::var(MIRRORD_DETECT_DEBUGGER_PORT_ENV)
.ok()
.and_then(|s| {
DebuggerType::from_str(&s)
.inspect_err(|e| {
error!(
"Failed to decode debugger type from {} env variable: {}",
MIRRORD_DETECT_DEBUGGER_PORT_ENV, e
)
})
.ok()
}) {
Some(debugger) => debugger.get_port(&std::env::args().collect::<Vec<_>>()),
None => vec![],
};
if !detected.is_empty() {
detected
.iter()
.for_each(|port| env::set_var(MIRRORD_IGNORE_DEBUGGER_PORTS_ENV, port.to_string()));
gememma marked this conversation as resolved.
Show resolved Hide resolved
env::remove_var(MIRRORD_DETECT_DEBUGGER_PORT_ENV);
return Self::Detected(port);
return Self::Detected(detected);
}

let fixed_range = env::var(MIRRORD_IGNORE_DEBUGGER_PORTS_ENV)
Expand Down Expand Up @@ -256,7 +331,7 @@
}

match self {
Self::Detected(port) => *port == addr.port(),
Self::Detected(ports) => ports.contains(&addr.port()),
Self::FixedRange(range) => range.contains(&addr.port()),
Self::None => false,
}
Expand All @@ -265,6 +340,7 @@

#[cfg(test)]
mod test {
use mirrord_config::util::testing::with_env_vars;
use rstest::rstest;

use super::*;
Expand All @@ -281,7 +357,7 @@
.map(ToString::to_string)
.collect::<Vec<_>>()
),
Some(57141),
vec![57141],
)
}

Expand All @@ -297,7 +373,7 @@
.map(ToString::to_string)
.collect::<Vec<_>>()
),
Some(32845),
vec![32845],
)
}

Expand All @@ -313,7 +389,7 @@
.map(ToString::to_string)
.collect::<Vec<_>>()
),
Some(40905)
vec![40905]
)
}

Expand All @@ -334,15 +410,40 @@
.map(ToString::to_string)
.collect::<Vec<_>>()
),
Some(54898)
vec![54898]
)
}

#[rstest]
#[case(("NODE_OPTIONS", Some("--require=/path --inspect-publish-uid=http --inspect=9994")), vec![9994])]
#[case(("NODE_OPTIONS", Some("--require=/path --inspect-publish-uid=http --inspect=9994 --inspect-brk=9001")), vec![9994, 9001])]
fn detect_nodeinspector_port(#[case] env: (&str, Option<&str>), #[case] ports: Vec<u16>) {
let debugger = DebuggerType::NodeInspector;
let command = "/Path/to/node /Path/to/node/v20.17.0/bin/npx next dev";

with_env_vars(vec![env], {
|| {
assert_eq!(
debugger.get_port(
&command
.split_ascii_whitespace()
.map(ToString::to_string)
.collect::<Vec<_>>()
),
ports
)
}
});
}

#[test]
fn debugger_ports_contain() {
assert!(DebuggerPorts::Detected(1337).contains(&"127.0.0.1:1337".parse().unwrap()));
assert!(!DebuggerPorts::Detected(1337).contains(&"127.0.0.1:1338".parse().unwrap()));
assert!(!DebuggerPorts::Detected(1337).contains(&"8.8.8.8:1337".parse().unwrap()));
assert!(DebuggerPorts::Detected(vec![1337]).contains(&"127.0.0.1:1337".parse().unwrap()));
assert!(
DebuggerPorts::Detected(vec![1337, 1338]).contains(&"127.0.0.1:1337".parse().unwrap())
);
assert!(!DebuggerPorts::Detected(vec![1337]).contains(&"127.0.0.1:1338".parse().unwrap()));
assert!(!DebuggerPorts::Detected(vec![1337]).contains(&"8.8.8.8:1337".parse().unwrap()));

assert!(
DebuggerPorts::FixedRange(45000..=50000).contains(&"127.0.0.1:47888".parse().unwrap())
Expand Down
Loading