remote: Recognize WSL interop to open browser for codex web login (#44136) (cherry-pick to preview) (#44159)

zed-zippy[bot] , Lukas Wirth , and Ben Brandt created

Cherry-pick of #44136 to preview

----
Closes #41521

Release Notes:

- Fixed codex web login not working on wsl remotes if no browser is
installed

Co-authored-by: Ben Brandt <benjamin.j.brandt@gmail.com>

Co-authored-by: Lukas Wirth <lukas@zed.dev>
Co-authored-by: Ben Brandt <benjamin.j.brandt@gmail.com>

Change summary

crates/client/src/client.rs              |  4 +++
crates/project/src/agent_server_store.rs | 10 +++++---
crates/remote/Cargo.toml                 |  1 
crates/remote/src/remote_client.rs       | 30 +++++++++++++++++++++----
crates/remote/src/transport.rs           |  6 ----
crates/remote/src/transport/ssh.rs       |  4 +++
crates/remote/src/transport/wsl.rs       | 24 ++++++++++++++++++++
crates/remote_server/src/unix.rs         | 12 ++++++++-
crates/rpc/src/proto_client.rs           |  5 ++++
9 files changed, 79 insertions(+), 17 deletions(-)

Detailed changes

crates/client/src/client.rs 🔗

@@ -1723,6 +1723,10 @@ impl ProtoClient for Client {
     fn is_via_collab(&self) -> bool {
         true
     }
+
+    fn has_wsl_interop(&self) -> bool {
+        false
+    }
 }
 
 /// prefix for the zed:// url scheme

crates/project/src/agent_server_store.rs 🔗

@@ -453,7 +453,9 @@ impl AgentServerStore {
                     .clone()
                     .and_then(|settings| settings.custom_command()),
                 http_client: http_client.clone(),
-                is_remote: downstream_client.is_some(),
+                no_browser: downstream_client
+                    .as_ref()
+                    .is_some_and(|(_, client)| !client.has_wsl_interop()),
             }),
         );
         self.external_agents.insert(
@@ -1355,7 +1357,7 @@ struct LocalCodex {
     project_environment: Entity<ProjectEnvironment>,
     http_client: Arc<dyn HttpClient>,
     custom_command: Option<AgentServerCommand>,
-    is_remote: bool,
+    no_browser: bool,
 }
 
 impl ExternalAgentServer for LocalCodex {
@@ -1375,7 +1377,7 @@ impl ExternalAgentServer for LocalCodex {
             .map(|root_dir| Path::new(root_dir))
             .unwrap_or(paths::home_dir())
             .into();
-        let is_remote = self.is_remote;
+        let no_browser = self.no_browser;
 
         cx.spawn(async move |cx| {
             let mut env = project_environment
@@ -1388,7 +1390,7 @@ impl ExternalAgentServer for LocalCodex {
                 })?
                 .await
                 .unwrap_or_default();
-            if is_remote {
+            if no_browser {
                 env.insert("NO_BROWSER".to_owned(), "1".to_owned());
             }
 

crates/remote/Cargo.toml 🔗

@@ -43,7 +43,6 @@ urlencoding.workspace = true
 util.workspace = true
 which.workspace = true
 
-
 [dev-dependencies]
 gpui = { workspace = true, features = ["test-support"] }
 fs = { workspace = true, features = ["test-support"] }

crates/remote/src/remote_client.rs 🔗

@@ -328,8 +328,15 @@ impl RemoteClient {
                 let (incoming_tx, incoming_rx) = mpsc::unbounded::<Envelope>();
                 let (connection_activity_tx, connection_activity_rx) = mpsc::channel::<()>(1);
 
-                let client =
-                    cx.update(|cx| ChannelClient::new(incoming_rx, outgoing_tx, cx, "client"))?;
+                let client = cx.update(|cx| {
+                    ChannelClient::new(
+                        incoming_rx,
+                        outgoing_tx,
+                        cx,
+                        "client",
+                        remote_connection.has_wsl_interop(),
+                    )
+                })?;
 
                 let path_style = remote_connection.path_style();
                 let this = cx.new(|_| Self {
@@ -420,8 +427,9 @@ impl RemoteClient {
         outgoing_tx: mpsc::UnboundedSender<Envelope>,
         cx: &App,
         name: &'static str,
+        has_wsl_interop: bool,
     ) -> AnyProtoClient {
-        ChannelClient::new(incoming_rx, outgoing_tx, cx, name).into()
+        ChannelClient::new(incoming_rx, outgoing_tx, cx, name, has_wsl_interop).into()
     }
 
     pub fn shutdown_processes<T: RequestMessage>(
@@ -921,8 +929,8 @@ impl RemoteClient {
         });
         let (outgoing_tx, _) = mpsc::unbounded::<Envelope>();
         let (_, incoming_rx) = mpsc::unbounded::<Envelope>();
-        let server_client =
-            server_cx.update(|cx| ChannelClient::new(incoming_rx, outgoing_tx, cx, "fake-server"));
+        let server_client = server_cx
+            .update(|cx| ChannelClient::new(incoming_rx, outgoing_tx, cx, "fake-server", false));
         let connection: Arc<dyn RemoteConnection> = Arc::new(fake::FakeRemoteConnection {
             connection_options: opts.clone(),
             server_cx: fake::SendableCx::new(server_cx),
@@ -1140,6 +1148,7 @@ pub trait RemoteConnection: Send + Sync {
     fn path_style(&self) -> PathStyle;
     fn shell(&self) -> String;
     fn default_system_shell(&self) -> String;
+    fn has_wsl_interop(&self) -> bool;
 
     #[cfg(any(test, feature = "test-support"))]
     fn simulate_disconnect(&self, _: &AsyncApp) {}
@@ -1188,6 +1197,7 @@ struct ChannelClient {
     name: &'static str,
     task: Mutex<Task<Result<()>>>,
     remote_started: Signal<()>,
+    has_wsl_interop: bool,
 }
 
 impl ChannelClient {
@@ -1196,6 +1206,7 @@ impl ChannelClient {
         outgoing_tx: mpsc::UnboundedSender<Envelope>,
         cx: &App,
         name: &'static str,
+        has_wsl_interop: bool,
     ) -> Arc<Self> {
         Arc::new_cyclic(|this| Self {
             outgoing_tx: Mutex::new(outgoing_tx),
@@ -1211,6 +1222,7 @@ impl ChannelClient {
                 &cx.to_async(),
             )),
             remote_started: Signal::new(cx),
+            has_wsl_interop,
         })
     }
 
@@ -1489,6 +1501,10 @@ impl ProtoClient for ChannelClient {
     fn is_via_collab(&self) -> bool {
         false
     }
+
+    fn has_wsl_interop(&self) -> bool {
+        self.has_wsl_interop
+    }
 }
 
 #[cfg(any(test, feature = "test-support"))]
@@ -1652,6 +1668,10 @@ mod fake {
         fn default_system_shell(&self) -> String {
             "sh".to_owned()
         }
+
+        fn has_wsl_interop(&self) -> bool {
+            false
+        }
     }
 
     pub(super) struct Delegate;

crates/remote/src/transport.rs 🔗

@@ -131,11 +131,7 @@ async fn build_remote_server_from_source(
     let build_remote_server =
         std::env::var("ZED_BUILD_REMOTE_SERVER").unwrap_or("nocompress".into());
 
-    if build_remote_server == "false"
-        || build_remote_server == "no"
-        || build_remote_server == "off"
-        || build_remote_server == "0"
-    {
+    if let "false" | "no" | "off" | "0" = &*build_remote_server {
         return Ok(None);
     }
 

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

@@ -394,6 +394,10 @@ impl RemoteConnection for SshRemoteConnection {
     fn path_style(&self) -> PathStyle {
         self.ssh_path_style
     }
+
+    fn has_wsl_interop(&self) -> bool {
+        false
+    }
 }
 
 impl SshRemoteConnection {

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

@@ -47,6 +47,7 @@ pub(crate) struct WslRemoteConnection {
     shell: String,
     shell_kind: ShellKind,
     default_system_shell: String,
+    has_wsl_interop: bool,
     connection_options: WslConnectionOptions,
 }
 
@@ -71,6 +72,7 @@ impl WslRemoteConnection {
             shell: String::new(),
             shell_kind: ShellKind::Posix,
             default_system_shell: String::from("/bin/sh"),
+            has_wsl_interop: false,
         };
         delegate.set_status(Some("Detecting WSL environment"), cx);
         this.shell = this
@@ -79,6 +81,15 @@ impl WslRemoteConnection {
             .context("failed detecting shell")?;
         log::info!("Remote shell discovered: {}", this.shell);
         this.shell_kind = ShellKind::new(&this.shell, false);
+        this.has_wsl_interop = this.detect_has_wsl_interop().await.unwrap_or_default();
+        log::info!(
+            "Remote has wsl interop {}",
+            if this.has_wsl_interop {
+                "enabled"
+            } else {
+                "disabled"
+            }
+        );
         this.platform = this
             .detect_platform()
             .await
@@ -115,6 +126,14 @@ impl WslRemoteConnection {
             .unwrap_or_else(|| "/bin/sh".to_string()))
     }
 
+    async fn detect_has_wsl_interop(&self) -> Result<bool> {
+        Ok(self
+            .run_wsl_command_with_output("cat", &["/proc/sys/fs/binfmt_misc/WSLInterop"])
+            .await
+            .inspect_err(|err| log::error!("Failed to detect wsl interop: {err}"))?
+            .contains("enabled"))
+    }
+
     async fn windows_path_to_wsl_path(&self, source: &Path) -> Result<String> {
         windows_path_to_wsl_path_impl(&self.connection_options, source).await
     }
@@ -317,6 +336,7 @@ impl RemoteConnection for WslRemoteConnection {
                 proxy_args.push(format!("{}={}", env_var, value));
             }
         }
+
         proxy_args.push(remote_binary_path.display(PathStyle::Posix).into_owned());
         proxy_args.push("proxy".to_owned());
         proxy_args.push("--identifier".to_owned());
@@ -489,6 +509,10 @@ impl RemoteConnection for WslRemoteConnection {
     fn default_system_shell(&self) -> String {
         self.default_system_shell.clone()
     }
+
+    fn has_wsl_interop(&self) -> bool {
+        self.has_wsl_interop
+    }
 }
 
 /// `wslpath` is a executable available in WSL, it's a linux binary.

crates/remote_server/src/unix.rs 🔗

@@ -199,6 +199,7 @@ fn start_server(
     listeners: ServerListeners,
     log_rx: Receiver<Vec<u8>>,
     cx: &mut App,
+    is_wsl_interop: bool,
 ) -> AnyProtoClient {
     // This is the server idle timeout. If no connection comes in this timeout, the server will shut down.
     const IDLE_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(10 * 60);
@@ -318,7 +319,7 @@ fn start_server(
     })
     .detach();
 
-    RemoteClient::proto_client_from_channels(incoming_rx, outgoing_tx, cx, "server")
+    RemoteClient::proto_client_from_channels(incoming_rx, outgoing_tx, cx, "server", is_wsl_interop)
 }
 
 fn init_paths() -> anyhow::Result<()> {
@@ -407,8 +408,15 @@ pub fn execute_run(
 
         HeadlessProject::init(cx);
 
+        let is_wsl_interop = if cfg!(target_os = "linux") {
+            // See: https://learn.microsoft.com/en-us/windows/wsl/filesystems#disable-interoperability
+            matches!(std::fs::read_to_string("/proc/sys/fs/binfmt_misc/WSLInterop"), Ok(s) if s.contains("enabled"))
+        } else {
+            false
+        };
+
         log::info!("gpui app started, initializing server");
-        let session = start_server(listeners, log_rx, cx);
+        let session = start_server(listeners, log_rx, cx, is_wsl_interop);
 
         GitHostingProviderRegistry::set_global(git_hosting_provider_registry, cx);
         git_hosting_providers::init(cx);

crates/rpc/src/proto_client.rs 🔗

@@ -59,6 +59,7 @@ pub trait ProtoClient: Send + Sync {
     fn message_handler_set(&self) -> &parking_lot::Mutex<ProtoMessageHandlerSet>;
 
     fn is_via_collab(&self) -> bool;
+    fn has_wsl_interop(&self) -> bool;
 }
 
 #[derive(Default)]
@@ -510,6 +511,10 @@ impl AnyProtoClient {
             },
         );
     }
+
+    pub fn has_wsl_interop(&self) -> bool {
+        self.0.client.has_wsl_interop()
+    }
 }
 
 fn to_any_envelope<T: EnvelopedMessage>(