Skip to content

Commit b47520d

Browse files
divybotlittledivy
andauthored
fix: allow --inspect=localhost:0 to resolve hostnames (#34230)
## Summary Node accepts hostnames in `--inspect=...`, `--inspect-port=...`, and `inspector.open(port, host)`, resolving them via DNS. Deno's inspector flag parser and `op_inspector_open` only accepted IP-literal hosts, so `--inspect=localhost:0` failed at parse time. - Resolve hostnames via `ToSocketAddrs` in `inspect_value_parser` and `op_inspector_open`, preferring IPv4 results to match Node's resolver. - Add `op_inspector_port`. The `process.debugPort` getter consults it so userland sees the ephemeral port chosen by `--inspect=...:0`. An explicit `process.debugPort = N` assignment still wins, matching Node's mutable semantics. - For `--inspect-port=N` used without `--inspect[-brk|-wait]`, Node updates `process.debugPort` but does not start the inspector. Deno doesn't surface `--inspect-port` as a runtime flag, so the node_shim translator injects the equivalent `process.debugPort = N;` assignment into the eval code used by `-p`/`-e`. - Enables `parallel/test-inspector-port-zero.js` in the node_compat config. Closes denoland/orchid#142 ## Test plan - New unit test `inspect_value_parser_resolves_hostnames` in `cli/args/flags.rs` covers `localhost:0`, `localhost:1234`, and `localhost`. - New unit tests in `libs/node_shim/lib.rs` (`test_translate_inspect_port_injects_debug_port_for_print` and `test_translate_inspect_port_does_not_inject_when_inspector_enabled`). - `tests/node_compat/runner/suite/test/parallel/test-inspector-port-zero.js` now passes (verified across 5 consecutive runs). - Existing inspector tests still pass: `test-inspector-debug-end.js`, `test-inspector-has-idle.js`, `test-inspector-promises.js`, `test-inspector-open.js`, `test-inspector-open-coverage.js`, `test-inspector-open-port-integer-overflow.js`. - Existing inspector parser unit tests still pass: `inspect_flag_parsing`, `inspect_wait`, `inspect_default_host`, `inspect_publish_uid_flag_parsing`. - `cargo clippy -p deno_node -p node_shim -p deno --bin deno --lib` clean. Co-authored-by: divybot <divybot@users.noreply.github.com> Co-authored-by: Divy Srivastava <me@littledivy.com>
1 parent fbb5be3 commit b47520d

6 files changed

Lines changed: 186 additions & 21 deletions

File tree

cli/args/flags.rs

Lines changed: 44 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ use std::env;
66
use std::ffi::OsString;
77
use std::net::IpAddr;
88
use std::net::SocketAddr;
9+
use std::net::ToSocketAddrs;
910
use std::num::NonZeroU8;
1011
use std::num::NonZeroU32;
1112
use std::num::NonZeroUsize;
@@ -5710,6 +5711,26 @@ pub fn inspect_value_parser(host_and_port: &str) -> Result<SocketAddr, String> {
57105711
.map_err(|_| format!("Invalid inspector port '{port}'"))
57115712
}
57125713

5714+
// Resolve a host string to an IP address. IPs are returned as-is.
5715+
// Hostnames (e.g. "localhost") are resolved via DNS so Node's
5716+
// `--inspect=localhost:0` syntax works. To match Node's resolver,
5717+
// IPv4 results are preferred when both families are returned.
5718+
fn resolve_host(host: &str) -> Result<IpAddr, String> {
5719+
if let Ok(ip) = host.parse::<IpAddr>() {
5720+
return Ok(ip);
5721+
}
5722+
let addrs = (host, 0u16)
5723+
.to_socket_addrs()
5724+
.map_err(|e| format!("Invalid inspector host '{host}': {}", e))?
5725+
.collect::<Vec<_>>();
5726+
addrs
5727+
.iter()
5728+
.find(|a| a.is_ipv4())
5729+
.or_else(|| addrs.first())
5730+
.map(|a| a.ip())
5731+
.ok_or_else(|| format!("Could not resolve inspector host '{host}'"))
5732+
}
5733+
57135734
let default_host: IpAddr = DEFAULT_HOST.parse().unwrap();
57145735

57155736
if host_and_port.is_empty() {
@@ -5744,9 +5765,7 @@ pub fn inspect_value_parser(host_and_port: &str) -> Result<SocketAddr, String> {
57445765
parse_port(port_part)?
57455766
};
57465767

5747-
let host_ip = host_part
5748-
.parse::<IpAddr>()
5749-
.map_err(|e| format!("Invalid inspector host '{host_part}': {:?}", e))?;
5768+
let host_ip = resolve_host(host_part)?;
57505769

57515770
return Ok(SocketAddr::new(host_ip, port));
57525771
}
@@ -5756,9 +5775,7 @@ pub fn inspect_value_parser(host_and_port: &str) -> Result<SocketAddr, String> {
57565775
return Ok(SocketAddr::new(default_host, port));
57575776
}
57585777

5759-
let host_ip = host_and_port.parse::<IpAddr>().map_err(|e| {
5760-
format!("Invalid inspector host '{host_and_port}': {:?}", e)
5761-
})?;
5778+
let host_ip = resolve_host(host_and_port)?;
57625779

57635780
Ok(SocketAddr::new(host_ip, DEFAULT_PORT))
57645781
}
@@ -16235,6 +16252,27 @@ Usage: deno repl [OPTIONS] [-- [ARGS]...]\n"
1623516252
}
1623616253
}
1623716254

