wip

Cole Miller created

Change summary

Cargo.lock                               |  36 +++--
crates/project/Cargo.toml                |   7 +
crates/project/src/debugger/dap_store.rs |   7 +
crates/project/src/debugger/session.rs   | 168 +++++++++++++++++++++++++
crates/remote/src/remote_client.rs       |  35 +++++
crates/remote/src/transport/ssh.rs       |  17 ++
crates/remote/src/transport/wsl.rs       |   9 +
7 files changed, 260 insertions(+), 19 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -2728,7 +2728,7 @@ dependencies = [
  "cap-primitives",
  "cap-std",
  "io-lifetimes",
- "windows-sys 0.52.0",
+ "windows-sys 0.59.0",
 ]
 
 [[package]]
@@ -2757,7 +2757,7 @@ dependencies = [
  "maybe-owned",
  "rustix 1.0.7",
  "rustix-linux-procfs",
- "windows-sys 0.52.0",
+ "windows-sys 0.59.0",
  "winx",
 ]
 
@@ -5488,7 +5488,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "976dd42dc7e85965fe702eb8164f21f450704bdde31faefd6471dba214cb594e"
 dependencies = [
  "libc",
- "windows-sys 0.52.0",
+ "windows-sys 0.59.0",
 ]
 
 [[package]]
@@ -5855,7 +5855,7 @@ checksum = "0ce92ff622d6dadf7349484f42c93271a0d49b7cc4d466a936405bacbe10aa78"
 dependencies = [
  "cfg-if",
  "rustix 1.0.7",
- "windows-sys 0.52.0",
+ "windows-sys 0.59.0",
 ]
 
 [[package]]
@@ -6243,7 +6243,7 @@ checksum = "94e7099f6313ecacbe1256e8ff9d617b75d1bcb16a6fddef94866d225a01a14a"
 dependencies = [
  "io-lifetimes",
  "rustix 1.0.7",
- "windows-sys 0.52.0",
+ "windows-sys 0.59.0",
 ]
 
 [[package]]
@@ -8146,7 +8146,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "2285ddfe3054097ef4b2fe909ef8c3bcd1ea52a8f0d274416caebeef39f04a65"
 dependencies = [
  "io-lifetimes",
- "windows-sys 0.52.0",
+ "windows-sys 0.59.0",
 ]
 
 [[package]]
@@ -8219,7 +8219,7 @@ checksum = "e04d7f318608d35d4b61ddd75cbdaee86b023ebe2bd5a66ee0915f0bf93095a9"
 dependencies = [
  "hermit-abi 0.5.0",
  "libc",
- "windows-sys 0.52.0",
+ "windows-sys 0.59.0",
 ]
 
 [[package]]
