Add SSH remote server for Windows (#47460)

John Tur and Lukas Wirth created

Closes https://github.com/zed-industries/zed/issues/33748

Release Notes:

- Windows is now supported as a target platform for SSH remoting.

---------

Co-authored-by: Lukas Wirth <me@lukaswirth.dev>

Change summary

.github/workflows/release.yml                     |  14 
.github/workflows/release_nightly.yml             |  14 
.github/workflows/run_bundling.yml                |  12 
Cargo.lock                                        |   2 
crates/gpui/src/platform.rs                       |   4 
crates/gpui/src/platform/windows/platform.rs      |  79 ++-
crates/remote/src/remote_client.rs                |   4 
crates/remote/src/transport/ssh.rs                | 123 ++++-
crates/remote_server/Cargo.toml                   |   8 
crates/remote_server/src/main.rs                  |  12 
crates/remote_server/src/remote_server.rs         |  81 ---
crates/remote_server/src/server.rs                | 371 +++++++++-------
crates/remote_server/src/windows.rs               |  48 ++
script/bundle-windows.ps1                         |  23 +
script/upload-nightly.ps1                         |  14 
tooling/xtask/src/tasks/workflows/run_bundling.rs |   9 
tooling/xtask/src/tasks/workflows/vars.rs         |   4 
17 files changed, 511 insertions(+), 311 deletions(-)

Detailed changes

.github/workflows/release.yml 🔗

@@ -449,6 +449,12 @@ jobs:
         name: Zed-aarch64.exe
         path: target/Zed-aarch64.exe
         if-no-files-found: error
+    - name: '@actions/upload-artifact zed-remote-server-windows-aarch64.zip'
+      uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4
+      with:
+        name: zed-remote-server-windows-aarch64.zip
+        path: target/zed-remote-server-windows-aarch64.zip
+        if-no-files-found: error
     timeout-minutes: 60
   bundle_windows_x86_64:
     needs:
@@ -488,6 +494,12 @@ jobs:
         name: Zed-x86_64.exe
         path: target/Zed-x86_64.exe
         if-no-files-found: error
+    - name: '@actions/upload-artifact zed-remote-server-windows-x86_64.zip'
+      uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4
+      with:
+        name: zed-remote-server-windows-x86_64.zip
+        path: target/zed-remote-server-windows-x86_64.zip
+        if-no-files-found: error
     timeout-minutes: 60
   upload_release_assets:
     needs:
@@ -521,6 +533,8 @@ jobs:
         mv ./artifacts/zed-remote-server-macos-x86_64.gz/zed-remote-server-macos-x86_64.gz release-artifacts/zed-remote-server-macos-x86_64.gz
         mv ./artifacts/zed-remote-server-linux-aarch64.gz/zed-remote-server-linux-aarch64.gz release-artifacts/zed-remote-server-linux-aarch64.gz
         mv ./artifacts/zed-remote-server-linux-x86_64.gz/zed-remote-server-linux-x86_64.gz release-artifacts/zed-remote-server-linux-x86_64.gz
+        mv ./artifacts/zed-remote-server-windows-aarch64.zip/zed-remote-server-windows-aarch64.zip release-artifacts/zed-remote-server-windows-aarch64.zip
+        mv ./artifacts/zed-remote-server-windows-x86_64.zip/zed-remote-server-windows-x86_64.zip release-artifacts/zed-remote-server-windows-x86_64.zip
       shell: bash -euxo pipefail {0}
     - name: gh release upload "$GITHUB_REF_NAME" --repo=zed-industries/zed release-artifacts/*
       run: gh release upload "$GITHUB_REF_NAME" --repo=zed-industries/zed release-artifacts/*

.github/workflows/release_nightly.yml 🔗

@@ -329,6 +329,12 @@ jobs:
         name: Zed-aarch64.exe
         path: target/Zed-aarch64.exe
         if-no-files-found: error
+    - name: '@actions/upload-artifact zed-remote-server-windows-aarch64.zip'
+      uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4
+      with:
+        name: zed-remote-server-windows-aarch64.zip
+        path: target/zed-remote-server-windows-aarch64.zip
+        if-no-files-found: error
     timeout-minutes: 60
   bundle_windows_x86_64:
     needs:
@@ -376,6 +382,12 @@ jobs:
         name: Zed-x86_64.exe
         path: target/Zed-x86_64.exe
         if-no-files-found: error
+    - name: '@actions/upload-artifact zed-remote-server-windows-x86_64.zip'
+      uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4
+      with:
+        name: zed-remote-server-windows-x86_64.zip
+        path: target/zed-remote-server-windows-x86_64.zip
+        if-no-files-found: error
     timeout-minutes: 60
   build_nix_linux_x86_64:
     needs:
@@ -483,6 +495,8 @@ jobs:
         mv ./artifacts/zed-remote-server-macos-x86_64.gz/zed-remote-server-macos-x86_64.gz release-artifacts/zed-remote-server-macos-x86_64.gz
         mv ./artifacts/zed-remote-server-linux-aarch64.gz/zed-remote-server-linux-aarch64.gz release-artifacts/zed-remote-server-linux-aarch64.gz
         mv ./artifacts/zed-remote-server-linux-x86_64.gz/zed-remote-server-linux-x86_64.gz release-artifacts/zed-remote-server-linux-x86_64.gz
+        mv ./artifacts/zed-remote-server-windows-aarch64.zip/zed-remote-server-windows-aarch64.zip release-artifacts/zed-remote-server-windows-aarch64.zip
+        mv ./artifacts/zed-remote-server-windows-x86_64.zip/zed-remote-server-windows-x86_64.zip release-artifacts/zed-remote-server-windows-x86_64.zip
       shell: bash -euxo pipefail {0}
     - name: ./script/upload-nightly
       run: ./script/upload-nightly

.github/workflows/run_bundling.yml 🔗

@@ -225,6 +225,12 @@ jobs:
         name: Zed-aarch64.exe
         path: target/Zed-aarch64.exe
         if-no-files-found: error
+    - name: '@actions/upload-artifact zed-remote-server-windows-aarch64.zip'
+      uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4
+      with:
+        name: zed-remote-server-windows-aarch64.zip
+        path: target/zed-remote-server-windows-aarch64.zip
+        if-no-files-found: error
     timeout-minutes: 60
   bundle_windows_x86_64:
     if: |-
@@ -263,6 +269,12 @@ jobs:
         name: Zed-x86_64.exe
         path: target/Zed-x86_64.exe
         if-no-files-found: error
+    - name: '@actions/upload-artifact zed-remote-server-windows-x86_64.zip'
+      uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4
+      with:
+        name: zed-remote-server-windows-x86_64.zip
+        path: target/zed-remote-server-windows-x86_64.zip
+        if-no-files-found: error
     timeout-minutes: 60
 concurrency:
   group: ${{ github.workflow }}-${{ github.head_ref || github.ref }}

Cargo.lock 🔗

@@ -13677,6 +13677,7 @@ dependencies = [
  "log",
  "lsp",
  "minidumper",
+ "net",
  "node_runtime",
  "paths",
  "pretty_assertions",
@@ -13703,6 +13704,7 @@ dependencies = [
  "unindent",
  "util",
  "watch",
+ "windows 0.61.3",
  "workspace",
  "worktree",
  "zlog",

crates/gpui/src/platform.rs 🔗

@@ -132,9 +132,9 @@ pub(crate) fn current_platform(headless: bool) -> Rc<dyn Platform> {
 }
 
 #[cfg(target_os = "windows")]
-pub(crate) fn current_platform(_headless: bool) -> Rc<dyn Platform> {
+pub(crate) fn current_platform(headless: bool) -> Rc<dyn Platform> {
     Rc::new(
-        WindowsPlatform::new()
+        WindowsPlatform::new(headless)
             .inspect_err(|err| show_error("Failed to launch", err.to_string()))
             .unwrap(),
     )

crates/gpui/src/platform/windows/platform.rs 🔗

@@ -33,12 +33,14 @@ pub(crate) struct WindowsPlatform {
     inner: Rc<WindowsPlatformInner>,
     raw_window_handles: Arc<RwLock<SmallVec<[SafeHwnd; 4]>>>,
     // The below members will never change throughout the entire lifecycle of the app.
+    headless: bool,
     icon: HICON,
     background_executor: BackgroundExecutor,
     foreground_executor: ForegroundExecutor,
-    text_system: Arc<DirectWriteTextSystem>,
+    text_system: Arc<dyn PlatformTextSystem>,
+    direct_write_text_system: Option<Arc<DirectWriteTextSystem>>,
     windows_version: WindowsVersion,
-    drop_target_helper: IDropTargetHelper,
+    drop_target_helper: Option<IDropTargetHelper>,
     /// Flag to instruct the `VSyncProvider` thread to invalidate the directx devices
     /// as resizing them has failed, causing us to have lost at least the render target.
     invalidate_devices: Arc<AtomicBool>,
@@ -76,11 +78,10 @@ struct PlatformCallbacks {
 }
 
 impl WindowsPlatformState {
-    fn new(directx_devices: DirectXDevices) -> Self {
+    fn new(directx_devices: Option<DirectXDevices>) -> Self {
         let callbacks = PlatformCallbacks::default();
         let jump_list = JumpList::new();
         let current_cursor = load_cursor(CursorStyle::Arrow);
-        let directx_devices = Some(directx_devices);
 
         Self {
             callbacks,
@@ -93,11 +94,29 @@ impl WindowsPlatformState {
 }
 
 impl WindowsPlatform {
-    pub(crate) fn new() -> Result<Self> {
+    pub(crate) fn new(headless: bool) -> Result<Self> {
         unsafe {
             OleInitialize(None).context("unable to initialize Windows OLE")?;
         }
-        let directx_devices = DirectXDevices::new().context("Creating DirectX devices")?;
+        let (directx_devices, text_system, direct_write_text_system) = if !headless {
+            let devices = DirectXDevices::new().context("Creating DirectX devices")?;
+            let dw_text_system = Arc::new(
+                DirectWriteTextSystem::new(&devices)
+                    .context("Error creating DirectWriteTextSystem")?,
+            );
+            (
+                Some(devices),
+                dw_text_system.clone() as Arc<dyn PlatformTextSystem>,
+                Some(dw_text_system),
+            )
+        } else {
+            (
+                None,
+                Arc::new(crate::NoopTextSystem::new()) as Arc<dyn PlatformTextSystem>,
+                None,
+            )
+        };
+
         let (main_sender, main_receiver) = PriorityQueueReceiver::new();
         let validation_number = if usize::BITS == 64 {
             rand::random::<u64>() as usize
@@ -105,10 +124,7 @@ impl WindowsPlatform {
             rand::random::<u32>() as usize
         };
         let raw_window_handles = Arc::new(RwLock::new(SmallVec::new()));
-        let text_system = Arc::new(
-            DirectWriteTextSystem::new(&directx_devices)
-                .context("Error creating DirectWriteTextSystem")?,
-        );
+
         register_platform_window_class();
         let mut context = PlatformWindowCreateContext {
             inner: None,
@@ -116,7 +132,7 @@ impl WindowsPlatform {
             validation_number,
             main_sender: Some(main_sender),
             main_receiver: Some(main_receiver),
-            directx_devices: Some(directx_devices),
+            directx_devices,
             dispatcher: None,
         };
         let result = unsafe {
@@ -150,21 +166,31 @@ impl WindowsPlatform {
         let background_executor = BackgroundExecutor::new(dispatcher.clone());
         let foreground_executor = ForegroundExecutor::new(dispatcher);
 
-        let drop_target_helper: IDropTargetHelper = unsafe {
-            CoCreateInstance(&CLSID_DragDropHelper, None, CLSCTX_INPROC_SERVER)
-                .context("Error creating drop target helper.")?
+        let drop_target_helper: Option<IDropTargetHelper> = if !headless {
+            Some(unsafe {
+                CoCreateInstance(&CLSID_DragDropHelper, None, CLSCTX_INPROC_SERVER)
+                    .context("Error creating drop target helper.")?
+            })
+        } else {
+            None
+        };
+        let icon = if !headless {
+            load_icon().unwrap_or_default()
+        } else {
+            HICON::default()
         };
-        let icon = load_icon().unwrap_or_default();
         let windows_version = WindowsVersion::new().context("Error retrieve windows version")?;
 
         Ok(Self {
             inner,
             handle,
             raw_window_handles,
+            headless,
             icon,
             background_executor,
             foreground_executor,
             text_system,
+            direct_write_text_system,
             disable_direct_composition,
             windows_version,
             drop_target_helper,
@@ -196,7 +222,7 @@ impl WindowsPlatform {
             executor: self.foreground_executor.clone(),
             current_cursor: self.inner.state.current_cursor.get(),
             windows_version: self.windows_version,
-            drop_target_helper: self.drop_target_helper.clone(),
+            drop_target_helper: self.drop_target_helper.clone().unwrap(),
             validation_number: self.inner.validation_number,
             main_receiver: self.inner.main_receiver.clone(),
             platform_window_handle: self.handle,
@@ -247,11 +273,17 @@ impl WindowsPlatform {
     }
 
     fn begin_vsync_thread(&self) {
-        let mut directx_device = self.inner.state.directx_devices.borrow().clone().unwrap();
+        let Some(directx_devices) = self.inner.state.directx_devices.borrow().clone() else {
+            return;
+        };
+        let Some(direct_write_text_system) = &self.direct_write_text_system else {
+            return;
+        };
+        let mut directx_device = directx_devices;
         let platform_window: SafeHwnd = self.handle.into();
         let validation_number = self.inner.validation_number;
         let all_windows = Arc::downgrade(&self.raw_window_handles);
-        let text_system = Arc::downgrade(&self.text_system);
+        let text_system = Arc::downgrade(direct_write_text_system);
         let invalidate_devices = self.invalidate_devices.clone();
 
         std::thread::Builder::new()
@@ -338,7 +370,9 @@ impl Platform for WindowsPlatform {
 
     fn run(&self, on_finish_launching: Box<dyn 'static + FnOnce()>) {
         on_finish_launching();
-        self.begin_vsync_thread();
+        if !self.headless {
+            self.begin_vsync_thread();
+        }
 
         let mut msg = MSG::default();
         unsafe {
@@ -733,12 +767,7 @@ impl Platform for WindowsPlatform {
 
 impl WindowsPlatformInner {
     fn new(context: &mut PlatformWindowCreateContext) -> Result<Rc<Self>> {
-        let state = WindowsPlatformState::new(
-            context
-                .directx_devices
-                .take()
-                .context("missing directx devices")?,
-        );
+        let state = WindowsPlatformState::new(context.directx_devices.take());
         Ok(Rc::new(Self {
             state,
             raw_window_handles: context.raw_window_handles.clone(),

crates/remote/src/remote_client.rs 🔗

@@ -447,9 +447,9 @@ impl RemoteClient {
                             error.push_str("Client exited with ");
                             match status {
                                 Ok(exit_code) => {
-                                    error.push_str(&format!(" exit_code {exit_code:?}"))
+                                    error.push_str(&format!("exit_code {exit_code:?}"))
                                 }
-                                Err(e) => error.push_str(&format!(" error {e:?}")),
+                                Err(e) => error.push_str(&format!("error {e:?}")),
                             }
                         } else {
                             error.push_str("client did not become ready within the timeout");

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

@@ -678,11 +678,16 @@ impl SshRemoteConnection {
             _ => Ok(Some(AppVersion::global(cx))),
         })?;
 
-        let tmp_path_gz = remote_server_dir_relative().join(
+        let tmp_path_compressed = remote_server_dir_relative().join(
             RelPath::unix(&format!(
-                "{}-download-{}.gz",
+                "{}-download-{}.{}",
                 binary_name,
-                std::process::id()
+                std::process::id(),
+                if self.ssh_platform.os.is_windows() {
+                    "zip"
+                } else {
+                    "gz"
+                }
             ))
             .unwrap(),
         );
@@ -697,11 +702,11 @@ impl SshRemoteConnection {
                 .await?
         {
             match self
-                .download_binary_on_server(&url, &tmp_path_gz, delegate, cx)
+                .download_binary_on_server(&url, &tmp_path_compressed, delegate, cx)
                 .await
             {
                 Ok(_) => {
-                    self.extract_server_binary(&dst_path, &tmp_path_gz, delegate, cx)
+                    self.extract_server_binary(&dst_path, &tmp_path_compressed, delegate, cx)
                         .await
                         .context("extracting server binary")?;
                     return Ok(dst_path);
@@ -723,10 +728,10 @@ impl SshRemoteConnection {
             )
             .await
             .context("downloading server binary locally")?;
-        self.upload_local_server_binary(&src_path, &tmp_path_gz, delegate, cx)
+        self.upload_local_server_binary(&src_path, &tmp_path_compressed, delegate, cx)
             .await
             .context("uploading server binary")?;
-        self.extract_server_binary(&dst_path, &tmp_path_gz, delegate, cx)
+        self.extract_server_binary(&dst_path, &tmp_path_compressed, delegate, cx)
             .await
             .context("extracting server binary")?;
         Ok(dst_path)
@@ -735,11 +740,11 @@ impl SshRemoteConnection {
     async fn download_binary_on_server(
         &self,
         url: &str,
-        tmp_path_gz: &RelPath,
+        tmp_path: &RelPath,
         delegate: &Arc<dyn RemoteClientDelegate>,
         cx: &mut AsyncApp,
     ) -> Result<()> {
-        if let Some(parent) = tmp_path_gz.parent() {
+        if let Some(parent) = tmp_path.parent() {
             let res = self
                 .socket
                 .run_command(
@@ -776,7 +781,7 @@ impl SshRemoteConnection {
                     &connection_timeout,
                     url,
                     "-o",
-                    &tmp_path_gz.display(self.path_style()),
+                    &tmp_path.display(self.path_style()),
                 ],
                 true,
             )
@@ -806,7 +811,7 @@ impl SshRemoteConnection {
                             "1",
                             url,
                             "-O",
-                            &tmp_path_gz.display(self.path_style()),
+                            &tmp_path.display(self.path_style()),
                         ],
                         true,
                     )
@@ -835,11 +840,11 @@ impl SshRemoteConnection {
     async fn upload_local_server_binary(
         &self,
         src_path: &Path,
-        tmp_path_gz: &RelPath,
+        tmp_path: &RelPath,
         delegate: &Arc<dyn RemoteClientDelegate>,
         cx: &mut AsyncApp,
     ) -> Result<()> {
-        if let Some(parent) = tmp_path_gz.parent() {
+        if let Some(parent) = tmp_path.parent() {
             let res = self
                 .socket
                 .run_command(
@@ -864,10 +869,10 @@ impl SshRemoteConnection {
         delegate.set_status(Some("Uploading remote development server"), cx);
         log::info!(
             "uploading remote development server to {:?} ({}kb)",
-            tmp_path_gz,
+            tmp_path,
             size / 1024
         );
-        self.upload_file(src_path, tmp_path_gz)
+        self.upload_file(src_path, tmp_path)
             .await
             .context("failed to upload server binary")?;
         log::info!("uploaded remote development server in {:?}", t0.elapsed());
@@ -882,9 +887,21 @@ impl SshRemoteConnection {
         cx: &mut AsyncApp,
     ) -> Result<()> {
         delegate.set_status(Some("Extracting remote development server"), cx);
-        let server_mode = 0o755;
 
+        if self.ssh_platform.os.is_windows() {
+            self.extract_server_binary_windows(dst_path, tmp_path).await
+        } else {
+            self.extract_server_binary_posix(dst_path, tmp_path).await
+        }
+    }
+
+    async fn extract_server_binary_posix(
+        &self,
+        dst_path: &RelPath,
+        tmp_path: &RelPath,
+    ) -> Result<()> {
         let shell_kind = ShellKind::Posix;
+        let server_mode = 0o755;
         let orig_tmp_path = tmp_path.display(self.path_style());
         let server_mode = format!("{:o}", server_mode);
         let server_mode = shell_kind
@@ -913,6 +930,39 @@ impl SshRemoteConnection {
         Ok(())
     }
 
+    async fn extract_server_binary_windows(
+        &self,
+        dst_path: &RelPath,
+        tmp_path: &RelPath,
+    ) -> Result<()> {
+        let shell_kind = ShellKind::Pwsh;
+        let orig_tmp_path = tmp_path.display(self.path_style());
+        let dst_path = dst_path.display(self.path_style());
+        let dst_path = shell_kind.try_quote(&dst_path).context("shell quoting")?;
+
+        let script = if let Some(tmp_path) = orig_tmp_path.strip_suffix(".zip") {
+            let orig_tmp_path = shell_kind
+                .try_quote(&orig_tmp_path)
+                .context("shell quoting")?;
+            let tmp_path = shell_kind.try_quote(tmp_path).context("shell quoting")?;
+            format!(
+                "Expand-Archive -Force -Path {orig_tmp_path} -DestinationPath {tmp_path} -ErrorAction Stop;
+                 Move-Item -Force {tmp_path} {dst_path}",
+            )
+        } else {
+            let orig_tmp_path = shell_kind
+                .try_quote(&orig_tmp_path)
+                .context("shell quoting")?;
+            format!("Move-Item -Force {orig_tmp_path} {dst_path}")
+        };
+
+        let args = shell_kind.args_for_shell(false, script);
+        self.socket
+            .run_command(self.ssh_shell_kind, "powershell", &args, true)
+            .await?;
+        Ok(())
+    }
+
     fn build_scp_command(
         &self,
         src_path: &Path,
@@ -1075,8 +1125,12 @@ impl SshSocket {
             to_run.push(' ');
             to_run.push_str(&shell_kind.try_quote(arg.as_ref()).expect("shell quoting"));
         }
-        let separator = shell_kind.sequential_commands_separator();
-        let to_run = format!("cd{separator} {to_run}");
+        let to_run = if shell_kind == ShellKind::Cmd {
+            to_run // 'cd' prints the current directory in CMD
+        } else {
+            let separator = shell_kind.sequential_commands_separator();
+            format!("cd{separator} {to_run}")
+        };
         self.ssh_options(&mut command, true)
             .arg(self.connection_options.ssh_destination());
         if !allow_pseudo_tty {
@@ -1188,7 +1242,7 @@ impl SshSocket {
         let output = self
             .run_command(
                 shell,
-                "cmd",
+                "cmd.exe",
                 &["/c", "echo", "%PROCESSOR_ARCHITECTURE%"],
                 false,
             )
@@ -1215,7 +1269,7 @@ impl SshSocket {
     /// If it succeeds and returns Windows-like output, we assume it's Windows.
     async fn probe_is_windows(&self) -> bool {
         match self
-            .run_command(ShellKind::PowerShell, "cmd", &["/c", "ver"], false)
+            .run_command(ShellKind::Cmd, "cmd.exe", &["/c", "ver"], false)
             .await
         {
             // Windows 'ver' command outputs something like "Microsoft Windows [Version 10.0.19045.5011]"
@@ -1247,10 +1301,31 @@ impl SshSocket {
     }
 
     async fn shell_windows(&self) -> String {
-        // powershell is always the default, and cannot really be removed from the system
-        // so we can rely on that fact and reasonably assume that we will be running in a
-        // powershell environment
-        "powershell.exe".to_owned()
+        const DEFAULT_SHELL: &str = "cmd.exe";
+
+        // We detect the shell used by the SSH session by running the following command in PowerShell:
+        // (Get-CimInstance Win32_Process -Filter "ProcessId = $((Get-CimInstance Win32_Process -Filter ProcessId=$PID).ParentProcessId)").Name
+        // This prints the name of PowerShell's parent process (which will be the shell that SSH launched).
+        // We pass it as a Base64 encoded string since we don't yet know how to correctly quote that command.
+        // (We'd need to know what the shell is to do that...)
+        match self
+            .run_command(
+                ShellKind::Cmd,
+                "powershell",
+                &[
+                    "-E",
+                    "KABHAGUAdAAtAEMAaQBtAEkAbgBzAHQAYQBuAGMAZQAgAFcAaQBuADMAMgBfAFAAcgBvAGMAZQBzAHMAIAAtAEYAaQBsAHQAZQByACAAIgBQAHIAbwBjAGUAcwBzAEkAZAAgAD0AIAAkACgAKABHAGUAdAAtAEMAaQBtAEkAbgBzAHQAYQBuAGMAZQAgAFcAaQBuADMAMgBfAFAAcgBvAGMAZQBzAHMAIAAtAEYAaQBsAHQAZQByACAAUAByAG8AYwBlAHMAcwBJAGQAPQAkAFAASQBEACkALgBQAGEAcgBlAG4AdABQAHIAbwBjAGUAcwBzAEkAZAApACIAKQAuAE4AYQBtAGUA",
+                ],
+                false,
+            )
+            .await
+        {
+            Ok(output) => parse_shell(&output, DEFAULT_SHELL),
+            Err(e) => {
+                log::error!("Failed to detect remote shell: {e}");
+                DEFAULT_SHELL.to_owned()
+            }
+        }
     }
 }
 

crates/remote_server/Cargo.toml 🔗

@@ -10,7 +10,7 @@ license = "GPL-3.0-or-later"
 workspace = true
 
 [lib]
-path = "src/remote_server.rs"
+path = "src/server.rs"
 doctest = false
 
 [[bin]]
@@ -38,7 +38,7 @@ futures.workspace = true
 git.workspace = true
 git_hosting_providers.workspace = true
 git2 = { workspace = true, features = ["vendored-libgit2"] }
-gpui.workspace = true
+gpui = { workspace = true, features = ["windows-manifest"] }
 gpui_tokio.workspace = true
 http_client.workspace = true
 image.workspace = true
@@ -48,6 +48,7 @@ language_extension.workspace = true
 languages.workspace = true
 log.workspace = true
 lsp.workspace = true
+net.workspace = true
 node_runtime.workspace = true
 paths.workspace = true
 project.workspace = true
@@ -77,6 +78,9 @@ fork.workspace = true
 libc.workspace = true
 minidumper.workspace = true
 
+[target.'cfg(windows)'.dependencies]
+windows.workspace = true
+
 [dev-dependencies]
 action_log.workspace = true
 agent = { workspace = true, features = ["test-support"] }

crates/remote_server/src/main.rs 🔗

@@ -38,9 +38,8 @@ fn main() -> anyhow::Result<()> {
         return Ok(());
     }
 
-    #[cfg(not(windows))]
     if let Some(command) = cli.command {
-        use remote_server::unix::ExecuteProxyError;
+        use remote_server::ExecuteProxyError;
 
         let res = remote_server::run(command);
         if let Err(e) = &res
@@ -58,13 +57,4 @@ fn main() -> anyhow::Result<()> {
         eprintln!("usage: remote <run|proxy|version>");
         std::process::exit(1);
     }
-
-    #[cfg(windows)]
-    if let Some(_) = cli.command {
-        eprintln!("run is not supported on Windows");
-        std::process::exit(2);
-    } else {
-        eprintln!("usage: remote <run|proxy|version>");
-        std::process::exit(1);
-    }
 }

crates/remote_server/src/remote_server.rs 🔗

@@ -1,81 +0,0 @@
-mod headless_project;
-
-#[cfg(not(windows))]
-pub mod unix;
-
-#[cfg(test)]
-mod remote_editing_tests;
-
-use clap::Subcommand;
-use std::path::PathBuf;
-
-pub use headless_project::{HeadlessAppState, HeadlessProject};
-
-#[derive(Subcommand)]
-pub enum Commands {
-    Run {
-        #[arg(long)]
-        log_file: PathBuf,
-        #[arg(long)]
-        pid_file: PathBuf,
-        #[arg(long)]
-        stdin_socket: PathBuf,
-        #[arg(long)]
-        stdout_socket: PathBuf,
-        #[arg(long)]
-        stderr_socket: PathBuf,
-    },
-    Proxy {
-        #[arg(long)]
-        reconnect: bool,
-        #[arg(long)]
-        identifier: String,
-    },
-    Version,
-}
-
-#[cfg(not(windows))]
-pub fn run(command: Commands) -> anyhow::Result<()> {
-    use anyhow::Context;
-    use release_channel::{RELEASE_CHANNEL, ReleaseChannel};
-    use unix::{execute_proxy, execute_run};
-
-    match command {
-        Commands::Run {
-            log_file,
-            pid_file,
-            stdin_socket,
-            stdout_socket,
-            stderr_socket,
-        } => execute_run(
-            log_file,
-            pid_file,
-            stdin_socket,
-            stdout_socket,
-            stderr_socket,
-        ),
-        Commands::Proxy {
-            identifier,
-            reconnect,
-        } => execute_proxy(identifier, reconnect).context("running proxy on the remote server"),
-        Commands::Version => {
-            let release_channel = *RELEASE_CHANNEL;
-            match release_channel {
-                ReleaseChannel::Stable | ReleaseChannel::Preview => {
-                    println!("{}", env!("ZED_PKG_VERSION"))
-                }
-                ReleaseChannel::Nightly | ReleaseChannel::Dev => {
-                    let commit_sha =
-                        option_env!("ZED_COMMIT_SHA").unwrap_or(release_channel.dev_name());
-                    let build_id = option_env!("ZED_BUILD_ID");
-                    if let Some(build_id) = build_id {
-                        println!("{}+{}", build_id, commit_sha)
-                    } else {
-                        println!("{commit_sha}");
-                    }
-                }
-            };
-            Ok(())
-        }
-    }
-}

crates/remote_server/src/unix.rs → crates/remote_server/src/server.rs 🔗

@@ -1,29 +1,37 @@
-use crate::HeadlessProject;
-use crate::headless_project::HeadlessAppState;
+mod headless_project;
+
+#[cfg(test)]
+mod remote_editing_tests;
+
+#[cfg(windows)]
+pub mod windows;
+
+pub use headless_project::{HeadlessAppState, HeadlessProject};
+
 use anyhow::{Context as _, Result, anyhow};
+use clap::Subcommand;
 use client::ProxySettings;
 use collections::HashMap;
-use project::trusted_worktrees;
-use util::ResultExt;
-
 use extension::ExtensionHostProxy;
 use fs::{Fs, RealFs};
-use futures::channel::{mpsc, oneshot};
-use futures::{AsyncRead, AsyncWrite, AsyncWriteExt, FutureExt, SinkExt, select, select_biased};
+use futures::{
+    AsyncRead, AsyncWrite, AsyncWriteExt, FutureExt, SinkExt,
+    channel::{mpsc, oneshot},
+    select, select_biased,
+};
 use git::GitHostingProviderRegistry;
 use gpui::{App, AppContext as _, Context, Entity, UpdateGlobal as _};
 use gpui_tokio::Tokio;
 use http_client::{Url, read_proxy_from_env};
 use language::LanguageRegistry;
+use net::async_net::{UnixListener, UnixStream};
 use node_runtime::{NodeBinaryOptions, NodeRuntime};
 use paths::logs_dir;
-use project::project_settings::ProjectSettings;
-use util::command::new_smol_command;
-
+use project::{project_settings::ProjectSettings, trusted_worktrees};
 use proto::CrashReport;
 use release_channel::{AppCommitSha, AppVersion, RELEASE_CHANNEL, ReleaseChannel};
-use remote::RemoteClient;
 use remote::{
+    RemoteClient,
     json_log::LogRecord,
     protocol::{read_message, write_message},
     proxy::ProxyLaunchError,
@@ -32,23 +40,90 @@ use reqwest_client::ReqwestClient;
 use rpc::proto::{self, Envelope, REMOTE_SERVER_PROJECT_ID};
 use rpc::{AnyProtoClient, TypedEnvelope};
 use settings::{Settings, SettingsStore, watch_config_file};
-
-use smol::channel::{Receiver, Sender};
-use smol::io::AsyncReadExt;
-use smol::{net::unix::UnixListener, stream::StreamExt as _};
+use smol::{
+    channel::{Receiver, Sender},
+    io::AsyncReadExt,
+    stream::StreamExt as _,
+};
 use std::{
     env,
     ffi::OsStr,
     fs::File,
     io::Write,
     mem,
-    ops::ControlFlow,
     path::{Path, PathBuf},
-    process::ExitStatus,
     str::FromStr,
     sync::{Arc, LazyLock},
 };
 use thiserror::Error;
+use util::{ResultExt, command::new_smol_command};
+
+#[derive(Subcommand)]
+pub enum Commands {
+    Run {
+        #[arg(long)]
+        log_file: PathBuf,
+        #[arg(long)]
+        pid_file: PathBuf,
+        #[arg(long)]
+        stdin_socket: PathBuf,
+        #[arg(long)]
+        stdout_socket: PathBuf,
+        #[arg(long)]
+        stderr_socket: PathBuf,
+    },
+    Proxy {
+        #[arg(long)]
+        reconnect: bool,
+        #[arg(long)]
+        identifier: String,
+    },
+    Version,
+}
+
+pub fn run(command: Commands) -> anyhow::Result<()> {
+    use anyhow::Context;
+    use release_channel::{RELEASE_CHANNEL, ReleaseChannel};
+
+    match command {
+        Commands::Run {
+            log_file,
+            pid_file,
+            stdin_socket,
+            stdout_socket,
+            stderr_socket,
+        } => execute_run(
+            log_file,
+            pid_file,
+            stdin_socket,
+            stdout_socket,
+            stderr_socket,
+        ),
+        Commands::Proxy {
+            identifier,
+            reconnect,
+        } => execute_proxy(identifier, reconnect).context("running proxy on the remote server"),
+        Commands::Version => {
+            let release_channel = *RELEASE_CHANNEL;
+            match release_channel {
+                ReleaseChannel::Stable | ReleaseChannel::Preview => {
+                    println!("{}", env!("ZED_PKG_VERSION"))
+                }
+                ReleaseChannel::Nightly | ReleaseChannel::Dev => {
+                    let commit_sha =
+                        option_env!("ZED_COMMIT_SHA").unwrap_or(release_channel.dev_name());
+                    let build_id = option_env!("ZED_BUILD_ID");
+                    if let Some(build_id) = build_id {
+                        println!("{}+{}", build_id, commit_sha)
+                    } else {
+                        println!("{commit_sha}");
+                    }
+                }
+            };
+            Ok(())
+        }
+    }
+}
 
 pub static VERSION: LazyLock<String> = LazyLock::new(|| match *RELEASE_CHANNEL {
     ReleaseChannel::Stable | ReleaseChannel::Preview => env!("ZED_PKG_VERSION").to_owned(),
@@ -238,17 +313,17 @@ fn start_server(
     .detach();
 
     cx.spawn(async move |cx| {
-        let mut stdin_incoming = listeners.stdin.incoming();
-        let mut stdout_incoming = listeners.stdout.incoming();
-        let mut stderr_incoming = listeners.stderr.incoming();
-
         loop {
-            let streams = futures::future::join3(stdin_incoming.next(), stdout_incoming.next(), stderr_incoming.next());
+            let streams = futures::future::join3(
+                listeners.stdin.accept(),
+                listeners.stdout.accept(),
+                listeners.stderr.accept(),
+            );
 
             log::info!("accepting new connections");
             let result = select! {
                 streams = streams.fuse() => {
-                    let (Some(Ok(stdin_stream)), Some(Ok(stdout_stream)), Some(Ok(stderr_stream))) = streams else {
+                    let (Ok((stdin_stream, _)), Ok((stdout_stream, _)), Ok((stderr_stream, _))) = streams else {
                         log::error!("failed to accept new connections");
                         break;
                     };
@@ -372,11 +447,6 @@ pub fn execute_run(
 ) -> Result<()> {
     init_paths()?;
 
-    match daemonize()? {
-        ControlFlow::Break(_) => return Ok(()),
-        ControlFlow::Continue(_) => {}
-    }
-
     let app = gpui::Application::headless();
     let pid = std::process::id();
     let id = pid.to_string();
@@ -412,13 +482,19 @@ pub fn execute_run(
         .build_global()
         .unwrap();
 
-    let (shell_env_loaded_tx, shell_env_loaded_rx) = oneshot::channel();
-    app.background_executor()
-        .spawn(async {
-            util::load_login_shell_environment().await.log_err();
-            shell_env_loaded_tx.send(()).ok();
-        })
-        .detach();
+    #[cfg(unix)]
+    let shell_env_loaded_rx = {
+        let (shell_env_loaded_tx, shell_env_loaded_rx) = oneshot::channel();
+        app.background_executor()
+            .spawn(async {
+                util::load_login_shell_environment().await.log_err();
+                shell_env_loaded_tx.send(()).ok();
+            })
+            .detach();
+        Some(shell_env_loaded_rx)
+    };
+    #[cfg(windows)]
+    let shell_env_loaded_rx: Option<oneshot::Receiver<()>> = None;
 
     let git_hosting_provider_registry = Arc::new(GitHostingProviderRegistry::new());
     let run = move |cx: &mut _| {
@@ -476,11 +552,8 @@ pub fn execute_run(
                 )
             };
 
-            let node_runtime = NodeRuntime::new(
-                http_client.clone(),
-                Some(shell_env_loaded_rx),
-                node_settings_rx,
-            );
+            let node_runtime =
+                NodeRuntime::new(http_client.clone(), shell_env_loaded_rx, node_settings_rx);
 
             let mut languages = LanguageRegistry::new(cx.background_executor().clone());
             languages.set_language_server_download_dir(paths::languages_dir().clone());
@@ -591,14 +664,10 @@ pub enum ExecuteProxyError {
         path: PathBuf,
     },
 
-    #[error("Failed to kill existing server with pid '{pid}': {source:#}")]
-    KillRunningServer {
-        #[source]
-        source: std::io::Error,
-        pid: u32,
-    },
+    #[error("Failed to kill existing server with pid '{pid}'")]
+    KillRunningServer { pid: u32 },
 
-    #[error("failed to spawn server: {0:#}")]
+    #[error("failed to spawn server")]
     SpawnServer(#[source] SpawnServerError),
 
     #[error("stdin_task failed: {0:#}")]
@@ -639,22 +708,22 @@ pub(crate) fn execute_proxy(
     .detach();
 
     log::info!("starting proxy process. PID: {}", std::process::id());
-    let server_pid = smol::block_on(async {
-        let server_pid = check_pid_file(&server_paths.pid_file)
-            .await
-            .map_err(|source| ExecuteProxyError::CheckPidFile {
+    let server_pid = {
+        let server_pid = check_pid_file(&server_paths.pid_file).map_err(|source| {
+            ExecuteProxyError::CheckPidFile {
                 source,
                 path: server_paths.pid_file.clone(),
-            })?;
+            }
+        })?;
         if is_reconnecting {
             match server_pid {
                 None => {
                     log::error!("attempted to reconnect, but no server running");
-                    Err(ExecuteProxyError::ServerNotRunning(
+                    return Err(ExecuteProxyError::ServerNotRunning(
                         ProxyLaunchError::ServerNotRunning,
-                    ))
+                    ));
                 }
-                Some(server_pid) => Ok(server_pid),
+                Some(server_pid) => server_pid,
             }
         } else {
             if let Some(pid) = server_pid {
@@ -662,11 +731,9 @@ pub(crate) fn execute_proxy(
                     "proxy found server already running with PID {}. Killing process and cleaning up files...",
                     pid
                 );
-                kill_running_server(pid, &server_paths).await?;
+                kill_running_server(pid, &server_paths)?;
             }
-            spawn_server(&server_paths)
-                .await
-                .map_err(ExecuteProxyError::SpawnServer)?;
+            smol::block_on(spawn_server(&server_paths)).map_err(ExecuteProxyError::SpawnServer)?;
             std::fs::read_to_string(&server_paths.pid_file)
                 .and_then(|contents| {
                     contents.parse::<u32>().map_err(|_| {
@@ -677,13 +744,13 @@ pub(crate) fn execute_proxy(
                     })
                 })
                 .map_err(SpawnServerError::ProcessStatus)
-                .map_err(ExecuteProxyError::SpawnServer)
+                .map_err(ExecuteProxyError::SpawnServer)?
         }
-    })?;
+    };
 
     let stdin_task = smol::spawn(async move {
         let stdin = smol::Unblock::new(std::io::stdin());
-        let stream = smol::net::unix::UnixStream::connect(&server_paths.stdin_socket)
+        let stream = UnixStream::connect(&server_paths.stdin_socket)
             .await
             .with_context(|| {
                 format!(
@@ -696,7 +763,7 @@ pub(crate) fn execute_proxy(
 
     let stdout_task: smol::Task<Result<()>> = smol::spawn(async move {
         let stdout = smol::Unblock::new(std::io::stdout());
-        let stream = smol::net::unix::UnixStream::connect(&server_paths.stdout_socket)
+        let stream = UnixStream::connect(&server_paths.stdout_socket)
             .await
             .with_context(|| {
                 format!(
@@ -709,7 +776,7 @@ pub(crate) fn execute_proxy(
 
     let stderr_task: smol::Task<Result<()>> = smol::spawn(async move {
         let mut stderr = smol::Unblock::new(std::io::stderr());
-        let mut stream = smol::net::unix::UnixStream::connect(&server_paths.stderr_socket)
+        let mut stream = UnixStream::connect(&server_paths.stderr_socket)
             .await
             .with_context(|| {
                 format!(
@@ -757,13 +824,18 @@ pub(crate) fn execute_proxy(
     Ok(())
 }
 
-async fn kill_running_server(pid: u32, paths: &ServerPaths) -> Result<(), ExecuteProxyError> {
+fn kill_running_server(pid: u32, paths: &ServerPaths) -> Result<(), ExecuteProxyError> {
     log::info!("killing existing server with PID {}", pid);
-    new_smol_command("kill")
-        .arg(pid.to_string())
-        .output()
-        .await
-        .map_err(|source| ExecuteProxyError::KillRunningServer { source, pid })?;
+    let system = sysinfo::System::new_with_specifics(
+        sysinfo::RefreshKind::nothing().with_processes(sysinfo::ProcessRefreshKind::nothing()),
+    );
+
+    if let Some(process) = system.process(sysinfo::Pid::from_u32(pid)) {
+        let killed = process.kill();
+        if !killed {
+            return Err(ExecuteProxyError::KillRunningServer { pid });
+        }
+    }
 
     for file in [
         &paths.pid_file,
@@ -774,6 +846,7 @@ async fn kill_running_server(pid: u32, paths: &ServerPaths) -> Result<(), Execut
         log::debug!("cleaning up file {:?} before starting new server", file);
         std::fs::remove_file(file).ok();
     }
+
     Ok(())
 }
 
@@ -794,9 +867,6 @@ pub enum SpawnServerError {
     #[error("failed to launch server process")]
     ProcessStatus(#[source] std::io::Error),
 
-    #[error("failed to launch and detach server process: {status}\n{paths}")]
-    LaunchStatus { status: ExitStatus, paths: String },
-
     #[error("failed to wait for server to be ready to accept connections")]
     Timeout,
 }
@@ -814,33 +884,15 @@ async fn spawn_server(paths: &ServerPaths) -> Result<(), SpawnServerError> {
     }
 
     let binary_name = std::env::current_exe().map_err(SpawnServerError::CurrentExe)?;
-    let mut server_process = new_smol_command(binary_name);
-    server_process
-        .arg("run")
-        .arg("--log-file")
-        .arg(&paths.log_file)
-        .arg("--pid-file")
-        .arg(&paths.pid_file)
-        .arg("--stdin-socket")
-        .arg(&paths.stdin_socket)
-        .arg("--stdout-socket")
-        .arg(&paths.stdout_socket)
-        .arg("--stderr-socket")
-        .arg(&paths.stderr_socket);
 
-    let status = server_process
-        .status()
-        .await
-        .map_err(SpawnServerError::ProcessStatus)?;
+    #[cfg(windows)]
+    {
+        spawn_server_windows(&binary_name, paths)?;
+    }
 
-    if !status.success() {
-        return Err(SpawnServerError::LaunchStatus {
-            status,
-            paths: format!(
-                "log file: {:?}, pid file: {:?}",
-                paths.log_file, paths.pid_file,
-            ),
-        });
+    #[cfg(not(windows))]
+    {
+        spawn_server_normal(&binary_name, paths)?;
     }
 
     let mut total_time_waited = std::time::Duration::from_secs(0);
@@ -865,6 +917,55 @@ async fn spawn_server(paths: &ServerPaths) -> Result<(), SpawnServerError> {
     Ok(())
 }
 
+#[cfg(windows)]
+fn spawn_server_windows(binary_name: &Path, paths: &ServerPaths) -> Result<(), SpawnServerError> {
+    let binary_path = binary_name.to_string_lossy().to_string();
+    let parameters = format!(
+        "run --log-file \"{}\" --pid-file \"{}\" --stdin-socket \"{}\" --stdout-socket \"{}\" --stderr-socket \"{}\"",
+        paths.log_file.to_string_lossy(),
+        paths.pid_file.to_string_lossy(),
+        paths.stdin_socket.to_string_lossy(),
+        paths.stdout_socket.to_string_lossy(),
+        paths.stderr_socket.to_string_lossy()
+    );
+
+    let directory = binary_name
+        .parent()
+        .map(|p| p.to_string_lossy().to_string())
+        .unwrap_or_default();
+
+    crate::windows::shell_execute_from_explorer(&binary_path, &parameters, &directory)
+        .map_err(|e| SpawnServerError::ProcessStatus(std::io::Error::other(e)))?;
+
+    Ok(())
+}
+
+#[cfg(not(windows))]
+fn spawn_server_normal(binary_name: &Path, paths: &ServerPaths) -> Result<(), SpawnServerError> {
+    let mut server_process = new_smol_command(binary_name);
+    server_process
+        .stdin(std::process::Stdio::null())
+        .stdout(std::process::Stdio::null())
+        .stderr(std::process::Stdio::null())
+        .arg("run")
+        .arg("--log-file")
+        .arg(&paths.log_file)
+        .arg("--pid-file")
+        .arg(&paths.pid_file)
+        .arg("--stdin-socket")
+        .arg(&paths.stdin_socket)
+        .arg("--stdout-socket")
+        .arg(&paths.stdout_socket)
+        .arg("--stderr-socket")
+        .arg(&paths.stderr_socket);
+
+    server_process
+        .spawn()
+        .map_err(SpawnServerError::ProcessStatus)?;
+
+    Ok(())
+}
+
 #[derive(Debug, Error)]
 #[error("Failed to remove PID file for missing process (pid `{pid}`")]
 pub struct CheckPidError {
@@ -881,8 +982,8 @@ async fn check_server_running(pid: u32) -> std::io::Result<bool> {
         .map(|output| output.status.success())
 }
 
-async fn check_pid_file(path: &Path) -> Result<Option<u32>, CheckPidError> {
-    let Some(pid) = std::fs::read_to_string(&path)
+fn check_pid_file(path: &Path) -> Result<Option<u32>, CheckPidError> {
+    let Some(pid) = std::fs::read_to_string(path)
         .ok()
         .and_then(|contents| contents.parse::<u32>().ok())
     else {
@@ -890,21 +991,21 @@ async fn check_pid_file(path: &Path) -> Result<Option<u32>, CheckPidError> {
     };
 
     log::debug!("Checking if process with PID {} exists...", pid);
-    match check_server_running(pid).await {
-        Ok(true) => {
-            log::debug!(
-                "Process with PID {} exists. NOT spawning new server, but attaching to existing one.",
-                pid
-            );
-            Ok(Some(pid))
-        }
-        _ => {
-            log::debug!(
-                "Found PID file, but process with that PID does not exist. Removing PID file."
-            );
-            std::fs::remove_file(&path).map_err(|source| CheckPidError { source, pid })?;
-            Ok(None)
-        }
+
+    let system = sysinfo::System::new_with_specifics(
+        sysinfo::RefreshKind::nothing().with_processes(sysinfo::ProcessRefreshKind::nothing()),
+    );
+
+    if system.process(sysinfo::Pid::from_u32(pid)).is_some() {
+        log::debug!(
+            "Process with PID {} exists. NOT spawning new server, but attaching to existing one.",
+            pid
+        );
+        Ok(Some(pid))
+    } else {
+        log::debug!("Found PID file, but process with that PID does not exist. Removing PID file.");
+        std::fs::remove_file(path).map_err(|source| CheckPidError { source, pid })?;
+        Ok(None)
     }
 }
 
@@ -1052,46 +1153,6 @@ fn read_proxy_settings(cx: &mut Context<HeadlessProject>) -> Option<Url> {
         .or_else(read_proxy_from_env)
 }
 
-fn daemonize() -> Result<ControlFlow<()>> {
-    match fork::fork().map_err(|e| anyhow!("failed to call fork with error code {e}"))? {
-        fork::Fork::Parent(_) => {
-            return Ok(ControlFlow::Break(()));
-        }
-        fork::Fork::Child => {}
-    }
-
-    // Once we've detached from the parent, we want to close stdout/stderr/stdin
-    // so that the outer SSH process is not attached to us in any way anymore.
-    unsafe { redirect_standard_streams() }?;
-
-    Ok(ControlFlow::Continue(()))
-}
-
-unsafe fn redirect_standard_streams() -> Result<()> {
-    let devnull_fd = unsafe { libc::open(b"/dev/null\0" as *const [u8; 10] as _, libc::O_RDWR) };
-    anyhow::ensure!(devnull_fd != -1, "failed to open /dev/null");
-
-    let process_stdio = |name, fd| {
-        let reopened_fd = unsafe { libc::dup2(devnull_fd, fd) };
-        anyhow::ensure!(
-            reopened_fd != -1,
-            format!("failed to redirect {} to /dev/null", name)
-        );
-        Ok(())
-    };
-
-    process_stdio("stdin", libc::STDIN_FILENO)?;
-    process_stdio("stdout", libc::STDOUT_FILENO)?;
-    process_stdio("stderr", libc::STDERR_FILENO)?;
-
-    anyhow::ensure!(
-        unsafe { libc::close(devnull_fd) != -1 },
-        "failed to close /dev/null fd after redirecting"
-    );
-
-    Ok(())
-}
-
 fn cleanup_old_binaries() -> Result<()> {
     let server_dir = paths::remote_server_dir_relative();
     let release_channel = release_channel::RELEASE_CHANNEL.dev_name();

crates/remote_server/src/windows.rs 🔗

@@ -0,0 +1,48 @@
+use windows::Win32::System::Com::{
+    CLSCTX_LOCAL_SERVER, COINIT_APARTMENTTHREADED, CoCreateInstance, CoInitializeEx, IDispatch,
+    IServiceProvider,
+};
+use windows::Win32::System::Variant::VARIANT;
+use windows::Win32::UI::Shell::{
+    CSIDL_DESKTOP, IShellBrowser, IShellDispatch2, IShellFolderViewDual, IShellWindows,
+    SID_STopLevelBrowser, SVGIO_BACKGROUND, SWC_DESKTOP, SWFO_NEEDDISPATCH, ShellWindows,
+};
+use windows::core::{BSTR, Interface};
+
+pub fn shell_execute_from_explorer(
+    file: &str,
+    parameters: &str,
+    directory: &str,
+) -> anyhow::Result<()> {
+    unsafe {
+        CoInitializeEx(None, COINIT_APARTMENTTHREADED).unwrap();
+
+        let mut _hwnd = Default::default();
+        let shell_dispatch: IShellDispatch2 =
+            CoCreateInstance::<_, IShellWindows>(&ShellWindows, None, CLSCTX_LOCAL_SERVER)?
+                .FindWindowSW(
+                    &VARIANT::from(CSIDL_DESKTOP as i32),
+                    &VARIANT::default(),
+                    SWC_DESKTOP,
+                    &mut _hwnd,
+                    SWFO_NEEDDISPATCH,
+                )?
+                .cast::<IServiceProvider>()?
+                .QueryService::<IShellBrowser>(&SID_STopLevelBrowser)?
+                .QueryActiveShellView()?
+                .GetItemObject::<IDispatch>(SVGIO_BACKGROUND)?
+                .cast::<IShellFolderViewDual>()?
+                .Application()?
+                .cast()?;
+
+        shell_dispatch.ShellExecute(
+            &BSTR::from(file),
+            &VARIANT::from(parameters),
+            &VARIANT::from(directory),
+            &VARIANT::from(""),
+            &VARIANT::from(0i32),
+        )?;
+
+        Ok(())
+    }
+}

script/bundle-windows.ps1 🔗

@@ -124,12 +124,32 @@ function BuildZedAndItsFriends {
     Copy-Item -Path ".\$CargoOutDir\explorer_command_injector.dll" -Destination "$innoDir\zed_explorer_command_injector.dll" -Force
 }
 
+function BuildRemoteServer {
+    Write-Output "Building remote_server for $target"
+    cargo build --release --package remote_server --target $target
+
+    # Create zipped remote server binary
+    $remoteServerSrc = (Resolve-Path ".\$CargoOutDir\remote_server.exe").Path
+
+    if ($env:CI) {
+        Write-Output "Code signing remote_server.exe"
+        & "$innoDir\sign.ps1" $remoteServerSrc
+    }
+
+    $remoteServerDst = "$env:ZED_WORKSPACE\target\zed-remote-server-windows-$Architecture.zip"
+    Write-Output "Compressing remote_server to $remoteServerDst"
+    Compress-Archive -Path $remoteServerSrc -DestinationPath $remoteServerDst -Force
+
+    Write-Output "Remote server compressed successfully"
+}
+
 function ZipZedAndItsFriendsDebug {
     $items = @(
         ".\$CargoOutDir\zed.pdb",
         ".\$CargoOutDir\cli.pdb",
         ".\$CargoOutDir\auto_update_helper.pdb",
-        ".\$CargoOutDir\explorer_command_injector.pdb"
+        ".\$CargoOutDir\explorer_command_injector.pdb",
+        ".\$CargoOutDir\remote_server.pdb"
     )
 
     Compress-Archive -Path $items -DestinationPath ".\$CargoOutDir\zed-$env:RELEASE_VERSION-$env:ZED_RELEASE_CHANNEL.dbg.zip" -Force
@@ -352,6 +372,7 @@ CheckEnvironmentVariables
 PrepareForBundle
 GenerateLicenses
 BuildZedAndItsFriends
+BuildRemoteServer
 MakeAppx
 SignZedAndItsFriends
 ZipZedAndItsFriendsDebug

script/upload-nightly.ps1 🔗

@@ -15,13 +15,13 @@ $bucketName = "zed-nightly-host"
 $releaseVersion = & "$PSScriptRoot\get-crate-version.ps1" zed
 $version = "$releaseVersion+nightly.$env:GITHUB_RUN_NUMBER.$env:GITHUB_SHA"
 
-# TODO:
-# Upload remote server files
-# $remoteServerFiles = Get-ChildItem -Path "target" -Filter "zed-remote-server-*.gz" -Recurse -File
-# foreach ($file in $remoteServerFiles) {
-#     Upload-ToBlobStore -BucketName $bucketName -FileToUpload $file.FullName -BlobStoreKey "nightly/$($file.Name)"
-#     Remove-Item -Path $file.FullName
-# }
+$remoteServerFiles = Get-ChildItem -Path "target" -Filter "zed-remote-server-windows-*.zip" -Recurse -File -ErrorAction SilentlyContinue
+
+foreach ($file in $remoteServerFiles) {
+    UploadToBlobStore -BucketName $bucketName -FileToUpload $file.FullName -BlobStoreKey "nightly/$($file.Name)"
+    UploadToBlobStore -BucketName $bucketName -FileToUpload $file.FullName -BlobStoreKey "$version/$($file.Name)"
+    Remove-Item -Path $file.FullName -ErrorAction SilentlyContinue
+}
 
 UploadToBlobStore -BucketName $bucketName -FileToUpload "target/Zed-$Architecture.exe" -BlobStoreKey "nightly/Zed-$Architecture.exe"
 UploadToBlobStore -BucketName $bucketName -FileToUpload "target/Zed-$Architecture.exe" -BlobStoreKey "$version/Zed-$Architecture.exe"

tooling/xtask/src/tasks/workflows/run_bundling.rs 🔗

@@ -155,6 +155,10 @@ pub(crate) fn bundle_windows(
         Arch::X86_64 => assets::WINDOWS_X86_64,
         Arch::AARCH64 => assets::WINDOWS_AARCH64,
     };
+    let remote_server_artifact_name = match arch {
+        Arch::X86_64 => assets::REMOTE_SERVER_WINDOWS_X86_64,
+        Arch::AARCH64 => assets::REMOTE_SERVER_WINDOWS_AARCH64,
+    };
     NamedJob {
         name: format!("bundle_windows_{arch}"),
         job: bundle_job(deps)
@@ -166,7 +170,10 @@ pub(crate) fn bundle_windows(
             })
             .add_step(steps::setup_sentry())
             .add_step(bundle_windows(arch))
-            .add_step(upload_artifact(&format!("target/{artifact_name}"))),
+            .add_step(upload_artifact(&format!("target/{artifact_name}")))
+            .add_step(upload_artifact(&format!(
+                "target/{remote_server_artifact_name}"
+            ))),
     }
 }
 

tooling/xtask/src/tasks/workflows/vars.rs 🔗

@@ -337,6 +337,8 @@ pub mod assets {
     pub const REMOTE_SERVER_MAC_X86_64: &str = "zed-remote-server-macos-x86_64.gz";
     pub const REMOTE_SERVER_LINUX_AARCH64: &str = "zed-remote-server-linux-aarch64.gz";
     pub const REMOTE_SERVER_LINUX_X86_64: &str = "zed-remote-server-linux-x86_64.gz";
+    pub const REMOTE_SERVER_WINDOWS_AARCH64: &str = "zed-remote-server-windows-aarch64.zip";
+    pub const REMOTE_SERVER_WINDOWS_X86_64: &str = "zed-remote-server-windows-x86_64.zip";
 
     pub fn all() -> Vec<&'static str> {
         vec![
@@ -350,6 +352,8 @@ pub mod assets {
             REMOTE_SERVER_MAC_X86_64,
             REMOTE_SERVER_LINUX_AARCH64,
             REMOTE_SERVER_LINUX_X86_64,
+            REMOTE_SERVER_WINDOWS_AARCH64,
+            REMOTE_SERVER_WINDOWS_X86_64,
         ]
     }
 }