From 9931c6f9444c87a0e48285a5c540f34fae2d2151 Mon Sep 17 00:00:00 2001 From: John Tur Date: Sat, 24 Jan 2026 13:15:01 -0500 Subject: [PATCH] Add SSH remote server for Windows (#47460) 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 --- .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 ---- .../remote_server/src/{unix.rs => server.rs} | 371 ++++++++++-------- crates/remote_server/src/windows.rs | 48 +++ script/bundle-windows.ps1 | 23 +- script/upload-nightly.ps1 | 14 +- .../xtask/src/tasks/workflows/run_bundling.rs | 9 +- tooling/xtask/src/tasks/workflows/vars.rs | 4 + 17 files changed, 511 insertions(+), 311 deletions(-) delete mode 100644 crates/remote_server/src/remote_server.rs rename crates/remote_server/src/{unix.rs => server.rs} (83%) create mode 100644 crates/remote_server/src/windows.rs diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c92daae67d0c182c9906f9fc155865a1ebbc22f1..50b0260b5a9b310770e3832b3022bd8ed6644542 100644 --- a/.github/workflows/release.yml +++ b/.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/* diff --git a/.github/workflows/release_nightly.yml b/.github/workflows/release_nightly.yml index 6c2fa572eccf93b665dc73bc4781c810f3bf25f1..b8c3191384259e129052232786348023b9aa07d6 100644 --- a/.github/workflows/release_nightly.yml +++ b/.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 diff --git a/.github/workflows/run_bundling.yml b/.github/workflows/run_bundling.yml index f56e56ac7f139926085f33d6e97d3dea6e03a4bb..c2ae85e336521f85d45e75c3cab92ae26709bc05 100644 --- a/.github/workflows/run_bundling.yml +++ b/.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 }} diff --git a/Cargo.lock b/Cargo.lock index 26797dab4a91856a3b03c95610ffc967c9b6b356..930289eb1e00dff1cef520f816407a7714191022 100644 --- a/Cargo.lock +++ b/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", diff --git a/crates/gpui/src/platform.rs b/crates/gpui/src/platform.rs index b9822ef7042ef31624dfe54b4eca144e23109547..c5db7dbf191d657f96a98ff64ca99da65c8cf700 100644 --- a/crates/gpui/src/platform.rs +++ b/crates/gpui/src/platform.rs @@ -132,9 +132,9 @@ pub(crate) fn current_platform(headless: bool) -> Rc { } #[cfg(target_os = "windows")] -pub(crate) fn current_platform(_headless: bool) -> Rc { +pub(crate) fn current_platform(headless: bool) -> Rc { Rc::new( - WindowsPlatform::new() + WindowsPlatform::new(headless) .inspect_err(|err| show_error("Failed to launch", err.to_string())) .unwrap(), ) diff --git a/crates/gpui/src/platform/windows/platform.rs b/crates/gpui/src/platform/windows/platform.rs index 6816a340a280bd794e0885868928b82fa6022bcd..cf7bc9dae456624baa584d4d93c5c73c6cac3646 100644 --- a/crates/gpui/src/platform/windows/platform.rs +++ b/crates/gpui/src/platform/windows/platform.rs @@ -33,12 +33,14 @@ pub(crate) struct WindowsPlatform { inner: Rc, raw_window_handles: Arc>>, // 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, + text_system: Arc, + direct_write_text_system: Option>, windows_version: WindowsVersion, - drop_target_helper: IDropTargetHelper, + drop_target_helper: Option, /// 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, @@ -76,11 +78,10 @@ struct PlatformCallbacks { } impl WindowsPlatformState { - fn new(directx_devices: DirectXDevices) -> Self { + fn new(directx_devices: Option) -> 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 { + pub(crate) fn new(headless: bool) -> Result { 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, + Some(dw_text_system), + ) + } else { + ( + None, + Arc::new(crate::NoopTextSystem::new()) as Arc, + None, + ) + }; + let (main_sender, main_receiver) = PriorityQueueReceiver::new(); let validation_number = if usize::BITS == 64 { rand::random::() as usize @@ -105,10 +124,7 @@ impl WindowsPlatform { rand::random::() 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 = 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) { 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> { - 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(), diff --git a/crates/remote/src/remote_client.rs b/crates/remote/src/remote_client.rs index 4f22ef6bd015483621d6ccc12b4849677f7fa6fd..5f67ae75df287b67709d3e6e68a8acecc41aada7 100644 --- a/crates/remote/src/remote_client.rs +++ b/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"); diff --git a/crates/remote/src/transport/ssh.rs b/crates/remote/src/transport/ssh.rs index 4332945b11d5036817845623eeeabcd3e714485d..ee68435eb6e90497b4982dbf47aef07335a8d1bd 100644 --- a/crates/remote/src/transport/ssh.rs +++ b/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, 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, 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() + } + } } } diff --git a/crates/remote_server/Cargo.toml b/crates/remote_server/Cargo.toml index ddb756618ba8b0671a76dc9113dbc0bf07c7cb6d..dcefdf80afb649264d23dc1db96c93d392d6489d 100644 --- a/crates/remote_server/Cargo.toml +++ b/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"] } diff --git a/crates/remote_server/src/main.rs b/crates/remote_server/src/main.rs index 114787f5950a1f8bf5b1ca0290ae82aa156bf466..5ca01c872e23b09e4dbbac2cb39645e9f5cb638f 100644 --- a/crates/remote_server/src/main.rs +++ b/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 "); 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 "); - std::process::exit(1); - } } diff --git a/crates/remote_server/src/remote_server.rs b/crates/remote_server/src/remote_server.rs deleted file mode 100644 index b0d6f74ace0da284f019996d476c489a15c944b0..0000000000000000000000000000000000000000 --- a/crates/remote_server/src/remote_server.rs +++ /dev/null @@ -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(()) - } - } -} diff --git a/crates/remote_server/src/unix.rs b/crates/remote_server/src/server.rs similarity index 83% rename from crates/remote_server/src/unix.rs rename to crates/remote_server/src/server.rs index 5965ad5adbeb2fa8f1108b2dd86ea930b7e3aed1..189ee7b3bd99dc95c61e472fbb3b873eeaff5778 100644 --- a/crates/remote_server/src/unix.rs +++ b/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 = 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> = 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::().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> = 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> = 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, ¶meters, &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 { .map(|output| output.status.success()) } -async fn check_pid_file(path: &Path) -> Result, CheckPidError> { - let Some(pid) = std::fs::read_to_string(&path) +fn check_pid_file(path: &Path) -> Result, CheckPidError> { + let Some(pid) = std::fs::read_to_string(path) .ok() .and_then(|contents| contents.parse::().ok()) else { @@ -890,21 +991,21 @@ async fn check_pid_file(path: &Path) -> Result, 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) -> Option { .or_else(read_proxy_from_env) } -fn daemonize() -> Result> { - 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(); diff --git a/crates/remote_server/src/windows.rs b/crates/remote_server/src/windows.rs new file mode 100644 index 0000000000000000000000000000000000000000..c1ef6c0c63ce6bde0c990cfdd279f3e6b0df109f --- /dev/null +++ b/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::()? + .QueryService::(&SID_STopLevelBrowser)? + .QueryActiveShellView()? + .GetItemObject::(SVGIO_BACKGROUND)? + .cast::()? + .Application()? + .cast()?; + + shell_dispatch.ShellExecute( + &BSTR::from(file), + &VARIANT::from(parameters), + &VARIANT::from(directory), + &VARIANT::from(""), + &VARIANT::from(0i32), + )?; + + Ok(()) + } +} diff --git a/script/bundle-windows.ps1 b/script/bundle-windows.ps1 index 49024be70d7f1b8cdc3a430ef991718f60da276f..f4b83785f99773458a8a5ea970a57cf694d3e7d9 100644 --- a/script/bundle-windows.ps1 +++ b/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 diff --git a/script/upload-nightly.ps1 b/script/upload-nightly.ps1 index 4400c4291b6001c7458e934cc497c2bc1fccff82..dbf86b2b82217fa7330d6a7f2b4bc1f07d2487f1 100644 --- a/script/upload-nightly.ps1 +++ b/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" diff --git a/tooling/xtask/src/tasks/workflows/run_bundling.rs b/tooling/xtask/src/tasks/workflows/run_bundling.rs index a0793ffb68913ae381f3d70de22f8e6c302bb92a..bdeec783476795370c78948ef678fda3a41cab3f 100644 --- a/tooling/xtask/src/tasks/workflows/run_bundling.rs +++ b/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}" + ))), } } diff --git a/tooling/xtask/src/tasks/workflows/vars.rs b/tooling/xtask/src/tasks/workflows/vars.rs index df6b91a6a2c8f6315a1a318c80926331014acc14..87e62c2061406017ee7b72fe98323692e964e019 100644 --- a/tooling/xtask/src/tasks/workflows/vars.rs +++ b/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, ] } }