@@ -12065,6 +12065,7 @@ dependencies = [
  "aho-corasick",
  "anyhow",
  "askpass",
+ "async-compat",
  "async-trait",
  "base64 0.22.1",
  "buffer_diff",
@@ -12085,6 +12086,7 @@ dependencies = [
  "git_hosting_providers",
  "globset",
  "gpui",
+ "gpui_tokio",
  "http_client",
  "image",
  "indexmap 2.9.0",
@@ -12121,6 +12123,8 @@ dependencies = [
  "tempfile",
  "terminal",
  "text",
+ "tokio",
+ "tokio-tungstenite 0.26.2",
  "toml 0.8.20",
  "unindent",
  "url",
@@ -12560,7 +12564,7 @@ dependencies = [
  "once_cell",
  "socket2",
  "tracing",
- "windows-sys 0.52.0",
+ "windows-sys 0.59.0",
 ]
 
 [[package]]
@@ -13642,7 +13646,7 @@ dependencies = [
  "errno 0.3.11",
  "libc",
  "linux-raw-sys 0.4.15",
- "windows-sys 0.52.0",
+ "windows-sys 0.59.0",
 ]
 
 [[package]]
@@ -13655,7 +13659,7 @@ dependencies = [
  "errno 0.3.11",
  "libc",
  "linux-raw-sys 0.9.4",
- "windows-sys 0.52.0",
+ "windows-sys 0.59.0",
 ]
 
 [[package]]
@@ -13777,7 +13781,7 @@ dependencies = [
  "security-framework 3.2.0",
  "security-framework-sys",
  "webpki-root-certs",
- "windows-sys 0.52.0",
+ "windows-sys 0.59.0",
 ]
 
 [[package]]
@@ -15161,7 +15165,7 @@ dependencies = [
  "cfg-if",
  "libc",
  "psm",
- "windows-sys 0.52.0",
+ "windows-sys 0.59.0",
 ]
 
 [[package]]
@@ -15854,7 +15858,7 @@ dependencies = [
  "fd-lock",
  "io-lifetimes",
  "rustix 0.38.44",
- "windows-sys 0.52.0",
+ "windows-sys 0.59.0",
  "winx",
 ]
 
@@ -16036,7 +16040,7 @@ dependencies = [
  "getrandom 0.3.2",
  "once_cell",
  "rustix 1.0.7",
- "windows-sys 0.52.0",
+ "windows-sys 0.59.0",
 ]
 
 [[package]]
@@ -18620,7 +18624,7 @@ version = "0.1.9"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
 dependencies = [
- "windows-sys 0.52.0",
+ "windows-sys 0.59.0",
 ]
 
 [[package]]
@@ -19332,7 +19336,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "3f3fd376f71958b862e7afb20cfe5a22830e1963462f3a17f49d82a6c1d1f42d"
 dependencies = [
  "bitflags 2.9.0",
- "windows-sys 0.52.0",
+ "windows-sys 0.59.0",
 ]
 
 [[package]]

crates/project/Cargo.toml 🔗

@@ -31,6 +31,13 @@ aho-corasick.workspace = true
 anyhow.workspace = true
 askpass.workspace = true
 async-trait.workspace = true
+
+# FIXME
+tokio-tungstenite.workspace = true
+gpui_tokio.workspace = true
+tokio.workspace = true
+async-compat.workspace = true
+
 base64.workspace = true
 buffer_diff.workspace = true
 circular-buffer.workspace = true

crates/project/src/debugger/dap_store.rs 🔗

@@ -401,6 +401,12 @@ impl DapStore {
             });
         }
 
+        let remote_client = match &self.mode {
+            DapStoreMode::Local(_) => None,
+            DapStoreMode::Remote(remote_dap_store) => Some(remote_dap_store.remote_client.clone()),
+            DapStoreMode::Collab => None,
+        };
+
         let session = Session::new(
             self.breakpoint_store.clone(),
             session_id,
@@ -409,6 +415,7 @@ impl DapStore {
             adapter,
             task_context,
             quirks,
+            remote_client,
             cx,
         );
 

crates/project/src/debugger/session.rs 🔗

@@ -31,21 +31,27 @@ use dap::{
     RunInTerminalRequestArguments, StackFramePresentationHint, StartDebuggingRequestArguments,
     StartDebuggingRequestArgumentsRequest, VariablePresentationHint, WriteMemoryArguments,
 };
-use futures::SinkExt;
 use futures::channel::mpsc::UnboundedSender;
 use futures::channel::{mpsc, oneshot};
+use futures::compat::CompatSink;
 use futures::{FutureExt, future::Shared};
+use futures::{SinkExt, StreamExt};
 use gpui::{
     App, AppContext, AsyncApp, BackgroundExecutor, Context, Entity, EventEmitter, SharedString,
     Task, WeakEntity,
 };
 
+use gpui_tokio::Tokio;
+use remote::RemoteClient;
 use rpc::ErrorExt;
+use serde::Deserialize;
 use serde_json::Value;
-use smol::stream::StreamExt;
+use smol::net::TcpListener;
 use std::any::TypeId;
 use std::collections::BTreeMap;
 use std::ops::RangeInclusive;
+use std::pin::Pin;
+use std::process::Stdio;
 use std::u64;
 use std::{
     any::Any,
@@ -56,6 +62,7 @@ use std::{
 };
 use task::TaskContext;
 use text::{PointUtf16, ToPointUtf16};
+use util::command::new_smol_command;
 use util::{ResultExt, debug_panic, maybe};
 use worktree::Worktree;
 
@@ -696,6 +703,7 @@ pub struct Session {
     task_context: TaskContext,
     memory: memory::Memory,
     quirks: SessionQuirks,
+    remote_client: Option<Entity<RemoteClient>>,
 }
 
 trait CacheableCommand: Any + Send + Sync {
@@ -812,6 +820,7 @@ impl Session {
         adapter: DebugAdapterName,
         task_context: TaskContext,
         quirks: SessionQuirks,
+        remote_client: Option<Entity<RemoteClient>>,
         cx: &mut App,
     ) -> Entity<Self> {
         cx.new::<Self>(|cx| {
@@ -867,6 +876,7 @@ impl Session {
                 task_context,
                 memory: memory::Memory::new(),
                 quirks,
+                remote_client,
             }
         })
     }
@@ -1558,8 +1568,19 @@ impl Session {
             Events::ProgressUpdate(_) => {}
             Events::Invalidated(_) => {}
             Events::Other(event) => {
+                // FIXME handle killRemoteBrowser too
                 if event.event == "launchBrowserInCompanion" {
-                    // TODO
+                    let Some(request) = serde_json::from_value(event.body).ok() else {
+                        log::error!("failed to deserialize launchBrowserInCompanion event");
+                        return;
+                    };
+                    let Some(remote_client) = self.remote_client.clone() else {
+                        log::error!(
+                            "no remote client so not handling launchBrowserInCompanion event"
+                        );
+                        return;
+                    };
+                    self.launch_browser_for_remote_server(remote_client, request, cx);
                 }
             }
         }
@@ -2720,4 +2741,145 @@ impl Session {
     pub fn quirks(&self) -> SessionQuirks {
         self.quirks
     }
+
+    fn launch_browser_for_remote_server(
+        &mut self,
+        remote_client: Entity<RemoteClient>,
+        request: LaunchBrowserInCompanionParams,
+        cx: &mut Context<Self>,
+    ) {
+        let task = cx.spawn(async move |_, cx| {
+            let (port, _child) =
+                if remote_client.read_with(cx, |client, _| client.shares_network_interface())? {
+                    (request.server_port, None)
+                } else {
+                    let port = TcpListener::bind("127.0.0.1").await?.local_addr()?.port();
+                    let child = remote_client.update(cx, |client, _| {
+                        let command = client.build_forward_port_command(
+                            port,
+                            "localhost".into(),
+                            request.server_port,
+                        )?;
+                        let child = new_smol_command(command.program)
+                            .args(command.args)
+                            .envs(command.env)
+                            .kill_on_drop(true)
+                            .spawn()
+                            .context("spawning port forwarding process")?;
+                        anyhow::Ok(child)
+                    })??;
+                    (port, Some(child))
+                };
+
+            let (_child, browser_stream) = spawn_browser(&request)?;
+
+            Tokio::spawn(cx, async move {
+                let path = request.path;
+                let url = format!("ws://localhost:{port}{path}");
+                log::info!("will connect to DAP running on remote at {url}");
+                let (dap_stream, _response) = tokio_tungstenite::connect_async(&url).await?;
+                let (mut dap_in, mut dap_out) = dap_stream.split();
+                log::info!("established websocket connection to DAP running on remote");
+
+                // FIXME what url to use here?
+                let (browser_stream, _response) =
+                    tokio_tungstenite::client_async("ws://localhost", browser_stream).await?;
+                let (mut browser_in, mut browser_out) = browser_stream.split();
+
+                let down_task = tokio::spawn(async move {
+                    while let Some(message) = dap_out.next().await {
+                        let message = message?;
+                        browser_in.send(message).await?;
+                    }
+                    anyhow::Ok(())
+                });
+                let up_task = tokio::spawn(async move {
+                    while let Some(message) = browser_out.next().await {
+                        let message = message?;
+                        dap_in.send(message).await?;
+                    }
+                    anyhow::Ok(())
+                });
+                down_task.await.ok();
+                up_task.await.ok();
+                anyhow::Ok(())
+            })?
+            .await??;
+            anyhow::Ok(())
+        });
+        self.background_tasks.push(cx.spawn(async move |_, _| {
+            task.await.ok();
+        }));
+    }
+}
+
+struct BrowserStream {
+    browser_in: async_compat::Compat<smol::Unblock<std::io::PipeWriter>>,
+    browser_out: async_compat::Compat<smol::Unblock<std::io::PipeReader>>,
+}
+
+impl BrowserStream {
+    fn new() -> Result<(Self, std::io::PipeWriter, std::io::PipeReader)> {
+        todo!()
+    }
+}
+
+impl tokio::io::AsyncRead for BrowserStream {
+    fn poll_read(
+        mut self: Pin<&mut Self>,
+        cx: &mut std::task::Context<'_>,
+        buf: &mut tokio::io::ReadBuf<'_>,
+    ) -> std::task::Poll<std::io::Result<()>> {
+        let browser_out = unsafe { Pin::new_unchecked(&mut self.browser_out) };
+        browser_out.poll_read(cx, buf)
+    }
+}
+
+impl tokio::io::AsyncWrite for BrowserStream {
+    fn poll_write(
+        mut self: Pin<&mut Self>,
+        cx: &mut std::task::Context<'_>,
+        buf: &[u8],
+    ) -> std::task::Poll<std::result::Result<usize, std::io::Error>> {
+        let browser_in = unsafe { Pin::new_unchecked(&mut self.browser_in) };
+        browser_in.poll_write(cx, buf)
+    }
+
+    fn poll_flush(
+        mut self: Pin<&mut Self>,
+        cx: &mut std::task::Context<'_>,
+    ) -> std::task::Poll<std::result::Result<(), std::io::Error>> {
+        let browser_in = unsafe { Pin::new_unchecked(&mut self.browser_in) };
+        browser_in.poll_flush(cx)
+    }
+
+    fn poll_shutdown(
+        mut self: Pin<&mut Self>,
+        cx: &mut std::task::Context<'_>,
+    ) -> std::task::Poll<std::result::Result<(), std::io::Error>> {
+        let browser_in = unsafe { Pin::new_unchecked(&mut self.browser_in) };
+        browser_in.poll_shutdown(cx)
+    }
+}
+
+#[cfg(windows)]
+fn spawn_browser(
+    request: &LaunchBrowserInCompanionParams,
+) -> Result<(smol::process::Child, Pin<Box<BrowserStream>>)> {
+    let (stream, child_in, child_out) = BrowserStream::new()?;
+
+    // create process
+    // DuplicateHandle or w/e
+    Box::pin(stream)
+}
+
+#[derive(Deserialize)]
+#[serde(rename_all = "camelCase")]
+struct LaunchBrowserInCompanionParams {
+    r#type: String,
+    browser_args: Vec<String>,
+    server_port: u16,
+    path: String,
+    launch_id: u64,
+    params: serde_json::Value,
 }

crates/remote/src/remote_client.rs 🔗

@@ -801,6 +801,18 @@ impl RemoteClient {
         connection.build_command(program, args, env, working_dir, port_forward)
     }
 
+    pub fn build_forward_port_command(
+        &self,
+        local_port: u16,
+        host: String,
+        remote_port: u16,
+    ) -> Result<CommandTemplate> {
+        let Some(connection) = self.remote_connection() else {
+            return Err(anyhow!("no ssh connection"));
+        };
+        connection.build_forward_port_command(local_port, host, remote_port)
+    }
+
     pub fn upload_directory(
         &self,
         src_path: PathBuf,
@@ -1069,6 +1081,12 @@ pub(crate) trait RemoteConnection: Send + Sync {
         working_dir: Option<String>,
         port_forward: Option<(u16, String, u16)>,
     ) -> Result<CommandTemplate>;
+    fn build_forward_port_command(
+        &self,
+        local_port: u16,
+        remote: String,
+        remote_port: u16,
+    ) -> Result<CommandTemplate>;
     fn connection_options(&self) -> RemoteConnectionOptions;
     fn path_style(&self) -> PathStyle;
     fn shell(&self) -> String;
@@ -1448,6 +1466,23 @@ mod fake {
             })
         }
 
+        fn build_forward_port_command(
+            &self,
+            local_port: u16,
+            host: String,
+            remote_port: u16,
+        ) -> anyhow::Result<CommandTemplate> {
+            Ok(CommandTemplate {
+                program: "ssh".into(),
+                args: vec![
+                    "-N".into(),
+                    "-L".into(),
+                    format!("{local_port}:{host}:{remote_port}"),
+                ],
+                env: Default::default(),
+            })
+        }
+
         fn upload_directory(
             &self,
             _src_path: PathBuf,

crates/remote/src/transport/ssh.rs 🔗

@@ -148,6 +148,23 @@ impl RemoteConnection for SshRemoteConnection {
         )
     }
 
+    fn build_forward_port_command(
+        &self,
+        local_port: u16,
+        host: String,
+        remote_port: u16,
+    ) -> Result<CommandTemplate> {
+        Ok(CommandTemplate {
+            program: "ssh".into(),
+            args: vec![
+                "-N".into(),
+                "-L".into(),
+                format!("{local_port}:{host}:{remote_port}"),
+            ],
+            env: Default::default(),
+        })
+    }
+
     fn upload_directory(
         &self,
         src_path: PathBuf,

crates/remote/src/transport/wsl.rs 🔗

@@ -438,6 +438,15 @@ impl RemoteConnection for WslRemoteConnection {
         })
     }
 
+    fn build_forward_port_command(
+        &self,
+        _: u16,
+        _: String,
+        _: u16,
+    ) -> anyhow::Result<CommandTemplate> {
+        Err(anyhow!("WSL shares a network interface with the host"))
+    }
+
     fn connection_options(&self) -> RemoteConnectionOptions {
         RemoteConnectionOptions::Wsl(self.connection_options.clone())
     }