16255+
#[test]
16256+
fn inspect_value_parser_resolves_hostnames() {
16257+
// Node accepts `--inspect=localhost:0` (and similar hostname forms);
16258+
// the parser should resolve them via DNS rather than rejecting.
16259+
let cases = [
16260+
("localhost:0", 0),
16261+
("localhost:1234", 1234),
16262+
("localhost", 9229),
16263+
];
16264+
for (input, expected_port) in cases {
16265+
let addr = inspect_value_parser(input)
16266+
.unwrap_or_else(|e| panic!("failed to parse {input:?}: {e}"));
16267+
assert_eq!(addr.port(), expected_port, "port for {input:?}");
16268+
assert!(
16269+
addr.ip().is_loopback(),
16270+
"expected loopback for {input:?}, got {}",
16271+
addr.ip()
16272+
);
16273+
}
16274+
}
16275+
1623816276
#[test]
1623916277
fn inspect_publish_uid_flag_parsing() {
1624016278
// Test with both stderr and http

ext/node/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -380,6 +380,7 @@ deno_core::extension!(deno_node,
380380
ops::inspector::op_inspector_disconnect,
381381
ops::inspector::op_inspector_emit_protocol_event,
382382
ops::inspector::op_inspector_enabled,
383+
ops::inspector::op_inspector_port,
383384
ops::udp::op_node_udp_bind,
384385
ops::udp::op_node_udp_join_multi_v4,
385386
ops::udp::op_node_udp_leave_multi_v4,

ext/node/ops/inspector.rs

Lines changed: 53 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
use std::cell::RefCell;
44
use std::net::IpAddr;
55
use std::net::SocketAddr;
6+
use std::net::ToSocketAddrs;
67
use std::rc::Rc;
78

89
use deno_core::GarbageCollected;
@@ -27,6 +28,30 @@ pub fn op_inspector_enabled(state: &OpState) -> bool {
2728
state.try_borrow::<InspectorServerUrl>().is_some()
2829
}
2930

31+
/// Returns the port the inspector server is listening on, or 0 if the
32+
/// inspector server has not been started. Used to back Node.js'
33+
/// `process.debugPort` so it reflects the actual bound port (which is
34+
/// important when `--inspect=...:0` requests an ephemeral port).
35+
#[op2(fast)]
36+
pub fn op_inspector_port(state: &OpState) -> u32 {
37+
let Some(url) = state.try_borrow::<InspectorServerUrl>() else {
38+
return 0;
39+
};
40+
// URL looks like `ws://host:port/<uuid>` (or `wss://...`). Parse out
41+
// the port. We don't depend on the `url` crate here to keep the op
42+
// tiny; the format is fixed by `get_websocket_debugger_url`.
43+
let s = url.0.as_str();
44+
let Some(after_scheme) = s.split_once("://").map(|(_, rest)| rest) else {
45+
return 0;
46+
};
47+
let host_and_port = after_scheme.split('/').next().unwrap_or("");
48+
let port_str = match host_and_port.rsplit_once(':') {
49+
Some((_, p)) => p,
50+
None => return 0,
51+
};
52+
port_str.parse::<u16>().map(|p| p as u32).unwrap_or(0)
53+
}
54+
3055
#[op2(stack_trace)]
3156
pub fn op_inspector_open(
3257
state: &mut OpState,
@@ -39,12 +64,34 @@ pub fn op_inspector_open(
3964
const DEFAULT_PORT: u16 = 9229;
4065

4166
let host_ip: IpAddr = match &host {
42-
Some(h) => h.parse().map_err(|e| {
43-
InspectorOpenError::InvalidHost(format!(
44-
"Invalid inspector host '{}': {}",
45-
h, e
46-
))
47-
})?,
67+
Some(h) => {
68+
if let Ok(ip) = h.parse::<IpAddr>() {
69+
ip
70+
} else {
71+
// Resolve hostnames like "localhost" via DNS to match Node's
72+
// `inspector.open(port, host)` behavior. Prefer IPv4 results.
73+
let addrs = (h.as_str(), 0u16)
74+
.to_socket_addrs()
75+
.map_err(|e| {
76+
InspectorOpenError::InvalidHost(format!(
77+
"Invalid inspector host '{}': {}",
78+
h, e
79+
))
80+
})?
81+
.collect::<Vec<_>>();
82+
addrs
83+
.iter()
84+
.find(|a| a.is_ipv4())
85+
.or_else(|| addrs.first())
86+
.map(|a| a.ip())
87+
.ok_or_else(|| {
88+
InspectorOpenError::InvalidHost(format!(
89+
"Could not resolve inspector host '{}'",
90+
h
91+
))
92+
})?
93+
}
94+
}
4895
None => DEFAULT_HOST,
4996
};
5097
let port = port.unwrap_or(DEFAULT_PORT);

ext/node/polyfills/process.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import {
2323
op_getgroups,
2424
op_inspector_close,
2525
op_inspector_enabled,
26+
op_inspector_port,
2627
op_node_load_env_file,
2728
op_node_process_constrained_memory,
2829
op_node_process_kill,
@@ -904,8 +905,17 @@ Object.defineProperty(process, "argv0", {
904905
*/
905906
// Node's default inspector port (kDefaultInspectorPort in src/node_options.h).
906907
let _debugPort = 9229;
908+
let _debugPortWasSet = false;
907909
Object.defineProperty(process, "debugPort", {
908910
get() {
911+
// When the inspector is running, report the actual bound port so
912+
// `--inspect=...:0` reflects the ephemeral port chosen at bind
913+
// time. An explicit assignment via the setter wins over this
914+
// (matching Node's mutable `process.debugPort`).
915+
if (!_debugPortWasSet) {
916+
const port = op_inspector_port();
917+
if (port !== 0) return port;
918+
}
909919
return _debugPort;
910920
},
911921
set(val) {
@@ -917,6 +927,7 @@ Object.defineProperty(process, "debugPort", {
917927
);
918928
}
919929
_debugPort = port;
930+
_debugPortWasSet = true;
920931
},
921932
enumerable: true,
922933
configurable: true,

libs/node_shim/lib.rs

Lines changed: 76 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,10 @@ pub struct DebugOptions {
7878
pub inspect_publish_uid_string: String,
7979
pub inspect_publish_uid: InspectPublishUid,
8080
pub host_port: HostPort,
81+
/// True when `--inspect-port` was explicitly passed. Used to make
82+
/// `process.debugPort` reflect the requested port even when the
83+
/// inspector itself was not enabled, matching Node's behavior.
84+
pub inspect_port_explicit: bool,
8185
}
8286

8387
impl Default for DebugOptions {
@@ -92,6 +96,7 @@ impl Default for DebugOptions {
9296
inspect_publish_uid_string: "stderr,http".to_string(),
9397
inspect_publish_uid: InspectPublishUid::default(),
9498
host_port: HostPort::default(),
99+
inspect_port_explicit: false,
95100
}
96101
}
97102
}
@@ -2764,12 +2769,19 @@ impl OptionsParser {
27642769
value: HostPort,
27652770
) {
27662771
match name {
2767-
"--inspect-port" => options
2768-
.per_isolate
2769-
.per_env
2770-
.debug_options
2771-
.host_port
2772-
.update(&value),
2772+
"--inspect-port" => {
2773+
options
2774+
.per_isolate
2775+
.per_env
2776+
.debug_options
2777+
.host_port
2778+
.update(&value);
2779+
options
2780+
.per_isolate
2781+
.per_env
2782+
.debug_options
2783+
.inspect_port_explicit = true;
2784+
}
27732785
_ => {
27742786
// Unknown host port option
27752787
}
@@ -3372,10 +3384,28 @@ pub fn translate_to_deno_args(
33723384
let raw_eval_code = eval_string_for_print
33733385
.as_ref()
33743386
.unwrap_or(&env_opts.eval_string);
3387+
3388+
// When `--inspect-port=N` is passed without `--inspect[-brk|-wait]`,
3389+
// Node updates `process.debugPort` to N but does not start the
3390+
// inspector. Deno doesn't carry `--inspect-port` as a runtime flag,
3391+
// so emit an equivalent assignment as part of the eval code.
3392+
let raw_eval_code_owned;
3393+
let raw_eval_code: &str = if env_opts.debug_options.inspect_port_explicit
3394+
&& !env_opts.debug_options.inspector_enabled
3395+
{
3396+
raw_eval_code_owned = format!(
3397+
"process.debugPort = {}; {}",
3398+
env_opts.debug_options.host_port.port, raw_eval_code
3399+
);
3400+
raw_eval_code_owned.as_str()
3401+
} else {
3402+
raw_eval_code.as_str()
3403+
};
3404+
33753405
let eval_code = if options.wrap_eval_code {
33763406
wrap_eval_code(raw_eval_code)
33773407
} else {
3378-
raw_eval_code.clone()
3408+
raw_eval_code.to_string()
33793409
};
33803410
deno_args.push(eval_code);
33813411

@@ -4793,6 +4823,45 @@ mod tests {
47934823
);
47944824
}
47954825

4826+
#[test]
4827+
fn test_translate_inspect_port_injects_debug_port_for_print() {
4828+
// `--inspect-port=N` without `--inspect[-brk|-wait]` should make
4829+
// `process.debugPort` equal N — matching Node — even though Deno
4830+
// has no equivalent runtime flag. The translator injects the
4831+
// assignment into the eval code used by `-p`/`-e`.
4832+
let parsed =
4833+
parse_args(svec!["--inspect-port=0", "-p", "process.debugPort"]).unwrap();
4834+
let result = translate_to_deno_args(parsed, &TranslateOptions::default());
4835+
let eval_arg = result
4836+
.deno_args
4837+
.iter()
4838+
.find(|a| a.contains("process.debugPort"))
4839+
.expect("expected an eval arg referencing process.debugPort");
4840+
assert!(
4841+
eval_arg.contains("process.debugPort = 0"),
4842+
"expected process.debugPort assignment in eval arg, got: {eval_arg}"
4843+
);
4844+
}
4845+
4846+
#[test]
4847+
fn test_translate_inspect_port_does_not_inject_when_inspector_enabled() {
4848+
// When `--inspect` is also set, `process.debugPort` is updated by
4849+
// the inspector itself; we should not inject an assignment.
4850+
let parsed =
4851+
parse_args(svec!["--inspect=localhost:0", "-p", "process.debugPort"])
4852+
.unwrap();
4853+
let result = translate_to_deno_args(parsed, &TranslateOptions::default());
4854+
let eval_arg = result
4855+
.deno_args
4856+
.iter()
4857+
.find(|a| a.contains("process.debugPort"))
4858+
.expect("expected an eval arg referencing process.debugPort");
4859+
assert!(
4860+
!eval_arg.contains("process.debugPort ="),
4861+
"should not inject debugPort assignment, got: {eval_arg}"
4862+
);
4863+
}
4864+
47964865
// ==================== Env File Options Tests ====================
47974866

47984867
#[test]

tests/node_compat/config.jsonc

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2334,8 +2334,7 @@
23342334
// "parallel/test-inspector-port-zero-cluster.js": {},
23352335
// "parallel/test-inspect-support-for-node_options.js": {},
23362336

2337-
// TODO(bartlomieju):: easy, `--inspect` flag doesn't support `localhost`
2338-
// "parallel/test-inspector-port-zero.js": {},
2337+
"parallel/test-inspector-port-zero.js": {},
23392338
"parallel/test-inspector-promises.js": {},
23402339
"parallel/test-inspector-reported-host.js": {},
23412340
"parallel/test-inspector-resource-name-to-url.js": {

0 commit comments

Comments
 (0)