From cf8ce3ed1e1a326d4b1e2e366ed99b012892c004 Mon Sep 17 00:00:00 2001 From: Cole Miller Date: Sat, 27 Sep 2025 11:12:56 -0400 Subject: [PATCH] wip --- 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(-) diff --git a/Cargo.lock b/Cargo.lock index 5bbe2f880ba8fb631ccbe382aa0a029f05a78ce2..498c29afd67aa6d2b915d1e237f8af12aa96c253 100644 --- a/Cargo.lock +++ b/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]] diff --git a/crates/project/Cargo.toml b/crates/project/Cargo.toml index 39dc0621732bfd42b3a24735ad803915fbf2885c..8318ca0182d9c1fd7ebd2cfdac5909c03ef77a58 100644 --- a/crates/project/Cargo.toml +++ b/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 diff --git a/crates/project/src/debugger/dap_store.rs b/crates/project/src/debugger/dap_store.rs index 0c8acb13e537ccbed95c2368f2f1887023251475..1ff42bb9c125c759fe5a2f9456fe6cd33f3a0b1b 100644 --- a/crates/project/src/debugger/dap_store.rs +++ b/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, ); diff --git a/crates/project/src/debugger/session.rs b/crates/project/src/debugger/session.rs index ca4894b88c881e8837c6f9fde773da6d34df6f6d..fc1ac1825c145f23b9075dc3cb6fa1ef3bf0dc57 100644 --- a/crates/project/src/debugger/session.rs +++ b/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>, } trait CacheableCommand: Any + Send + Sync { @@ -812,6 +820,7 @@ impl Session { adapter: DebugAdapterName, task_context: TaskContext, quirks: SessionQuirks, + remote_client: Option>, cx: &mut App, ) -> Entity { cx.new::(|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, + request: LaunchBrowserInCompanionParams, + cx: &mut Context, + ) { + 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>, + browser_out: async_compat::Compat>, +} + +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> { + 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> { + 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> { + 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> { + 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>)> { + 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, + server_port: u16, + path: String, + launch_id: u64, + params: serde_json::Value, } diff --git a/crates/remote/src/remote_client.rs b/crates/remote/src/remote_client.rs index d26c64bc69658c6ad7450b2a3345a11c71eb3d66..a016c1d4d6f5d5941fcba51e0186f22965b28191 100644 --- a/crates/remote/src/remote_client.rs +++ b/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 { + 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, port_forward: Option<(u16, String, u16)>, ) -> Result; + fn build_forward_port_command( + &self, + local_port: u16, + remote: String, + remote_port: u16, + ) -> Result; 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 { + 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, diff --git a/crates/remote/src/transport/ssh.rs b/crates/remote/src/transport/ssh.rs index 616cf3810658c4efc25cc4ae348ff73873bd664f..4d83042e4bc4bb6e86b28d1e84a6b8f50e738a7b 100644 --- a/crates/remote/src/transport/ssh.rs +++ b/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 { + 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, diff --git a/crates/remote/src/transport/wsl.rs b/crates/remote/src/transport/wsl.rs index 2096c25fa461df723c837314072b55fda6b3c988..9d223a280fab8a9bc7ff5994686d52bd0efe7138 100644 --- a/crates/remote/src/transport/wsl.rs +++ b/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 { + Err(anyhow!("WSL shares a network interface with the host")) + } + fn connection_options(&self) -> RemoteConnectionOptions { RemoteConnectionOptions::Wsl(self.connection_options.clone()) }