summaryrefslogtreecommitdiff
path: root/cli
diff options
context:
space:
mode:
authorRyan Dahl <ry@tinyclouds.org>2020-03-27 16:09:51 -0400
committerGitHub <noreply@github.com>2020-03-27 16:09:51 -0400
commit2874664e9131616b71dd0d7d23750245b023833f (patch)
tree6782161aa151d00f930bc0157e95b14d895347f0 /cli
parent8bcdb422e387a88075126d80e1612a30f5a7d89e (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.toml5
-rw-r--r--cli/flags.rs88
-rw-r--r--cli/global_state.rs11
-rw-r--r--cli/inspector.rs549
-rw-r--r--cli/lib.rs1
-rw-r--r--cli/ops/fs.rs8
-rw-r--r--cli/tests/inspector1.js3
-rw-r--r--cli/tests/integration_tests.rs105
-rw-r--r--cli/worker.rs24
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)
}