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 <me@lukaswirth.dev>
This commit is contained in:
John Tur 2026-01-24 13:15:01 -05:00 committed by GitHub
parent e9d94748e8
commit 9931c6f944
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 511 additions and 311 deletions

View file

@ -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/*

View file

@ -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

View file

@ -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 }}

2
Cargo.lock generated
View file

@ -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",

View file

@ -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(),
)

View file

@ -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(),

View file

@ -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");

View file

@ -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()
}
}
}
}

View file

@ -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"] }

View file

@ -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);
}
}

View file

@ -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(())
}
}
}

View file

@ -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();

View file

@ -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(())
}
}

View file

@ -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

View file

@ -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"

View file

@ -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}"
))),
}
}

View file

@ -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,
]
}
}