Detailed changes
@@ -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]]
@@ -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
@@ -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,
);
@@ -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,
}
@@ -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,
@@ -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,
@@ -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())
}