diff --git a/cli/args/flags.rs b/cli/args/flags.rs index c60bc0d99895bd..ac2b7a0624dbc2 100644 --- a/cli/args/flags.rs +++ b/cli/args/flags.rs @@ -312,6 +312,7 @@ pub struct Flags { pub ignore: Vec, pub import_map_path: Option, pub inspect_brk: Option, + pub inspect_wait: Option, pub inspect: Option, pub location: Option, pub lock_write: bool, @@ -1482,6 +1483,7 @@ fn run_subcommand<'a>() -> Command<'a> { .arg( watch_arg(true) .conflicts_with("inspect") + .conflicts_with("inspect-wait") .conflicts_with("inspect-brk"), ) .arg(no_clear_screen_arg()) @@ -1622,6 +1624,7 @@ fn test_subcommand<'a>() -> Command<'a> { .takes_value(true) .value_name("DIR") .conflicts_with("inspect") + .conflicts_with("inspect-wait") .conflicts_with("inspect-brk") .help("UNSTABLE: Collect coverage profile data into DIR"), ) @@ -1964,7 +1967,20 @@ fn inspect_args(app: Command) -> Command { .long("inspect-brk") .value_name("HOST:PORT") .help( - "Activate inspector on host:port and break at start of user script", + "Activate inspector on host:port, wait for debugger to connect and break at the start of user script", + ) + .min_values(0) + .max_values(1) + .require_equals(true) + .takes_value(true) + .validator(inspect_arg_validate), + ) + .arg( + Arg::new("inspect-wait") + .long("inspect-wait") + .value_name("HOST:PORT") + .help( + "Activate inspector on host:port and wait for debugger to connect before running user code", ) .min_values(0) .max_values(1) @@ -3040,6 +3056,15 @@ fn inspect_arg_parse(flags: &mut Flags, matches: &clap::ArgMatches) { } else { None }; + flags.inspect_wait = if matches.is_present("inspect-wait") { + if let Some(host) = matches.value_of("inspect-wait") { + Some(host.parse().unwrap()) + } else { + Some(default()) + } + } else { + None + }; } fn import_map_arg_parse(flags: &mut Flags, matches: &clap::ArgMatches) { @@ -5875,6 +5900,38 @@ mod tests { ); } + #[test] + fn inspect_wait() { + let r = flags_from_vec(svec!["deno", "run", "--inspect-wait", "foo.js"]); + assert_eq!( + r.unwrap(), + Flags { + subcommand: DenoSubcommand::Run(RunFlags { + script: "foo.js".to_string(), + }), + inspect_wait: Some("127.0.0.1:9229".parse().unwrap()), + ..Flags::default() + } + ); + + let r = flags_from_vec(svec![ + "deno", + "run", + "--inspect-wait=127.0.0.1:3567", + "foo.js" + ]); + assert_eq!( + r.unwrap(), + Flags { + subcommand: DenoSubcommand::Run(RunFlags { + script: "foo.js".to_string(), + }), + inspect_wait: Some("127.0.0.1:3567".parse().unwrap()), + ..Flags::default() + } + ); + } + #[test] fn compile() { let r = flags_from_vec(svec![ diff --git a/cli/args/mod.rs b/cli/args/mod.rs index 3f3f53d183ab87..4b9d92e96af23c 100644 --- a/cli/args/mod.rs +++ b/cli/args/mod.rs @@ -340,7 +340,11 @@ impl CliOptions { } pub fn resolve_inspector_server(&self) -> Option { - let maybe_inspect_host = self.flags.inspect.or(self.flags.inspect_brk); + let maybe_inspect_host = self + .flags + .inspect + .or(self.flags.inspect_brk) + .or(self.flags.inspect_wait); maybe_inspect_host .map(|host| InspectorServer::new(host, version::get_user_agent())) } @@ -457,13 +461,19 @@ impl CliOptions { /// If the --inspect or --inspect-brk flags are used. pub fn is_inspecting(&self) -> bool { - self.flags.inspect.is_some() || self.flags.inspect_brk.is_some() + self.flags.inspect.is_some() + || self.flags.inspect_brk.is_some() + || self.flags.inspect_wait.is_some() } pub fn inspect_brk(&self) -> Option { self.flags.inspect_brk } + pub fn inspect_wait(&self) -> Option { + self.flags.inspect_wait + } + pub fn log_level(&self) -> Option { self.flags.log_level } diff --git a/cli/standalone.rs b/cli/standalone.rs index 2742f9bbdba5e8..d4bf8edb3fbcdf 100644 --- a/cli/standalone.rs +++ b/cli/standalone.rs @@ -299,6 +299,7 @@ pub async fn run( web_worker_pre_execute_module_cb: web_worker_cb, maybe_inspector_server: None, should_break_on_first_statement: false, + should_wait_for_inspector_session: false, module_loader, npm_resolver: None, // not currently supported get_error_class_fn: Some(&get_error_class_name), diff --git a/cli/tests/inspector_tests.rs b/cli/tests/inspector_tests.rs index 0cf8cc3bbe438f..7044469e4118bd 100644 --- a/cli/tests/inspector_tests.rs +++ b/cli/tests/inspector_tests.rs @@ -13,6 +13,7 @@ use deno_runtime::deno_websocket::tokio_tungstenite::tungstenite; use std::io::BufRead; use std::process::Child; use test_util as util; +use test_util::TempDir; use tokio::net::TcpStream; use util::http_server; @@ -1319,4 +1320,59 @@ mod inspector { assert_eq!(tester.child.wait().unwrap().code(), Some(1)); } + + #[tokio::test] + async fn inspector_wait() { + let script = util::testdata_path().join("inspector/inspect_wait.js"); + let temp_dir = TempDir::new(); + + let child = util::deno_cmd() + .current_dir(temp_dir.path()) + .arg("run") + .arg("--quiet") + .arg("-A") + .arg(inspect_flag_with_unique_port("--inspect-wait")) + .arg(script) + .stdout(std::process::Stdio::piped()) + .stderr(std::process::Stdio::piped()) + .spawn() + .unwrap(); + + tokio::time::sleep(tokio::time::Duration::from_millis(300)).await; + assert!(!temp_dir.path().join("hello.txt").exists()); + + let mut tester = InspectorTester::create(child, ignore_script_parsed).await; + + tester.assert_stderr_for_inspect_brk(); + tester + .send_many(&[ + json!({"id":1,"method":"Runtime.enable"}), + json!({"id":2,"method":"Debugger.enable"}), + ]) + .await; + tester.assert_received_messages( + &[ + r#"{"id":1,"result":{}}"#, + r#"{"id":2,"result":{"debuggerId":"#, + ], + &[ + r#"{"method":"Runtime.executionContextCreated","params":{"context":{"id":1,"#, + ], + ) + .await; + // TODO(bartlomieju): ideally this shouldn't be needed, but currently there's + // no way to express that in inspector code. Most clients always send this + // message anyway. + tester + .send(json!({"id":3,"method":"Runtime.runIfWaitingForDebugger"})) + .await; + tester + .assert_received_messages(&[r#"{"id":3,"result":{}}"#], &[]) + .await; + assert_eq!(&tester.stderr_line(), "Debugger session started."); + tokio::time::sleep(tokio::time::Duration::from_millis(300)).await; + assert_eq!(&tester.stderr_line(), "did run"); + assert!(temp_dir.path().join("hello.txt").exists()); + tester.child.kill().unwrap(); + } } diff --git a/cli/tests/testdata/inspector/inspect_wait.js b/cli/tests/testdata/inspector/inspect_wait.js new file mode 100644 index 00000000000000..e2b10199ca953e --- /dev/null +++ b/cli/tests/testdata/inspector/inspect_wait.js @@ -0,0 +1,2 @@ +Deno.writeTextFileSync("./hello.txt", "hello world"); +console.error("did run"); diff --git a/cli/worker.rs b/cli/worker.rs index 27cefc1b8d1e15..03d0728e68532e 100644 --- a/cli/worker.rs +++ b/cli/worker.rs @@ -472,7 +472,6 @@ async fn create_main_worker_internal( let module_loader = CliModuleLoader::new(ps.clone()); let maybe_inspector_server = ps.maybe_inspector_server.clone(); - let should_break_on_first_statement = ps.options.inspect_brk().is_some(); let create_web_worker_cb = create_web_worker_callback(ps.clone(), stdio.clone()); @@ -533,7 +532,8 @@ async fn create_main_worker_internal( web_worker_preload_module_cb, web_worker_pre_execute_module_cb, maybe_inspector_server, - should_break_on_first_statement, + should_break_on_first_statement: ps.options.inspect_brk().is_some(), + should_wait_for_inspector_session: ps.options.inspect_wait().is_some(), module_loader, npm_resolver: Some(Rc::new(ps.npm_resolver.clone())), get_error_class_fn: Some(&errors::get_error_class_name), @@ -755,6 +755,7 @@ mod tests { create_web_worker_cb: Arc::new(|_| unreachable!()), maybe_inspector_server: None, should_break_on_first_statement: false, + should_wait_for_inspector_session: false, module_loader: Rc::new(FsModuleLoader), npm_resolver: None, get_error_class_fn: None, diff --git a/core/inspector.rs b/core/inspector.rs index 8a913609195377..b9a5908ed58719 100644 --- a/core/inspector.rs +++ b/core/inspector.rs @@ -347,6 +347,23 @@ impl JsRuntimeInspector { } } + /// This function blocks the thread until at least one inspector client has + /// established a websocket connection. + pub fn wait_for_session(&mut self) { + loop { + match self.sessions.get_mut().established.iter_mut().next() { + Some(_session) => { + self.flags.get_mut().waiting_for_session = false; + break; + } + None => { + self.flags.get_mut().waiting_for_session = true; + let _ = self.poll_sessions(None).unwrap(); + } + }; + } + } + /// This function blocks the thread until at least one inspector client has /// established a websocket connection. /// diff --git a/runtime/examples/hello_runtime.rs b/runtime/examples/hello_runtime.rs index d71cc467ae90c9..371ecf63faa806 100644 --- a/runtime/examples/hello_runtime.rs +++ b/runtime/examples/hello_runtime.rs @@ -54,6 +54,7 @@ async fn main() -> Result<(), AnyError> { create_web_worker_cb, maybe_inspector_server: None, should_break_on_first_statement: false, + should_wait_for_inspector_session: false, module_loader, npm_resolver: None, get_error_class_fn: Some(&get_error_class_name), diff --git a/runtime/inspector_server.rs b/runtime/inspector_server.rs index f29eec2e2243df..0423bbdce7ae99 100644 --- a/runtime/inspector_server.rs +++ b/runtime/inspector_server.rs @@ -67,7 +67,7 @@ impl InspectorServer { &self, module_url: String, js_runtime: &mut JsRuntime, - should_break_on_first_statement: bool, + wait_for_session: bool, ) { let inspector_rc = js_runtime.inspector(); let mut inspector = inspector_rc.borrow_mut(); @@ -78,7 +78,7 @@ impl InspectorServer { session_sender, deregister_rx, module_url, - should_break_on_first_statement, + wait_for_session, ); self.register_inspector_tx.unbounded_send(info).unwrap(); } @@ -233,7 +233,7 @@ async fn server( info.get_websocket_debugger_url() ); eprintln!("Visit chrome://inspect to connect to the debugger."); - if info.should_break_on_first_statement { + if info.wait_for_session { eprintln!("Deno is waiting for debugger to connect."); } if inspector_map.borrow_mut().insert(info.uuid, info).is_some() { @@ -370,7 +370,7 @@ pub struct InspectorInfo { pub new_session_tx: UnboundedSender, pub deregister_rx: oneshot::Receiver<()>, pub url: String, - pub should_break_on_first_statement: bool, + pub wait_for_session: bool, } impl InspectorInfo { @@ -379,7 +379,7 @@ impl InspectorInfo { new_session_tx: mpsc::UnboundedSender, deregister_rx: oneshot::Receiver<()>, url: String, - should_break_on_first_statement: bool, + wait_for_session: bool, ) -> Self { Self { host, @@ -388,7 +388,7 @@ impl InspectorInfo { new_session_tx, deregister_rx, url, - should_break_on_first_statement, + wait_for_session, } } diff --git a/runtime/worker.rs b/runtime/worker.rs index 01498e1f620828..b184ac3fa851c4 100644 --- a/runtime/worker.rs +++ b/runtime/worker.rs @@ -62,6 +62,7 @@ impl ExitCode { pub struct MainWorker { pub js_runtime: JsRuntime, should_break_on_first_statement: bool, + should_wait_for_inspector_session: bool, exit_code: ExitCode, } @@ -81,7 +82,13 @@ pub struct WorkerOptions { pub format_js_error_fn: Option>, pub source_map_getter: Option>, pub maybe_inspector_server: Option>, + // If true, the worker will wait for inspector session and break on first + // statement of user code. Takes higher precedence than + // `should_wait_for_inspector_session`. pub should_break_on_first_statement: bool, + // If true, the worker will wait for inspector session before executing + // user code. + pub should_wait_for_inspector_session: bool, pub get_error_class_fn: Option, pub cache_storage_dir: Option, pub origin_storage_dir: Option, @@ -108,6 +115,7 @@ impl Default for WorkerOptions { seed: None, unsafely_ignore_certificate_errors: Default::default(), should_break_on_first_statement: Default::default(), + should_wait_for_inspector_session: Default::default(), compiled_wasm_module_store: Default::default(), shared_array_buffer_store: Default::default(), maybe_inspector_server: Default::default(), @@ -248,7 +256,8 @@ impl MainWorker { server.register_inspector( main_module.to_string(), &mut js_runtime, - options.should_break_on_first_statement, + options.should_break_on_first_statement + || options.should_wait_for_inspector_session, ); // Put inspector handle into the op state so we can put a breakpoint when @@ -261,6 +270,8 @@ impl MainWorker { Self { js_runtime, should_break_on_first_statement: options.should_break_on_first_statement, + should_wait_for_inspector_session: options + .should_wait_for_inspector_session, exit_code, } } @@ -355,7 +366,9 @@ impl MainWorker { .js_runtime .inspector() .borrow_mut() - .wait_for_session_and_break_on_next_statement() + .wait_for_session_and_break_on_next_statement(); + } else if self.should_wait_for_inspector_session { + self.js_runtime.inspector().borrow_mut().wait_for_session(); } }