diff --git a/CHANGELOG.md b/CHANGELOG.md index 122f3dcf1a8..c52d3d77a21 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,43 @@ This project uses [*towncrier*](https://towncrier.readthedocs.io/) and the chang +## [3.128.0](https://github.com/metalbear-co/mirrord/tree/3.128.0) - 2024-12-19 + + +### Added + +- Added to mirrord config a new experimental field + `.experimental.readonly_file_buffer`. If set to a value greater than 0, + mirrord will fetch remote readonly files in chunks of at least this size (in bytes). + This is to improve performance with applications that make many small reads + from remote files. + [#2069](https://github.com/metalbear-co/mirrord/issues/2069) +- Added `mirrord container-ext` command that should be used by extension and + works similarly to `mirrord ext` but for containers. +- Added runAsNonRoot and RO file system to operator deployment +- Added custom resource definition for cluster-wide mirrord policy - + `MirrordClusterPolicy`. +- Added mapping option for env vars config, allowing the user to map multiple env + vars to another value based on regexes. + [#2920](https://github.com/metalbear-co/mirrord/issues/2920) +- Added mkdir support + [#2221](https://github.com/metalbear-co/mirrord/issues/2221) + + +### Fixed + +- Added debugger port detection type for the node `--inspect`, `--inspect-wait` + and `--inspect-brk` flags. + [#2936](https://github.com/metalbear-co/mirrord/issues/2936) +- Fixed `mirrord operator setup` - added missing `/tmp` volume to the operator + deployment. + + +### Internal + +- Added E2E test for `MirrordClusterPolicy` that blocks incoming traffic + mirroring. + ## [3.127.0](https://github.com/metalbear-co/mirrord/tree/3.127.0) - 2024-12-10 diff --git a/Cargo.lock b/Cargo.lock index 2fa998364dd..daa15fb105b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2410,7 +2410,7 @@ dependencies = [ [[package]] name = "fileops" -version = "3.127.0" +version = "3.128.0" dependencies = [ "libc", ] @@ -3536,7 +3536,7 @@ checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" [[package]] name = "issue1317" -version = "3.127.0" +version = "3.128.0" dependencies = [ "actix-web", "env_logger 0.11.5", @@ -3546,7 +3546,7 @@ dependencies = [ [[package]] name = "issue1776" -version = "3.127.0" +version = "3.128.0" dependencies = [ "errno 0.3.10", "libc", @@ -3555,7 +3555,7 @@ dependencies = [ [[package]] name = "issue1776portnot53" -version = "3.127.0" +version = "3.128.0" dependencies = [ "libc", "socket2", @@ -3563,14 +3563,14 @@ dependencies = [ [[package]] name = "issue1899" -version = "3.127.0" +version = "3.128.0" dependencies = [ "libc", ] [[package]] name = "issue2001" -version = "3.127.0" +version = "3.128.0" dependencies = [ "libc", ] @@ -3891,7 +3891,7 @@ checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" [[package]] name = "listen_ports" -version = "3.127.0" +version = "3.128.0" [[package]] name = "litemap" @@ -4119,7 +4119,7 @@ dependencies = [ [[package]] name = "mirrord" -version = "3.127.0" +version = "3.128.0" dependencies = [ "actix-codec", "clap", @@ -4179,7 +4179,7 @@ dependencies = [ [[package]] name = "mirrord-agent" -version = "3.127.0" +version = "3.128.0" dependencies = [ "actix-codec", "async-trait", @@ -4235,7 +4235,7 @@ dependencies = [ [[package]] name = "mirrord-analytics" -version = "3.127.0" +version = "3.128.0" dependencies = [ "assert-json-diff", "base64 0.22.1", @@ -4249,7 +4249,7 @@ dependencies = [ [[package]] name = "mirrord-auth" -version = "3.127.0" +version = "3.128.0" dependencies = [ "bcder", "chrono", @@ -4270,7 +4270,7 @@ dependencies = [ [[package]] name = "mirrord-config" -version = "3.127.0" +version = "3.128.0" dependencies = [ "bimap", "bitflags 2.6.0", @@ -4293,7 +4293,7 @@ dependencies = [ [[package]] name = "mirrord-config-derive" -version = "3.127.0" +version = "3.128.0" dependencies = [ "proc-macro2", "proc-macro2-diagnostics", @@ -4303,7 +4303,7 @@ dependencies = [ [[package]] name = "mirrord-console" -version = "3.127.0" +version = "3.128.0" dependencies = [ "bincode", "drain", @@ -4319,7 +4319,7 @@ dependencies = [ [[package]] name = "mirrord-intproxy" -version = "3.127.0" +version = "3.128.0" dependencies = [ "bytes", "exponential-backoff", @@ -4350,7 +4350,7 @@ dependencies = [ [[package]] name = "mirrord-intproxy-protocol" -version = "3.127.0" +version = "3.128.0" dependencies = [ "bincode", "mirrord-protocol", @@ -4360,7 +4360,7 @@ dependencies = [ [[package]] name = "mirrord-kube" -version = "3.127.0" +version = "3.128.0" dependencies = [ "actix-codec", "async-stream", @@ -4390,7 +4390,7 @@ dependencies = [ [[package]] name = "mirrord-layer" -version = "3.127.0" +version = "3.128.0" dependencies = [ "actix-codec", "base64 0.22.1", @@ -4440,7 +4440,7 @@ dependencies = [ [[package]] name = "mirrord-layer-macro" -version = "3.127.0" +version = "3.128.0" dependencies = [ "proc-macro2", "quote", @@ -4449,7 +4449,7 @@ dependencies = [ [[package]] name = "mirrord-macros" -version = "3.127.0" +version = "3.128.0" dependencies = [ "proc-macro2", "proc-macro2-diagnostics", @@ -4459,7 +4459,7 @@ dependencies = [ [[package]] name = "mirrord-operator" -version = "3.127.0" +version = "3.128.0" dependencies = [ "base64 0.22.1", "bincode", @@ -4493,7 +4493,7 @@ dependencies = [ [[package]] name = "mirrord-progress" -version = "3.127.0" +version = "3.128.0" dependencies = [ "enum_dispatch", "indicatif", @@ -4528,7 +4528,7 @@ dependencies = [ [[package]] name = "mirrord-sip" -version = "3.127.0" +version = "3.128.0" dependencies = [ "apple-codesign", "object 0.36.5", @@ -4541,7 +4541,7 @@ dependencies = [ [[package]] name = "mirrord-vpn" -version = "3.127.0" +version = "3.128.0" dependencies = [ "futures", "ipnet", @@ -4891,7 +4891,7 @@ dependencies = [ [[package]] name = "outgoing" -version = "3.127.0" +version = "3.128.0" [[package]] name = "outref" @@ -5976,14 +5976,14 @@ dependencies = [ [[package]] name = "rust-bypassed-unix-socket" -version = "3.127.0" +version = "3.128.0" dependencies = [ "tokio", ] [[package]] name = "rust-e2e-fileops" -version = "3.127.0" +version = "3.128.0" dependencies = [ "libc", ] @@ -5999,7 +5999,7 @@ dependencies = [ [[package]] name = "rust-unix-socket-client" -version = "3.127.0" +version = "3.128.0" dependencies = [ "tokio", ] diff --git a/Cargo.toml b/Cargo.toml index 11f578c8c28..da8a4e788d0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,7 +27,7 @@ resolver = "2" # latest commits on rustls suppress certificate verification [workspace.package] -version = "3.127.0" +version = "3.128.0" edition = "2021" license = "MIT" readme = "README.md" diff --git a/changelog.d/+cluster-policy-test.internal.md b/changelog.d/+cluster-policy-test.internal.md deleted file mode 100644 index 3ac77a3ce02..00000000000 --- a/changelog.d/+cluster-policy-test.internal.md +++ /dev/null @@ -1 +0,0 @@ -Added E2E test for `MirrordClusterPolicy` that blocks incoming traffic mirroring. diff --git a/changelog.d/+cluster-policy.added.md b/changelog.d/+cluster-policy.added.md deleted file mode 100644 index 2725780086b..00000000000 --- a/changelog.d/+cluster-policy.added.md +++ /dev/null @@ -1 +0,0 @@ -Added custom resource definition for cluster-wide mirrord policy - `MirrordClusterPolicy`. diff --git a/changelog.d/+container-ext.added.md b/changelog.d/+container-ext.added.md deleted file mode 100644 index cf5c2549dce..00000000000 --- a/changelog.d/+container-ext.added.md +++ /dev/null @@ -1 +0,0 @@ -Add `mirrord container-ext` command that should be used by extension and works similarly to `mirrord ext` but for containers. diff --git a/changelog.d/+operator-deployment-strict.added.md b/changelog.d/+operator-deployment-strict.added.md deleted file mode 100644 index a72b1ccd58a..00000000000 --- a/changelog.d/+operator-deployment-strict.added.md +++ /dev/null @@ -1 +0,0 @@ -Add runAsNonRoot and RO file system to operator deployment diff --git a/changelog.d/+operator-tmp-volume.fixed.md b/changelog.d/+operator-tmp-volume.fixed.md deleted file mode 100644 index db28de877e5..00000000000 --- a/changelog.d/+operator-tmp-volume.fixed.md +++ /dev/null @@ -1 +0,0 @@ -Fixed `mirrord operator setup` - added missing `/tmp` volume to the operator deployment. diff --git a/changelog.d/2069.added.md b/changelog.d/2069.added.md deleted file mode 100644 index f8efa7d3147..00000000000 --- a/changelog.d/2069.added.md +++ /dev/null @@ -1,2 +0,0 @@ -Added to mirrord config a new experimental flag `.experimental.buffer_file_reads`. When this flag is enabled, mirrord will fetch remote readonly files in at least 4kb chunks. -This is to improve performance with applications that make many small reads from remote files. diff --git a/changelog.d/2920.changed.md b/changelog.d/2920.changed.md deleted file mode 100644 index b03aff8d438..00000000000 --- a/changelog.d/2920.changed.md +++ /dev/null @@ -1 +0,0 @@ -Add mapping option for env vars config, allowing the user to map multiple env vars to another value based on regexes. diff --git a/mirrord/layer/Cargo.toml b/mirrord/layer/Cargo.toml index b14e577d4f1..d46147b8482 100644 --- a/mirrord/layer/Cargo.toml +++ b/mirrord/layer/Cargo.toml @@ -19,19 +19,22 @@ workspace = true # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -mirrord-config = { path = "../config"} -mirrord-protocol = { path = "../protocol"} -mirrord-layer-macro = { path = "./macro"} +mirrord-config = { path = "../config" } +mirrord-protocol = { path = "../protocol" } +mirrord-layer-macro = { path = "./macro" } mirrord-console = { path = "../console" } -mirrord-intproxy-protocol = { path = "../intproxy/protocol", features = ["codec"] } +mirrord-intproxy-protocol = { path = "../intproxy/protocol", features = [ + "codec", +] } ctor = "0.2" libc.workspace = true bincode.workspace = true -nix = { workspace = true, features = ["net", "process", "signal"]} +nix = { workspace = true, features = ["net", "process", "signal"] } tracing.workspace = true tracing-subscriber.workspace = true frida-gum = { version = "0.15", features = ["auto-download"] } +hyper = { workspace = true } serde_json.workspace = true @@ -60,7 +63,6 @@ mirrord-intproxy = { path = "../intproxy" } k8s-openapi.workspace = true chrono = { workspace = true, features = ["clock"] } http-body = { workspace = true } -hyper = { workspace = true } rstest.workspace = true tempfile.workspace = true test-cdylib = "*" diff --git a/mirrord/layer/src/debugger_ports.rs b/mirrord/layer/src/debugger_ports.rs index 2b2feb0aed6..f78520fa643 100644 --- a/mirrord/layer/src/debugger_ports.rs +++ b/mirrord/layer/src/debugger_ports.rs @@ -1,10 +1,11 @@ use std::{ - env, + env, fmt, net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr}, ops::RangeInclusive, str::FromStr, }; +use hyper::Uri; use tracing::{error, warn}; /// Environment variable used to tell the layer that it should dynamically detect the local @@ -20,10 +21,14 @@ pub const MIRRORD_DETECT_DEBUGGER_PORT_ENV: &str = "MIRRORD_DETECT_DEBUGGER_PORT /// be used by the debugger. Used when injecting the layer through IDE. This setting will be ignored /// if the layer successfully detects the port at runtime, see [`MIRRORD_DETECT_DEBUGGER_PORT_ENV`]. /// -/// Value passed through this variable can represent a single port like '12233' or a range of ports -/// like `12233-13000`. +/// Value passed through this variable can represent a single port like '12233', a range of ports +/// like '12233-13000' or multiple individual ports like '12233,13344,14455' pub const MIRRORD_IGNORE_DEBUGGER_PORTS_ENV: &str = "MIRRORD_IGNORE_DEBUGGER_PORTS"; +/// The default port used by node's --inspect debugger from the +/// [node doceumentation](https://nodejs.org/en/learn/getting-started/debugging#enable-inspector) +pub const NODE_INSPECTOR_DEFAULT_PORT: u16 = 9229; + /// Type of debugger which is used to run the user's processes. /// Determines the way we parse the command line arguments the debugger's port. /// Logic of processing the arguments is based on examples taken from the IDEs. @@ -80,6 +85,21 @@ pub enum DebuggerType { /// @/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/to/thing --inspect-publish-uid=http + /// --max-old-space-size=9216 --enable-source-maps --inspect=9994" + /// + /// Alternatively, the process may be string on a different port with the NODE_INSPECTOR_INFO + /// env var set, with the port in the "inspectorURL" address + /// + /// the NODE_INSPECTOR_INFO env var looks like this: + /// "NODE_INSPECTOR_INFO" : {"ipcAddress":"/Path/to/thing","pid":"75321",... + /// "inspectorURL":"ws://127.0.0.1:9229/8decd19b-8ea8-45f4-bf72-095ddbdad103"} + NodeInspector, } impl FromStr for DebuggerType { @@ -91,81 +111,162 @@ impl FromStr for DebuggerType { "pydevd" => Ok(Self::PyDevD), "resharper" => Ok(Self::ReSharper), "javaagent" => Ok(Self::JavaAgent), + "nodeinspector" => Ok(Self::NodeInspector), _ => Err(format!("invalid debugger type: {s}")), } } } +impl fmt::Display for DebuggerType { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "{}", + match self { + DebuggerType::DebugPy => "debugpy", + DebuggerType::PyDevD => "pydevd", + DebuggerType::ReSharper => "resharper", + DebuggerType::JavaAgent => "javaagent", + DebuggerType::NodeInspector => "nodeinspector", + } + ) + } +} + impl DebuggerType { /// Retrieves the port used by debugger of this type from the command. - fn get_port(self, args: &[String]) -> Option { - match self { + /// May return multiple ports when using the inspect flags with node. + /// Also returns the value to set into MIRRORD_DETECT_DEBUGGER_PORT_ENV, if any + fn get_ports Option>( + self, + args: &[String], + mut get_env: F, + ) -> (Vec, Option) { + let ports = 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() + .map(String::as_str) + .unwrap_or_default() + .rsplit('/') + .next() + .unwrap_or_default() + .starts_with("py"); + let runs_debugpy = if args.get(1).map(String::as_str).unwrap_or_default().starts_with("-X") { + args.get(3).map(String::as_str).unwrap_or_default().ends_with("debugpy") // newer args layout } else { - args.get(1)?.ends_with("debugpy") // older args layout + args.get(1).map(String::as_str).unwrap_or_default().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::().ok(), + _ => None, + }) + } else { + None } - - args.windows(2).find_map(|window| match window { - [opt, val] if opt == "--connect" => val.parse::().ok(), - _ => None, - }) + .into_iter() + .collect::>() } 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() + .map(String::as_str) + .unwrap_or_default() + .rsplit('/') + .next() + .unwrap_or_default() + .starts_with("py"); + let runs_pydevd = args + .get(1) + .map(String::as_str) + .unwrap_or_default() + .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::().ok(), + _ => None, + }); + let port = args.windows(2).find_map(|window| match window { + [opt, val] if opt == "--port" => val.parse::().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::().ok(), - _ => None, - })?; - let port = args.windows(2).find_map(|window| match window { - [opt, val] if opt == "--port" => val.parse::().ok(), - _ => None, - })?; - - SocketAddr::new(client, port).into() + .into_iter() + .collect::>() } 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().map(String::as_str).unwrap_or_default().ends_with("dotnet"); + let runs_debugger = args.get(2).map(String::as_str).unwrap_or_default().contains("Debugger"); + + if is_dotnet && runs_debugger { + args.iter() + .find_map(|arg| arg.strip_prefix("--frontend-port=")) + .and_then(|port| port.parse::().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::().ok()) - .map(|port| SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), port)) + .into_iter() + .collect::>() } Self::JavaAgent => { - let is_java = args.first()?.ends_with("java"); - - if !is_java { - None? + let is_java = args.first().map(String::as_str).unwrap_or_default().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::().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::().ok()) - .map(|port| SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), port)) - + .into_iter() + .collect::>() } - } - .and_then(|addr| match addr.ip() { + Self::NodeInspector => { + if let Some(value) = get_env("NODE_OPTIONS") { + // matching specific flags so we avoid matching on, for example, + // `--inspect-publish-uid=http` + value.split_ascii_whitespace() + .filter_map(|flag| match flag.split_once('=') { + Some(("--inspect" | "--inspect-brk" | "--inspect-wait", port)) => port.parse::().ok(), + None if ["--inspect", "--inspect-brk", "--inspect-wait"].contains(&flag) => Some(NODE_INSPECTOR_DEFAULT_PORT), + _ => None, + }) + .map(|port| SocketAddr::new(Ipv4Addr::LOCALHOST.into(), port)) + .collect::>() + } else if let Some(value) = get_env("NODE_INSPECTOR_INFO") { + value.split(',').filter_map(|var| match var.split_once(':')? { + ("inspectorURL", url) => url.parse::().ok()?.port_u16(), + _ => None, + }) + .map(|port| SocketAddr::new(Ipv4Addr::LOCALHOST.into(), port)) + .collect::>() + } else { + vec![] + } + } + }.iter().filter_map(|addr| match addr.ip() { IpAddr::V4(Ipv4Addr::LOCALHOST) | IpAddr::V6(Ipv6Addr::LOCALHOST) => Some(addr.port()), other => { warn!( @@ -175,18 +276,85 @@ impl DebuggerType { None } }) + .collect::>(); + + let next_type = match self { + // node may require several rounds of port ignoring when used in the VSCode plugin + DebuggerType::NodeInspector => Some(DebuggerType::NodeInspector), + _ => None, + }; + (ports, next_type) } } /// Local ports used by the debugger running the process. /// These should be ignored by the layer. -#[derive(Debug)] +#[derive(Clone, Debug)] pub enum DebuggerPorts { - Detected(u16), + Single(u16), FixedRange(RangeInclusive), + Combination(Vec), None, } +impl FromStr for DebuggerPorts { + type Err = std::convert::Infallible; + + fn from_str(s: &str) -> Result { + // string looks like 'port1,port2,port3-portx,porty-portz' + let mut vec = vec![]; + s.split(',') + .for_each(|s| { + let chunks = s + .split('-') + .map(u16::from_str) + .collect::, _>>() + .inspect_err(|e| error!( + "Failed to decode debugger port range from {} env variable: {}", + MIRRORD_IGNORE_DEBUGGER_PORTS_ENV, + e + )) + .ok().unwrap_or_default(); + match *chunks.as_slice() { + [p] => vec.push(Self::Single(p)), + [p1, p2] if p1 <= p2 => vec.push(Self::FixedRange(p1..=p2)), + _ => { + error!( + "Failed to decode debugger ports from {} env variable: expected a port or range of ports", + MIRRORD_IGNORE_DEBUGGER_PORTS_ENV, + ); + }, + }; + }); + if !vec.is_empty() { + Ok(Self::Combination(vec)) + } else { + Ok(Self::None) + } + } +} + +impl fmt::Display for DebuggerPorts { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "{}", + match self { + DebuggerPorts::Single(port) => port.to_string(), + DebuggerPorts::FixedRange(range_inclusive) => + format!("{}-{}", range_inclusive.start(), range_inclusive.end()), + DebuggerPorts::Combination(vec) => { + vec.iter() + .map(ToString::to_string) + .collect::>() + .join(",") + } + DebuggerPorts::None => String::default(), + } + ) + } +} + impl DebuggerPorts { /// Create a new instance of this enum based on the environment variables /// ([`MIRRORD_DETECT_DEBUGGER_PORT_ENV`] and [`MIRRORD_IGNORE_DEBUGGER_PORTS_ENV`]) and command @@ -194,7 +362,7 @@ impl DebuggerPorts { /// /// Log errors (like malformed env variables) but do not panic. pub fn from_env() -> Self { - let detected = env::var(MIRRORD_DETECT_DEBUGGER_PORT_ENV) + let (detected, next) = match env::var(MIRRORD_DETECT_DEBUGGER_PORT_ENV) .ok() .and_then(|s| { DebuggerType::from_str(&s) @@ -205,44 +373,38 @@ impl DebuggerPorts { ) }) .ok() - }) - .and_then(|d| d.get_port(&std::env::args().collect::>())); - if let Some(port) = detected { - env::set_var(MIRRORD_IGNORE_DEBUGGER_PORTS_ENV, port.to_string()); - env::remove_var(MIRRORD_DETECT_DEBUGGER_PORT_ENV); - return Self::Detected(port); + }) { + Some(debugger) => debugger.get_ports(&std::env::args().collect::>(), |name| { + std::env::var(name).ok() + }), + None => (vec![], None), + }; + if !detected.is_empty() { + let mut dbg_ports = detected + .iter() + .map(|&port| Some(Self::Single(port))) + .collect::>(); + if let Ok(existing) = env::var(MIRRORD_IGNORE_DEBUGGER_PORTS_ENV) { + dbg_ports.push(DebuggerPorts::from_str(&existing).ok()); + } + let dbg_ports = dbg_ports.into_iter().flatten().collect::>(); + let dbg_port = Self::Combination(dbg_ports); + env::set_var(MIRRORD_IGNORE_DEBUGGER_PORTS_ENV, dbg_port.to_string()); + + if let Some(next_type) = next { + env::set_var(MIRRORD_DETECT_DEBUGGER_PORT_ENV, next_type.to_string()); + } else { + env::remove_var(MIRRORD_DETECT_DEBUGGER_PORT_ENV); + } + return dbg_port; } - let fixed_range = env::var(MIRRORD_IGNORE_DEBUGGER_PORTS_ENV) + // IGNORE_DEBUGGER_PORTS may have a combination of single, multiple or ranges of ports + // separated by a comma they need to be parsed individually + env::var(MIRRORD_IGNORE_DEBUGGER_PORTS_ENV) .ok() - .and_then(|s| { - let chunks = s - .split('-') - .map(u16::from_str) - .collect::, _>>() - .inspect_err(|e| error!( - "Failed to decode debugger ports from {} env variable: {}", - MIRRORD_IGNORE_DEBUGGER_PORTS_ENV, - e - )) - .ok()?; - match *chunks.as_slice() { - [p] => Some(p..=p), - [p1, p2] if p1 <= p2 => Some(p1..=p2), - _ => { - error!( - "Failed to decode debugger ports from {} env variable: expected a port or a range of ports", - MIRRORD_IGNORE_DEBUGGER_PORTS_ENV, - ); - None - }, - } - }); - if let Some(range) = fixed_range { - return Self::FixedRange(range); - } - - Self::None + .and_then(|s| Self::from_str(&s).ok()) + .unwrap_or(Self::None) } /// Return whether the given [SocketAddr] is used by the debugger. @@ -256,8 +418,12 @@ impl DebuggerPorts { } match self { - Self::Detected(port) => *port == addr.port(), + Self::Single(port) => port == &addr.port(), Self::FixedRange(range) => range.contains(&addr.port()), + Self::Combination(vec) => vec + .iter() + .map(|ports| ports.contains(addr)) + .fold(false, |acc, ports| ports || acc), Self::None => false, } } @@ -275,13 +441,16 @@ mod test { let debugger = DebuggerType::DebugPy; assert_eq!( - debugger.get_port( - &command - .split_ascii_whitespace() - .map(ToString::to_string) - .collect::>() - ), - Some(57141), + debugger + .get_ports( + &command + .split_ascii_whitespace() + .map(ToString::to_string) + .collect::>(), + |_| None + ) + .0, + vec![57141], ) } @@ -291,13 +460,16 @@ mod test { let command = "/path/to/python /path/to/pycharm/plugins/pydevd.py --multiprocess --qt-support=auto --client 127.0.0.1 --port 32845 --file /path/to/script.py"; assert_eq!( - debugger.get_port( - &command - .split_ascii_whitespace() - .map(ToString::to_string) - .collect::>() - ), - Some(32845), + debugger + .get_ports( + &command + .split_ascii_whitespace() + .map(ToString::to_string) + .collect::>(), + |_| None + ) + .0, + vec![32845], ) } @@ -307,13 +479,16 @@ mod test { let command = "/path/to/rider/lib/ReSharperHost/linux-x64/dotnet/dotnet exec /path/to/rider/lib/ReSharperHost/JetBrains.Debugger.Worker.exe --mode=client --frontend-port=40905 --plugins=/path/to/rider/plugins/rider-unity/dotnetDebuggerWorker;/path/to/rider/plugins/dpa/DotFiles/JetBrains.DPA.DebugInjector.dll --etw-collect-flags=2 --backend-pid=222222 --handle=333"; assert_eq!( - debugger.get_port( - &command - .split_ascii_whitespace() - .map(ToString::to_string) - .collect::>() - ), - Some(40905) + debugger + .get_ports( + &command + .split_ascii_whitespace() + .map(ToString::to_string) + .collect::>(), + |_| None + ) + .0, + vec![40905] ) } @@ -328,21 +503,52 @@ mod test { let debugger = DebuggerType::JavaAgent; assert_eq!( - debugger.get_port( - &command_line - .split_ascii_whitespace() - .map(ToString::to_string) - .collect::>() - ), - Some(54898) + debugger + .get_ports( + &command_line + .split_ascii_whitespace() + .map(ToString::to_string) + .collect::>(), + |_| None + ) + .0, + 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")), vec![9229])] + #[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) { + let debugger = DebuggerType::NodeInspector; + let command = "/Path/to/node /Path/to/node/v20.17.0/bin/npx next dev"; + + assert_eq!( + debugger + .get_ports( + &command + .split_ascii_whitespace() + .map(ToString::to_string) + .collect::>(), + |name| { + if name == env.0 { + env.1.map(ToString::to_string) + } else { + None + } + } + ) + .0, + 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::Single(1337).contains(&"127.0.0.1:1337".parse().unwrap())); + assert!(!DebuggerPorts::Single(1337).contains(&"127.0.0.1:1338".parse().unwrap())); + assert!(!DebuggerPorts::Single(1337).contains(&"8.8.8.8:1337".parse().unwrap())); assert!( DebuggerPorts::FixedRange(45000..=50000).contains(&"127.0.0.1:47888".parse().unwrap()) @@ -354,6 +560,12 @@ mod test { !DebuggerPorts::FixedRange(45000..=50000).contains(&"8.8.8.8:47888".parse().unwrap()) ); + let ports = vec![DebuggerPorts::Single(1337), DebuggerPorts::Single(1338)]; + assert!( + DebuggerPorts::Combination(ports.clone()).contains(&"127.0.0.1:1337".parse().unwrap()) + ); + assert!(!DebuggerPorts::Combination(ports).contains(&"127.0.0.1:1340".parse().unwrap())); + assert!(!DebuggerPorts::None.contains(&"127.0.0.1:1337".parse().unwrap())); } }