diff options
author | Ryan Dahl <ry@tinyclouds.org> | 2020-03-27 16:09:51 -0400 |
---|---|---|
committer | GitHub <noreply@github.com> | 2020-03-27 16:09:51 -0400 |
commit | 2874664e9131616b71dd0d7d23750245b023833f (patch) | |
tree | 6782161aa151d00f930bc0157e95b14d895347f0 /cli | |
parent | 8bcdb422e387a88075126d80e1612a30f5a7d89e (diff) |
feat: Support Inspector / Chrome Devtools (#4484)
This is a first pass implementation which is still missing several important
features:
- support for --inspect-brk (#4503)
- support for source maps (#4501)
- support for piping console.log to devtools console (#4502)
Co-authored-by: Bert Belder <bertbelder@gmail.com>
Co-authored-by: Matt Harrison <mt.harrison86@gmail.com>
Co-authored-by: Bartek IwaĆczuk <biwanczuk@gmail.com>
Diffstat (limited to 'cli')
-rw-r--r-- | cli/Cargo.toml | 5 | ||||
-rw-r--r-- | cli/flags.rs | 88 | ||||
-rw-r--r-- | cli/global_state.rs | 11 | ||||
-rw-r--r-- | cli/inspector.rs | 549 | ||||
-rw-r--r-- | cli/lib.rs | 1 | ||||
-rw-r--r-- | cli/ops/fs.rs | 8 | ||||
-rw-r--r-- | cli/tests/inspector1.js | 3 | ||||
-rw-r--r-- | cli/tests/integration_tests.rs | 105 | ||||
-rw-r--r-- | cli/worker.rs | 24 |
9 files changed, 787 insertions, 7 deletions
diff --git a/cli/Cargo.toml b/cli/Cargo.toml index 1a7a8db8c..aa4fd15de 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -61,7 +61,10 @@ utime = "0.2.1" webpki = "0.21.2" webpki-roots = "0.19.0" walkdir = "2.3.1" +warp = "0.2.2" semver-parser = "0.9.0" +uuid = { version = "0.8", features = ["v4"] } + [target.'cfg(windows)'.dependencies] winapi = "0.3.8" @@ -72,6 +75,8 @@ nix = "0.14" # rustyline depends on 0.14, to avoid duplicates we do too. [dev-dependencies] os_pipe = "0.9.1" +# Used for testing inspector. Keep in-sync with warp. +tokio-tungstenite = { version = "0.10", features = ["connect"] } [target.'cfg(unix)'.dev-dependencies] pty = "0.2.2" diff --git a/cli/flags.rs b/cli/flags.rs index 475172f0a..4f55d69ef 100644 --- a/cli/flags.rs +++ b/cli/flags.rs @@ -101,6 +101,8 @@ pub struct Flags { pub no_prompts: bool, pub no_remote: bool, pub cached_only: bool, + pub inspect: Option<String>, + pub inspect_brk: Option<String>, pub seed: Option<u64>, pub v8_flags: Option<Vec<String>>, @@ -474,6 +476,7 @@ fn run_test_args_parse(flags: &mut Flags, matches: &clap::ArgMatches) { no_remote_arg_parse(flags, matches); permission_args_parse(flags, matches); ca_file_arg_parse(flags, matches); + inspect_arg_parse(flags, matches); if matches.is_present("cached-only") { flags.cached_only = true; @@ -825,7 +828,7 @@ fn permission_args<'a, 'b>(app: App<'a, 'b>) -> App<'a, 'b> { } fn run_test_args<'a, 'b>(app: App<'a, 'b>) -> App<'a, 'b> { - permission_args(app) + permission_args(inspect_args(app)) .arg(importmap_arg()) .arg(reload_arg()) .arg(config_arg()) @@ -956,6 +959,54 @@ fn ca_file_arg_parse(flags: &mut Flags, matches: &clap::ArgMatches) { flags.ca_file = matches.value_of("cert").map(ToOwned::to_owned); } +fn inspect_args<'a, 'b>(app: App<'a, 'b>) -> App<'a, 'b> { + app + .arg( + Arg::with_name("inspect") + .long("inspect") + .value_name("HOST:PORT") + .help("activate inspector on host:port (default: 127.0.0.1:9229)") + .min_values(0) + .max_values(1) + .require_equals(true) + .takes_value(true), + ) + .arg( + Arg::with_name("inspect-brk") + .long("inspect-brk") + .value_name("HOST:PORT") + .help( + "activate inspector on host:port and break at start of user script", + ) + .min_values(0) + .max_values(1) + .require_equals(true) + .takes_value(true), + ) +} + +fn inspect_arg_parse(flags: &mut Flags, matches: &clap::ArgMatches) { + const DEFAULT: &str = "127.0.0.1:9229"; + flags.inspect = if matches.is_present("inspect") { + if let Some(host) = matches.value_of("inspect") { + Some(host.to_string()) + } else { + Some(DEFAULT.to_string()) + } + } else { + None + }; + flags.inspect_brk = if matches.is_present("inspect-brk") { + if let Some(host) = matches.value_of("inspect-brk") { + Some(host.to_string()) + } else { + Some(DEFAULT.to_string()) + } + } else { + None + }; +} + fn reload_arg<'a, 'b>() -> Arg<'a, 'b> { Arg::with_name("reload") .short("r") @@ -2327,3 +2378,38 @@ fn repl_with_cafile() { } ); } + +#[test] +fn inspect_default_host() { + let r = flags_from_vec_safe(svec!["deno", "run", "--inspect", "foo.js"]); + assert_eq!( + r.unwrap(), + Flags { + subcommand: DenoSubcommand::Run { + script: "foo.js".to_string(), + }, + inspect: Some("127.0.0.1:9229".to_string()), + ..Flags::default() + } + ); +} + +#[test] +fn inspect_custom_host() { + let r = flags_from_vec_safe(svec![ + "deno", + "run", + "--inspect=deno.land:80", + "foo.js" + ]); + assert_eq!( + r.unwrap(), + Flags { + subcommand: DenoSubcommand::Run { + script: "foo.js".to_string(), + }, + inspect: Some("deno.land:80".to_string()), + ..Flags::default() + } + ); +} diff --git a/cli/global_state.rs b/cli/global_state.rs index 45a31406c..001c3f55f 100644 --- a/cli/global_state.rs +++ b/cli/global_state.rs @@ -9,6 +9,7 @@ use crate::deno_dir; use crate::file_fetcher::SourceFileFetcher; use crate::flags; use crate::http_cache; +use crate::inspector::InspectorServer; use crate::lockfile::Lockfile; use crate::msg; use crate::permissions::DenoPermissions; @@ -42,6 +43,7 @@ pub struct GlobalStateInner { pub wasm_compiler: WasmCompiler, pub lockfile: Option<Mutex<Lockfile>>, pub compiler_starts: AtomicUsize, + pub inspector_server: Option<InspectorServer>, compile_lock: AsyncMutex<()>, } @@ -82,7 +84,16 @@ impl GlobalState { None }; + let inspector_server = if let Some(ref host) = flags.inspect { + Some(InspectorServer::new(host, false)) + } else if let Some(ref host) = flags.inspect_brk { + Some(InspectorServer::new(host, true)) + } else { + None + }; + let inner = GlobalStateInner { + inspector_server, dir, permissions: DenoPermissions::from_flags(&flags), flags, diff --git a/cli/inspector.rs b/cli/inspector.rs new file mode 100644 index 000000000..a30e5c0d7 --- /dev/null +++ b/cli/inspector.rs @@ -0,0 +1,549 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. + +// The documentation for the inspector API is sparse, but these are helpful: +// https://chromedevtools.github.io/devtools-protocol/ +// https://hyperandroid.com/2020/02/12/v8-inspector-from-an-embedder-standpoint/ + +use deno_core::v8; +use futures; +use futures::executor; +use futures::future; +use futures::FutureExt; +use futures::SinkExt; +use futures::StreamExt; +use std::collections::HashMap; +use std::ffi::c_void; +use std::future::Future; +use std::mem::MaybeUninit; +use std::net::SocketAddrV4; +use std::pin::Pin; +use std::ptr; +use std::sync::atomic::AtomicBool; +use std::sync::atomic::Ordering; +use std::sync::Arc; +use std::task::Context; +use std::task::Poll; +use tokio; +use tokio::sync::mpsc; +use tokio::sync::mpsc::error::TryRecvError; +use uuid::Uuid; +use warp; +use warp::filters::ws; +use warp::Filter; + +const CONTEXT_GROUP_ID: i32 = 1; + +/// Owned by GloalState, this channel end can be used by any isolate thread +/// to register it's inspector with the inspector server. +type ServerMsgTx = mpsc::UnboundedSender<ServerMsg>; +/// Owned by the inspector server thread, used to to receive information about +/// new isolates. +type ServerMsgRx = mpsc::UnboundedReceiver<ServerMsg>; +/// These messages can be sent from any thread to the server thread. +enum ServerMsg { + AddInspector(InspectorInfo), +} + +/// Owned by the web socket server. Relays incoming websocket connections and +/// messages to the isolate/inspector thread. +type FrontendToInspectorTx = mpsc::UnboundedSender<FrontendToInspectorMsg>; +/// Owned by the isolate/worker. Receives incoming websocket connections and +/// messages from the inspector server thread. +type FrontendToInspectorRx = mpsc::UnboundedReceiver<FrontendToInspectorMsg>; +/// Messages sent over the FrontendToInspectorTx/FrontendToInspectorRx channel. +pub enum FrontendToInspectorMsg { + WsConnection { + session_uuid: Uuid, + session_to_frontend_tx: SessionToFrontendTx, + }, + WsIncoming { + session_uuid: Uuid, + msg: ws::Message, + }, +} + +/// Owned by the deno inspector session, used to forward messages from the +/// inspector channel on the isolate thread to the websocket that is owned by +/// the inspector server. +type SessionToFrontendTx = mpsc::UnboundedSender<ws::Message>; +/// Owned by the inspector server. Messages arriving on this channel, coming +/// from the inspector session on the isolate thread are forwarded over the +/// websocket to the devtools frontend. +type SessionToFrontendRx = mpsc::UnboundedReceiver<ws::Message>; + +/// Stored in a UUID hashmap, used by WS server. Clonable. +#[derive(Clone)] +struct InspectorInfo { + uuid: Uuid, + frontend_to_inspector_tx: FrontendToInspectorTx, + inspector_handle: DenoInspectorHandle, +} + +/// Owned by GlobalState. +pub struct InspectorServer { + address: SocketAddrV4, + thread_handle: Option<std::thread::JoinHandle<()>>, + server_msg_tx: Option<ServerMsgTx>, +} + +impl InspectorServer { + pub fn new(host: &str, brk: bool) -> Self { + if brk { + todo!("--inspect-brk not yet supported"); + } + let address = host.parse::<SocketAddrV4>().unwrap(); + let (server_msg_tx, server_msg_rx) = mpsc::unbounded_channel::<ServerMsg>(); + let thread_handle = std::thread::spawn(move || { + crate::tokio_util::run_basic(server(address, server_msg_rx)); + }); + Self { + address, + thread_handle: Some(thread_handle), + server_msg_tx: Some(server_msg_tx), + } + } + + /// Each worker/isolate to be debugged should call this exactly one. + /// Called from worker's thread + pub fn add_inspector( + &self, + isolate: &mut deno_core::Isolate, + ) -> Box<DenoInspector> { + let deno_core::Isolate { + v8_isolate, + global_context, + .. + } = isolate; + let v8_isolate = v8_isolate.as_mut().unwrap(); + + let mut hs = v8::HandleScope::new(v8_isolate); + let scope = hs.enter(); + let context = global_context.get(scope).unwrap(); + + let server_msg_tx = self.server_msg_tx.as_ref().unwrap().clone(); + let address = self.address; + let (frontend_to_inspector_tx, frontend_to_inspector_rx) = + mpsc::unbounded_channel::<FrontendToInspectorMsg>(); + let uuid = Uuid::new_v4(); + + let inspector = crate::inspector::DenoInspector::new( + scope, + context, + frontend_to_inspector_rx, + ); + + info!( + "Debugger listening on {}", + websocket_debugger_url(address, &uuid) + ); + + server_msg_tx + .send(ServerMsg::AddInspector(InspectorInfo { + uuid, + frontend_to_inspector_tx, + inspector_handle: DenoInspectorHandle::new( + &inspector, + v8_isolate.thread_safe_handle(), + ), + })) + .unwrap_or_else(|_| { + panic!("sending message to inspector server thread failed"); + }); + + inspector + } +} + +impl Drop for InspectorServer { + fn drop(&mut self) { + self.server_msg_tx.take(); + self.thread_handle.take().unwrap().join().unwrap(); + panic!("TODO: this drop is never called"); + } +} + +fn websocket_debugger_url(address: SocketAddrV4, uuid: &Uuid) -> String { + format!("ws://{}:{}/ws/{}", address.ip(), address.port(), uuid) +} + +async fn server(address: SocketAddrV4, mut server_msg_rx: ServerMsgRx) { + let inspector_map = HashMap::<Uuid, InspectorInfo>::new(); + let inspector_map = Arc::new(std::sync::Mutex::new(inspector_map)); + + let inspector_map_ = inspector_map.clone(); + let msg_handler = async move { + while let Some(msg) = server_msg_rx.next().await { + match msg { + ServerMsg::AddInspector(inspector_info) => { + let existing = inspector_map_ + .lock() + .unwrap() + .insert(inspector_info.uuid, inspector_info); + if existing.is_some() { + panic!("UUID already in map"); + } + } + }; + } + }; + + let inspector_map_ = inspector_map.clone(); + let websocket = warp::path("ws") + .and(warp::path::param()) + .and(warp::ws()) + .map(move |uuid: String, ws: warp::ws::Ws| { + let inspector_map__ = inspector_map_.clone(); + ws.on_upgrade(move |socket| async move { + let inspector_info = { + if let Ok(uuid) = Uuid::parse_str(&uuid) { + let g = inspector_map__.lock().unwrap(); + if let Some(inspector_info) = g.get(&uuid) { + inspector_info.clone() + } else { + return; + } + } else { + return; + } + }; + + // send a message back so register_worker can return... + let (mut ws_tx, mut ws_rx) = socket.split(); + + let (session_to_frontend_tx, mut session_to_frontend_rx): ( + SessionToFrontendTx, + SessionToFrontendRx, + ) = mpsc::unbounded_channel(); + + // Not to be confused with the WS's uuid... + let session_uuid = Uuid::new_v4(); + + inspector_info + .frontend_to_inspector_tx + .send(FrontendToInspectorMsg::WsConnection { + session_to_frontend_tx, + session_uuid, + }) + .unwrap_or_else(|_| { + panic!("sending message to frontend_to_inspector_tx failed"); + }); + + inspector_info.inspector_handle.interrupt(); + + let pump_to_inspector = async { + while let Some(Ok(msg)) = ws_rx.next().await { + inspector_info + .frontend_to_inspector_tx + .send(FrontendToInspectorMsg::WsIncoming { msg, session_uuid }) + .unwrap_or_else(|_| { + panic!("sending message to frontend_to_inspector_tx failed"); + }); + + inspector_info.inspector_handle.interrupt(); + } + }; + + let pump_from_session = async { + while let Some(msg) = session_to_frontend_rx.next().await { + ws_tx.send(msg).await.ok(); + } + }; + + future::join(pump_to_inspector, pump_from_session).await; + }) + }); + + let inspector_map_ = inspector_map.clone(); + let json_list = + warp::path("json") + .map(move || { + let g = inspector_map_.lock().unwrap(); + let json_values: Vec<serde_json::Value> = g.iter().map(|(uuid, _)| { + let url = websocket_debugger_url(address, uuid); + json!({ + "description": "deno", + "devtoolsFrontendUrl": format!("chrome-devtools://devtools/bundled/js_app.html?experiments=true&v8only=true&ws={}", url), + "faviconUrl": "https://deno.land/favicon.ico", + "id": uuid.to_string(), + "title": format!("deno[{}]", std::process::id()), + "type": "deno", + "url": "file://", + "webSocketDebuggerUrl": url, + }) + }).collect(); + warp::reply::json(&json!(json_values)) + }); + + let version = warp::path!("json" / "version").map(|| { + warp::reply::json(&json!({ + "Browser": format!("Deno/{}", crate::version::DENO), + "Protocol-Version": "1.3", + "V8-Version": crate::version::v8(), + })) + }); + + let routes = websocket.or(version).or(json_list); + let web_handler = warp::serve(routes).bind(address); + + future::join(msg_handler, web_handler).await; +} + +pub struct DenoInspector { + client: v8::inspector::V8InspectorClientBase, + inspector: v8::UniqueRef<v8::inspector::V8Inspector>, + pub sessions: HashMap<Uuid, Box<DenoInspectorSession>>, + frontend_to_inspector_rx: FrontendToInspectorRx, + paused: bool, + interrupted: Arc<AtomicBool>, +} + +impl DenoInspector { + pub fn new<P>( + scope: &mut P, + context: v8::Local<v8::Context>, + frontend_to_inspector_rx: FrontendToInspectorRx, + ) -> Box<Self> + where + P: v8::InIsolate, + { + let mut deno_inspector = new_box_with(|address| Self { + client: v8::inspector::V8InspectorClientBase::new::<Self>(), + // TODO(piscisaureus): V8Inspector::create() should require that + // the 'client' argument cannot move. + inspector: v8::inspector::V8Inspector::create(scope, unsafe { + &mut *address + }), + sessions: HashMap::new(), + frontend_to_inspector_rx, + paused: false, + interrupted: Arc::new(AtomicBool::new(false)), + }); + + let empty_view = v8::inspector::StringView::empty(); + deno_inspector.inspector.context_created( + context, + CONTEXT_GROUP_ID, + &empty_view, + ); + + deno_inspector + } + + pub fn connect( + &mut self, + session_uuid: Uuid, + session_to_frontend_tx: SessionToFrontendTx, + ) { + let session = + DenoInspectorSession::new(&mut self.inspector, session_to_frontend_tx); + self.sessions.insert(session_uuid, session); + } + + fn dispatch_frontend_to_inspector_msg( + &mut self, + msg: FrontendToInspectorMsg, + ) { + match msg { + FrontendToInspectorMsg::WsConnection { + session_uuid, + session_to_frontend_tx, + } => { + self.connect(session_uuid, session_to_frontend_tx); + } + FrontendToInspectorMsg::WsIncoming { session_uuid, msg } => { + if let Some(deno_session) = self.sessions.get_mut(&session_uuid) { + deno_session.dispatch_protocol_message(msg) + } else { + info!("Unknown inspector session {}. msg {:?}", session_uuid, msg); + } + } + }; + } + + extern "C" fn poll_interrupt( + _isolate: &mut v8::Isolate, + self_ptr: *mut c_void, + ) { + let self_ = unsafe { &mut *(self_ptr as *mut Self) }; + let _ = self_.poll_without_waker(); + } + + fn poll_without_waker(&mut self) -> Poll<<Self as Future>::Output> { + loop { + match self.frontend_to_inspector_rx.try_recv() { + Ok(msg) => self.dispatch_frontend_to_inspector_msg(msg), + Err(TryRecvError::Closed) => break Poll::Ready(()), + Err(TryRecvError::Empty) + if self.interrupted.swap(false, Ordering::AcqRel) => {} + Err(TryRecvError::Empty) => break Poll::Pending, + } + } + } +} + +/// DenoInspector implements a Future so that it can poll for incoming messages +/// from the WebSocket server. Since a Worker ownes a DenoInspector, and because +/// a Worker is a Future too, Worker::poll will call this. +impl Future for DenoInspector { + type Output = (); + + fn poll(self: Pin<&mut Self>, cx: &mut Context) -> Poll<Self::Output> { + let self_ = self.get_mut(); + loop { + match self_.frontend_to_inspector_rx.poll_recv(cx) { + Poll::Ready(Some(msg)) => self_.dispatch_frontend_to_inspector_msg(msg), + Poll::Ready(None) => break Poll::Ready(()), + Poll::Pending if self_.interrupted.swap(false, Ordering::AcqRel) => {} + Poll::Pending => break Poll::Pending, + } + } + } +} + +impl v8::inspector::V8InspectorClientImpl for DenoInspector { + fn base(&self) -> &v8::inspector::V8InspectorClientBase { + &self.client + } + + fn base_mut(&mut self) -> &mut v8::inspector::V8InspectorClientBase { + &mut self.client + } + + fn run_message_loop_on_pause(&mut self, context_group_id: i32) { + assert_eq!(context_group_id, CONTEXT_GROUP_ID); + assert!(!self.paused); + self.paused = true; + + // Creating a new executor and calling block_on generally causes a panic. + // In this case it works because the outer executor is provided by tokio + // and the one created here comes from the 'futures' crate, and they don't + // see each other. + let dispatch_messages_while_paused = + future::poll_fn(|cx| match self.poll_unpin(cx) { + Poll::Pending if self.paused => Poll::Pending, + _ => Poll::Ready(()), + }); + executor::block_on(dispatch_messages_while_paused); + } + + fn quit_message_loop_on_pause(&mut self) { + self.paused = false; + } + + fn run_if_waiting_for_debugger(&mut self, context_group_id: i32) { + assert_eq!(context_group_id, CONTEXT_GROUP_ID); + } +} + +#[derive(Clone)] +struct DenoInspectorHandle { + deno_inspector_ptr: *mut c_void, + isolate_handle: v8::IsolateHandle, + interrupted: Arc<AtomicBool>, +} + +impl DenoInspectorHandle { + pub fn new( + deno_inspector: &DenoInspector, + isolate_handle: v8::IsolateHandle, + ) -> Self { + Self { + deno_inspector_ptr: deno_inspector as *const DenoInspector + as *const c_void as *mut c_void, + isolate_handle, + interrupted: deno_inspector.interrupted.clone(), + } + } + + pub fn interrupt(&self) { + if !self.interrupted.swap(true, Ordering::AcqRel) { + self.isolate_handle.request_interrupt( + DenoInspector::poll_interrupt, + self.deno_inspector_ptr, + ); + } + } +} + +unsafe impl Send for DenoInspectorHandle {} +unsafe impl Sync for DenoInspectorHandle {} + +/// sub-class of v8::inspector::Channel +pub struct DenoInspectorSession { + channel: v8::inspector::ChannelBase, + session: v8::UniqueRef<v8::inspector::V8InspectorSession>, + session_to_frontend_tx: SessionToFrontendTx, +} + +impl DenoInspectorSession { + pub fn new( + inspector: &mut v8::inspector::V8Inspector, + session_to_frontend_tx: SessionToFrontendTx, + ) -> Box<Self> { + new_box_with(|address| { + let empty_view = v8::inspector::StringView::empty(); + Self { + channel: v8::inspector::ChannelBase::new::<Self>(), + session: inspector.connect( + CONTEXT_GROUP_ID, + // Todo(piscisaureus): V8Inspector::connect() should require that + // the 'channel' argument cannot move. + unsafe { &mut *address }, + &empty_view, + ), + session_to_frontend_tx, + } + }) + } + + pub fn dispatch_protocol_message(&mut self, ws_msg: ws::Message) { + let bytes = ws_msg.as_bytes(); + let string_view = v8::inspector::StringView::from(bytes); + self.session.dispatch_protocol_message(&string_view); + } +} + +impl v8::inspector::ChannelImpl for DenoInspectorSession { + fn base(&self) -> &v8::inspector::ChannelBase { + &self.channel + } + + fn base_mut(&mut self) -> &mut v8::inspector::ChannelBase { + &mut self.channel + } + + fn send_response( + &mut self, + _call_id: i32, + message: v8::UniquePtr<v8::inspector::StringBuffer>, + ) { + let ws_msg = v8_to_ws_msg(message); + self.session_to_frontend_tx.send(ws_msg).unwrap(); + } + + fn send_notification( + &mut self, + message: v8::UniquePtr<v8::inspector::StringBuffer>, + ) { + let ws_msg = v8_to_ws_msg(message); + self.session_to_frontend_tx.send(ws_msg).unwrap(); + } + + fn flush_protocol_notifications(&mut self) {} +} + +// TODO impl From or Into +fn v8_to_ws_msg( + message: v8::UniquePtr<v8::inspector::StringBuffer>, +) -> ws::Message { + let mut x = message.unwrap(); + let s = x.string().to_string(); + ws::Message::text(s) +} + +fn new_box_with<T>(new_fn: impl FnOnce(*mut T) -> T) -> Box<T> { + let b = Box::new(MaybeUninit::<T>::uninit()); + let p = Box::into_raw(b) as *mut T; + unsafe { ptr::write(p, new_fn(p)) }; + unsafe { Box::from_raw(p) } +} diff --git a/cli/lib.rs b/cli/lib.rs index ba5152bd6..7b5b56ba2 100644 --- a/cli/lib.rs +++ b/cli/lib.rs @@ -37,6 +37,7 @@ mod global_timer; pub mod http_cache; mod http_util; mod import_map; +mod inspector; pub mod installer; mod js; mod lockfile; diff --git a/cli/ops/fs.rs b/cli/ops/fs.rs index 7e526d71e..493bd31e4 100644 --- a/cli/ops/fs.rs +++ b/cli/ops/fs.rs @@ -256,7 +256,7 @@ fn op_umask( #[cfg(not(unix))] { let _ = args.mask; // avoid unused warning. - return Err(OpError::not_implemented()); + Err(OpError::not_implemented()) } #[cfg(unix)] { @@ -360,7 +360,7 @@ fn op_chmod( { // Still check file/dir exists on Windows let _metadata = std::fs::metadata(&path)?; - return Err(OpError::not_implemented()); + Err(OpError::not_implemented()) } }) } @@ -400,7 +400,7 @@ fn op_chown( { // Still check file/dir exists on Windows let _metadata = std::fs::metadata(&path)?; - return Err(OpError::not_implemented()); + Err(OpError::not_implemented()) } }) } @@ -731,7 +731,7 @@ fn op_symlink( // Unlike with chmod/chown, here we don't // require `oldpath` to exist on Windows let _ = oldpath; // avoid unused warning - return Err(OpError::not_implemented()); + Err(OpError::not_implemented()) } }) } diff --git a/cli/tests/inspector1.js b/cli/tests/inspector1.js new file mode 100644 index 000000000..5cb059def --- /dev/null +++ b/cli/tests/inspector1.js @@ -0,0 +1,3 @@ +setInterval(() => { + console.log("hello"); +}, 1000); diff --git a/cli/tests/integration_tests.rs b/cli/tests/integration_tests.rs index f8651869b..02cccf9cf 100644 --- a/cli/tests/integration_tests.rs +++ b/cli/tests/integration_tests.rs @@ -1967,6 +1967,111 @@ fn test_permissions_net_listen_allow_localhost() { assert!(!err.contains(util::PERMISSION_DENIED_PATTERN)); } +#[cfg(not(target_os = "linux"))] // TODO(ry) broken on github actions. +fn extract_ws_url_from_stderr( + stderr: &mut std::process::ChildStderr, +) -> url::Url { + use std::io::BufRead; + let mut stderr = std::io::BufReader::new(stderr); + let mut stderr_first_line = String::from(""); + let _ = stderr.read_line(&mut stderr_first_line).unwrap(); + assert!(stderr_first_line.starts_with("Debugger listening on ")); + let v: Vec<_> = stderr_first_line.match_indices("ws:").collect(); + assert_eq!(v.len(), 1); + let ws_url_index = v[0].0; + let ws_url = &stderr_first_line[ws_url_index..]; + url::Url::parse(ws_url).unwrap() +} + +#[cfg(not(target_os = "linux"))] // TODO(ry) broken on github actions. +#[tokio::test] +async fn inspector_connect() { + let script = deno::test_util::root_path() + .join("cli") + .join("tests") + .join("inspector1.js"); + let mut child = util::deno_cmd() + .arg("run") + // Warning: each inspector test should be on its own port to avoid + // conflicting with another inspector test. + .arg("--inspect=127.0.0.1:9229") + .arg(script) + .stderr(std::process::Stdio::piped()) + .spawn() + .unwrap(); + let ws_url = extract_ws_url_from_stderr(child.stderr.as_mut().unwrap()); + println!("ws_url {}", ws_url); + // We use tokio_tungstenite as a websocket client because warp (which is + // a dependency of Deno) uses it. + let (_socket, response) = tokio_tungstenite::connect_async(ws_url) + .await + .expect("Can't connect"); + assert_eq!("101 Switching Protocols", response.status().to_string()); + child.kill().unwrap(); +} + +#[cfg(not(target_os = "linux"))] // TODO(ry) broken on github actions. +#[tokio::test] +async fn inspector_pause() { + let script = deno::test_util::root_path() + .join("cli") + .join("tests") + .join("inspector1.js"); + let mut child = util::deno_cmd() + .arg("run") + // Warning: each inspector test should be on its own port to avoid + // conflicting with another inspector test. + .arg("--inspect=127.0.0.1:9230") + .arg(script) + .stderr(std::process::Stdio::piped()) + .spawn() + .unwrap(); + let ws_url = extract_ws_url_from_stderr(child.stderr.as_mut().unwrap()); + println!("ws_url {}", ws_url); + // We use tokio_tungstenite as a websocket client because warp (which is + // a dependency of Deno) uses it. + let (mut socket, _) = tokio_tungstenite::connect_async(ws_url) + .await + .expect("Can't connect"); + + /// Returns the next websocket message as a string ignoring + /// Debugger.scriptParsed messages. + async fn ws_read_msg( + socket: &mut tokio_tungstenite::WebSocketStream<tokio::net::TcpStream>, + ) -> String { + use futures::stream::StreamExt; + while let Some(msg) = socket.next().await { + let msg = msg.unwrap().to_string(); + assert!(!msg.contains("error")); + if !msg.contains("Debugger.scriptParsed") { + return msg; + } + } + unreachable!() + } + + use futures::sink::SinkExt; + socket + .send(r#"{"id":6,"method":"Debugger.enable"}"#.into()) + .await + .unwrap(); + + let msg = ws_read_msg(&mut socket).await; + println!("response msg 1 {}", msg); + assert!(msg.starts_with(r#"{"id":6,"result":{"debuggerId":"#)); + + socket + .send(r#"{"id":31,"method":"Debugger.pause"}"#.into()) + .await + .unwrap(); + + let msg = ws_read_msg(&mut socket).await; + println!("response msg 2 {}", msg); + assert_eq!(msg, r#"{"id":31,"result":{}}"#); + + child.kill().unwrap(); +} + mod util { use deno::colors::strip_ansi_codes; pub use deno::test_util::*; diff --git a/cli/worker.rs b/cli/worker.rs index 6593ade0b..994f22f04 100644 --- a/cli/worker.rs +++ b/cli/worker.rs @@ -97,6 +97,7 @@ pub struct Worker { pub waker: AtomicWaker, pub(crate) internal_channels: WorkerChannelsInternal, external_channels: WorkerHandle, + inspector: Option<Box<crate::inspector::DenoInspector>>, } impl Worker { @@ -104,9 +105,15 @@ impl Worker { let loader = Rc::new(state.clone()); let mut isolate = deno_core::EsIsolate::new(loader, startup_data, false); - let global_state_ = state.borrow().global_state.clone(); + let global_state = state.borrow().global_state.clone(); + + let inspector = global_state + .inspector_server + .as_ref() + .map(|s| s.add_inspector(&mut *isolate)); + isolate.set_js_error_create_fn(move |core_js_error| { - JSError::create(core_js_error, &global_state_.ts_compiler) + JSError::create(core_js_error, &global_state.ts_compiler) }); let (internal_channels, external_channels) = create_channels(); @@ -118,6 +125,7 @@ impl Worker { waker: AtomicWaker::new(), internal_channels, external_channels, + inspector, } } @@ -175,11 +183,23 @@ impl Worker { } } +impl Drop for Worker { + fn drop(&mut self) { + // The Isolate object must outlive the Inspector object, but this is + // currently not enforced by the type system. + self.inspector.take(); + } +} + impl Future for Worker { type Output = Result<(), ErrBox>; fn poll(self: Pin<&mut Self>, cx: &mut Context) -> Poll<Self::Output> { let inner = self.get_mut(); + if let Some(deno_inspector) = inner.inspector.as_mut() { + // We always poll the inspector if it exists. + let _ = deno_inspector.poll_unpin(cx); + } inner.waker.register(cx.waker()); inner.isolate.poll_unpin(cx) } |