windows: Add support for SSH (#29145)
Closes #19892 This PR builds on top of #20587 and improves upon it. Release Notes: - N/A --------- Co-authored-by: Kirill Bulatov <kirill@zed.dev>
This commit is contained in:
parent
8bd739d869
commit
0ca0914cca
26 changed files with 1435 additions and 354 deletions
21
Cargo.lock
generated
21
Cargo.lock
generated
|
@ -538,6 +538,8 @@ dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"futures 0.3.31",
|
"futures 0.3.31",
|
||||||
"gpui",
|
"gpui",
|
||||||
|
"net",
|
||||||
|
"parking_lot",
|
||||||
"smol",
|
"smol",
|
||||||
"tempfile",
|
"tempfile",
|
||||||
"util",
|
"util",
|
||||||
|
@ -10231,6 +10233,18 @@ dependencies = [
|
||||||
"jni-sys",
|
"jni-sys",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "net"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
"async-io",
|
||||||
|
"smol",
|
||||||
|
"tempfile",
|
||||||
|
"windows 0.61.1",
|
||||||
|
"workspace-hack",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "new_debug_unreachable"
|
name = "new_debug_unreachable"
|
||||||
version = "1.0.6"
|
version = "1.0.6"
|
||||||
|
@ -12535,6 +12549,7 @@ dependencies = [
|
||||||
"prost 0.9.0",
|
"prost 0.9.0",
|
||||||
"prost-build 0.9.0",
|
"prost-build 0.9.0",
|
||||||
"serde",
|
"serde",
|
||||||
|
"typed-path",
|
||||||
"workspace-hack",
|
"workspace-hack",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -17036,6 +17051,12 @@ dependencies = [
|
||||||
"utf-8",
|
"utf-8",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "typed-path"
|
||||||
|
version = "0.11.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c462d18470a2857aa657d338af5fa67170bb48bcc80a296710ce3b0802a32566"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "typeid"
|
name = "typeid"
|
||||||
version = "1.0.3"
|
version = "1.0.3"
|
||||||
|
|
|
@ -99,6 +99,7 @@ members = [
|
||||||
"crates/migrator",
|
"crates/migrator",
|
||||||
"crates/mistral",
|
"crates/mistral",
|
||||||
"crates/multi_buffer",
|
"crates/multi_buffer",
|
||||||
|
"crates/net",
|
||||||
"crates/node_runtime",
|
"crates/node_runtime",
|
||||||
"crates/notifications",
|
"crates/notifications",
|
||||||
"crates/ollama",
|
"crates/ollama",
|
||||||
|
@ -311,6 +312,7 @@ menu = { path = "crates/menu" }
|
||||||
migrator = { path = "crates/migrator" }
|
migrator = { path = "crates/migrator" }
|
||||||
mistral = { path = "crates/mistral" }
|
mistral = { path = "crates/mistral" }
|
||||||
multi_buffer = { path = "crates/multi_buffer" }
|
multi_buffer = { path = "crates/multi_buffer" }
|
||||||
|
net = { path = "crates/net" }
|
||||||
node_runtime = { path = "crates/node_runtime" }
|
node_runtime = { path = "crates/node_runtime" }
|
||||||
notifications = { path = "crates/notifications" }
|
notifications = { path = "crates/notifications" }
|
||||||
ollama = { path = "crates/ollama" }
|
ollama = { path = "crates/ollama" }
|
||||||
|
@ -660,6 +662,7 @@ features = [
|
||||||
"Win32_Graphics_Gdi",
|
"Win32_Graphics_Gdi",
|
||||||
"Win32_Graphics_Imaging",
|
"Win32_Graphics_Imaging",
|
||||||
"Win32_Graphics_Imaging_D2D",
|
"Win32_Graphics_Imaging_D2D",
|
||||||
|
"Win32_Networking_WinSock",
|
||||||
"Win32_Security",
|
"Win32_Security",
|
||||||
"Win32_Security_Credentials",
|
"Win32_Security_Credentials",
|
||||||
"Win32_Storage_FileSystem",
|
"Win32_Storage_FileSystem",
|
||||||
|
|
|
@ -15,6 +15,8 @@ path = "src/askpass.rs"
|
||||||
anyhow.workspace = true
|
anyhow.workspace = true
|
||||||
futures.workspace = true
|
futures.workspace = true
|
||||||
gpui.workspace = true
|
gpui.workspace = true
|
||||||
|
net.workspace = true
|
||||||
|
parking_lot.workspace = true
|
||||||
smol.workspace = true
|
smol.workspace = true
|
||||||
tempfile.workspace = true
|
tempfile.workspace = true
|
||||||
util.workspace = true
|
util.workspace = true
|
||||||
|
|
|
@ -1,21 +1,14 @@
|
||||||
use std::path::{Path, PathBuf};
|
use std::{ffi::OsStr, time::Duration};
|
||||||
use std::time::Duration;
|
|
||||||
|
|
||||||
#[cfg(unix)]
|
use anyhow::{Context as _, Result};
|
||||||
use anyhow::Context as _;
|
|
||||||
use futures::channel::{mpsc, oneshot};
|
use futures::channel::{mpsc, oneshot};
|
||||||
#[cfg(unix)]
|
use futures::{
|
||||||
use futures::{AsyncBufReadExt as _, io::BufReader};
|
AsyncBufReadExt as _, AsyncWriteExt as _, FutureExt as _, SinkExt, StreamExt, io::BufReader,
|
||||||
#[cfg(unix)]
|
select_biased,
|
||||||
use futures::{AsyncWriteExt as _, FutureExt as _, select_biased};
|
};
|
||||||
use futures::{SinkExt, StreamExt};
|
|
||||||
use gpui::{AsyncApp, BackgroundExecutor, Task};
|
use gpui::{AsyncApp, BackgroundExecutor, Task};
|
||||||
#[cfg(unix)]
|
|
||||||
use smol::fs;
|
use smol::fs;
|
||||||
#[cfg(unix)]
|
use util::ResultExt as _;
|
||||||
use smol::net::unix::UnixListener;
|
|
||||||
#[cfg(unix)]
|
|
||||||
use util::{ResultExt as _, fs::make_file_executable, get_shell_safe_zed_path};
|
|
||||||
|
|
||||||
#[derive(PartialEq, Eq)]
|
#[derive(PartialEq, Eq)]
|
||||||
pub enum AskPassResult {
|
pub enum AskPassResult {
|
||||||
|
@ -42,41 +35,56 @@ impl AskPassDelegate {
|
||||||
Self { tx, _task: task }
|
Self { tx, _task: task }
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn ask_password(&mut self, prompt: String) -> anyhow::Result<String> {
|
pub async fn ask_password(&mut self, prompt: String) -> Result<String> {
|
||||||
let (tx, rx) = oneshot::channel();
|
let (tx, rx) = oneshot::channel();
|
||||||
self.tx.send((prompt, tx)).await?;
|
self.tx.send((prompt, tx)).await?;
|
||||||
Ok(rx.await?)
|
Ok(rx.await?)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(unix)]
|
|
||||||
pub struct AskPassSession {
|
pub struct AskPassSession {
|
||||||
script_path: PathBuf,
|
#[cfg(not(target_os = "windows"))]
|
||||||
|
script_path: std::path::PathBuf,
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
askpass_helper: String,
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
secret: std::sync::Arc<parking_lot::Mutex<String>>,
|
||||||
_askpass_task: Task<()>,
|
_askpass_task: Task<()>,
|
||||||
askpass_opened_rx: Option<oneshot::Receiver<()>>,
|
askpass_opened_rx: Option<oneshot::Receiver<()>>,
|
||||||
askpass_kill_master_rx: Option<oneshot::Receiver<()>>,
|
askpass_kill_master_rx: Option<oneshot::Receiver<()>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(unix)]
|
#[cfg(not(target_os = "windows"))]
|
||||||
|
const ASKPASS_SCRIPT_NAME: &str = "askpass.sh";
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
const ASKPASS_SCRIPT_NAME: &str = "askpass.ps1";
|
||||||
|
|
||||||
impl AskPassSession {
|
impl AskPassSession {
|
||||||
/// This will create a new AskPassSession.
|
/// This will create a new AskPassSession.
|
||||||
/// You must retain this session until the master process exits.
|
/// You must retain this session until the master process exits.
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub async fn new(
|
pub async fn new(executor: &BackgroundExecutor, mut delegate: AskPassDelegate) -> Result<Self> {
|
||||||
executor: &BackgroundExecutor,
|
use net::async_net::UnixListener;
|
||||||
mut delegate: AskPassDelegate,
|
use util::fs::make_file_executable;
|
||||||
) -> anyhow::Result<Self> {
|
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
let secret = std::sync::Arc::new(parking_lot::Mutex::new(String::new()));
|
||||||
let temp_dir = tempfile::Builder::new().prefix("zed-askpass").tempdir()?;
|
let temp_dir = tempfile::Builder::new().prefix("zed-askpass").tempdir()?;
|
||||||
let askpass_socket = temp_dir.path().join("askpass.sock");
|
let askpass_socket = temp_dir.path().join("askpass.sock");
|
||||||
let askpass_script_path = temp_dir.path().join("askpass.sh");
|
let askpass_script_path = temp_dir.path().join(ASKPASS_SCRIPT_NAME);
|
||||||
let (askpass_opened_tx, askpass_opened_rx) = oneshot::channel::<()>();
|
let (askpass_opened_tx, askpass_opened_rx) = oneshot::channel::<()>();
|
||||||
let listener =
|
let listener = UnixListener::bind(&askpass_socket).context("creating askpass socket")?;
|
||||||
UnixListener::bind(&askpass_socket).context("failed to create askpass socket")?;
|
#[cfg(not(target_os = "windows"))]
|
||||||
let zed_path = get_shell_safe_zed_path()?;
|
let zed_path = util::get_shell_safe_zed_path()?;
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
let zed_path = std::env::current_exe()
|
||||||
|
.context("finding current executable path for use in askpass")?;
|
||||||
|
|
||||||
let (askpass_kill_master_tx, askpass_kill_master_rx) = oneshot::channel::<()>();
|
let (askpass_kill_master_tx, askpass_kill_master_rx) = oneshot::channel::<()>();
|
||||||
let mut kill_tx = Some(askpass_kill_master_tx);
|
let mut kill_tx = Some(askpass_kill_master_tx);
|
||||||
|
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
let askpass_secret = secret.clone();
|
||||||
let askpass_task = executor.spawn(async move {
|
let askpass_task = executor.spawn(async move {
|
||||||
let mut askpass_opened_tx = Some(askpass_opened_tx);
|
let mut askpass_opened_tx = Some(askpass_opened_tx);
|
||||||
|
|
||||||
|
@ -93,10 +101,14 @@ impl AskPassSession {
|
||||||
if let Some(password) = delegate
|
if let Some(password) = delegate
|
||||||
.ask_password(prompt.to_string())
|
.ask_password(prompt.to_string())
|
||||||
.await
|
.await
|
||||||
.context("failed to get askpass password")
|
.context("getting askpass password")
|
||||||
.log_err()
|
.log_err()
|
||||||
{
|
{
|
||||||
stream.write_all(password.as_bytes()).await.log_err();
|
stream.write_all(password.as_bytes()).await.log_err();
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
{
|
||||||
|
*askpass_secret.lock() = password;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
if let Some(kill_tx) = kill_tx.take() {
|
if let Some(kill_tx) = kill_tx.take() {
|
||||||
kill_tx.send(()).log_err();
|
kill_tx.send(()).log_err();
|
||||||
|
@ -112,34 +124,49 @@ impl AskPassSession {
|
||||||
});
|
});
|
||||||
|
|
||||||
// Create an askpass script that communicates back to this process.
|
// Create an askpass script that communicates back to this process.
|
||||||
let askpass_script = format!(
|
let askpass_script = generate_askpass_script(&zed_path, &askpass_socket);
|
||||||
"{shebang}\n{print_args} | {zed_exe} --askpass={askpass_socket} 2> /dev/null \n",
|
fs::write(&askpass_script_path, askpass_script)
|
||||||
zed_exe = zed_path,
|
.await
|
||||||
askpass_socket = askpass_socket.display(),
|
.with_context(|| format!("creating askpass script at {askpass_script_path:?}"))?;
|
||||||
print_args = "printf '%s\\0' \"$@\"",
|
|
||||||
shebang = "#!/bin/sh",
|
|
||||||
);
|
|
||||||
fs::write(&askpass_script_path, askpass_script).await?;
|
|
||||||
make_file_executable(&askpass_script_path).await?;
|
make_file_executable(&askpass_script_path).await?;
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
let askpass_helper = format!(
|
||||||
|
"powershell.exe -ExecutionPolicy Bypass -File {}",
|
||||||
|
askpass_script_path.display()
|
||||||
|
);
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
|
#[cfg(not(target_os = "windows"))]
|
||||||
script_path: askpass_script_path,
|
script_path: askpass_script_path,
|
||||||
|
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
secret,
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
askpass_helper,
|
||||||
|
|
||||||
_askpass_task: askpass_task,
|
_askpass_task: askpass_task,
|
||||||
askpass_kill_master_rx: Some(askpass_kill_master_rx),
|
askpass_kill_master_rx: Some(askpass_kill_master_rx),
|
||||||
askpass_opened_rx: Some(askpass_opened_rx),
|
askpass_opened_rx: Some(askpass_opened_rx),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn script_path(&self) -> &Path {
|
#[cfg(not(target_os = "windows"))]
|
||||||
|
pub fn script_path(&self) -> impl AsRef<OsStr> {
|
||||||
&self.script_path
|
&self.script_path
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
pub fn script_path(&self) -> impl AsRef<OsStr> {
|
||||||
|
&self.askpass_helper
|
||||||
|
}
|
||||||
|
|
||||||
// This will run the askpass task forever, resolving as many authentication requests as needed.
|
// This will run the askpass task forever, resolving as many authentication requests as needed.
|
||||||
// The caller is responsible for examining the result of their own commands and cancelling this
|
// The caller is responsible for examining the result of their own commands and cancelling this
|
||||||
// future when this is no longer needed. Note that this can only be called once, but due to the
|
// future when this is no longer needed. Note that this can only be called once, but due to the
|
||||||
// drop order this takes an &mut, so you can `drop()` it after you're done with the master process.
|
// drop order this takes an &mut, so you can `drop()` it after you're done with the master process.
|
||||||
pub async fn run(&mut self) -> AskPassResult {
|
pub async fn run(&mut self) -> AskPassResult {
|
||||||
let connection_timeout = Duration::from_secs(10);
|
// This is the default timeout setting used by VSCode.
|
||||||
|
let connection_timeout = Duration::from_secs(17);
|
||||||
let askpass_opened_rx = self.askpass_opened_rx.take().expect("Only call run once");
|
let askpass_opened_rx = self.askpass_opened_rx.take().expect("Only call run once");
|
||||||
let askpass_kill_master_rx = self
|
let askpass_kill_master_rx = self
|
||||||
.askpass_kill_master_rx
|
.askpass_kill_master_rx
|
||||||
|
@ -158,14 +185,19 @@ impl AskPassSession {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// This will return the password that was last set by the askpass script.
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
pub fn get_password(&self) -> String {
|
||||||
|
self.secret.lock().clone()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The main function for when Zed is running in netcat mode for use in askpass.
|
/// The main function for when Zed is running in netcat mode for use in askpass.
|
||||||
/// Called from both the remote server binary and the zed binary in their respective main functions.
|
/// Called from both the remote server binary and the zed binary in their respective main functions.
|
||||||
#[cfg(unix)]
|
|
||||||
pub fn main(socket: &str) {
|
pub fn main(socket: &str) {
|
||||||
|
use net::UnixStream;
|
||||||
use std::io::{self, Read, Write};
|
use std::io::{self, Read, Write};
|
||||||
use std::os::unix::net::UnixStream;
|
|
||||||
use std::process::exit;
|
use std::process::exit;
|
||||||
|
|
||||||
let mut stream = match UnixStream::connect(socket) {
|
let mut stream = match UnixStream::connect(socket) {
|
||||||
|
@ -182,6 +214,10 @@ pub fn main(socket: &str) {
|
||||||
exit(1);
|
exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
while buffer.last().map_or(false, |&b| b == b'\n' || b == b'\r') {
|
||||||
|
buffer.pop();
|
||||||
|
}
|
||||||
if buffer.last() != Some(&b'\0') {
|
if buffer.last() != Some(&b'\0') {
|
||||||
buffer.push(b'\0');
|
buffer.push(b'\0');
|
||||||
}
|
}
|
||||||
|
@ -202,28 +238,28 @@ pub fn main(socket: &str) {
|
||||||
exit(1);
|
exit(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#[cfg(not(unix))]
|
|
||||||
pub fn main(_socket: &str) {}
|
|
||||||
|
|
||||||
#[cfg(not(unix))]
|
#[inline]
|
||||||
pub struct AskPassSession {
|
#[cfg(not(target_os = "windows"))]
|
||||||
path: PathBuf,
|
fn generate_askpass_script(zed_path: &str, askpass_socket: &std::path::Path) -> String {
|
||||||
|
format!(
|
||||||
|
"{shebang}\n{print_args} | {zed_exe} --askpass={askpass_socket} 2> /dev/null \n",
|
||||||
|
zed_exe = zed_path,
|
||||||
|
askpass_socket = askpass_socket.display(),
|
||||||
|
print_args = "printf '%s\\0' \"$@\"",
|
||||||
|
shebang = "#!/bin/sh",
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(not(unix))]
|
#[inline]
|
||||||
impl AskPassSession {
|
#[cfg(target_os = "windows")]
|
||||||
pub async fn new(_: &BackgroundExecutor, _: AskPassDelegate) -> anyhow::Result<Self> {
|
fn generate_askpass_script(zed_path: &std::path::Path, askpass_socket: &std::path::Path) -> String {
|
||||||
Ok(Self {
|
format!(
|
||||||
path: PathBuf::new(),
|
r#"
|
||||||
})
|
$ErrorActionPreference = 'Stop';
|
||||||
}
|
($args -join [char]0) | & "{zed_exe}" --askpass={askpass_socket} 2> $null
|
||||||
|
"#,
|
||||||
pub fn script_path(&self) -> &Path {
|
zed_exe = zed_path.display(),
|
||||||
&self.path
|
askpass_socket = askpass_socket.display(),
|
||||||
}
|
)
|
||||||
|
|
||||||
pub async fn run(&mut self) -> AskPassResult {
|
|
||||||
futures::FutureExt::fuse(smol::Timer::after(Duration::from_secs(20))).await;
|
|
||||||
AskPassResult::Timedout
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -54,7 +54,7 @@ use std::{
|
||||||
time::{Duration, Instant},
|
time::{Duration, Instant},
|
||||||
};
|
};
|
||||||
use url::Url;
|
use url::Url;
|
||||||
use util::ResultExt;
|
use util::{ResultExt, paths::RemotePathBuf};
|
||||||
use wasm_host::{
|
use wasm_host::{
|
||||||
WasmExtension, WasmHost,
|
WasmExtension, WasmHost,
|
||||||
wit::{is_supported_wasm_api_version, wasm_api_version_range},
|
wit::{is_supported_wasm_api_version, wasm_api_version_range},
|
||||||
|
@ -1689,6 +1689,7 @@ impl ExtensionStore {
|
||||||
.request(proto::SyncExtensions { extensions })
|
.request(proto::SyncExtensions { extensions })
|
||||||
})?
|
})?
|
||||||
.await?;
|
.await?;
|
||||||
|
let path_style = client.read_with(cx, |client, _| client.path_style())?;
|
||||||
|
|
||||||
for missing_extension in response.missing_extensions.into_iter() {
|
for missing_extension in response.missing_extensions.into_iter() {
|
||||||
let tmp_dir = tempfile::tempdir()?;
|
let tmp_dir = tempfile::tempdir()?;
|
||||||
|
@ -1701,7 +1702,10 @@ impl ExtensionStore {
|
||||||
)
|
)
|
||||||
})?
|
})?
|
||||||
.await?;
|
.await?;
|
||||||
let dest_dir = PathBuf::from(&response.tmp_dir).join(missing_extension.clone().id);
|
let dest_dir = RemotePathBuf::new(
|
||||||
|
PathBuf::from(&response.tmp_dir).join(missing_extension.clone().id),
|
||||||
|
path_style,
|
||||||
|
);
|
||||||
log::info!("Uploading extension {}", missing_extension.clone().id);
|
log::info!("Uploading extension {}", missing_extension.clone().id);
|
||||||
|
|
||||||
client
|
client
|
||||||
|
@ -1718,7 +1722,7 @@ impl ExtensionStore {
|
||||||
client
|
client
|
||||||
.update(cx, |client, _cx| {
|
.update(cx, |client, _cx| {
|
||||||
client.proto_client().request(proto::InstallExtension {
|
client.proto_client().request(proto::InstallExtension {
|
||||||
tmp_dir: dest_dir.to_string_lossy().to_string(),
|
tmp_dir: dest_dir.to_proto(),
|
||||||
extension: Some(missing_extension),
|
extension: Some(missing_extension),
|
||||||
})
|
})
|
||||||
})?
|
})?
|
||||||
|
|
|
@ -1,7 +1,10 @@
|
||||||
use std::{path::PathBuf, sync::Arc};
|
use std::{path::PathBuf, sync::Arc};
|
||||||
|
|
||||||
use anyhow::{Context as _, Result};
|
use anyhow::{Context as _, Result};
|
||||||
use client::{TypedEnvelope, proto};
|
use client::{
|
||||||
|
TypedEnvelope,
|
||||||
|
proto::{self, FromProto},
|
||||||
|
};
|
||||||
use collections::{HashMap, HashSet};
|
use collections::{HashMap, HashSet};
|
||||||
use extension::{
|
use extension::{
|
||||||
Extension, ExtensionDebugAdapterProviderProxy, ExtensionHostProxy, ExtensionLanguageProxy,
|
Extension, ExtensionDebugAdapterProviderProxy, ExtensionHostProxy, ExtensionLanguageProxy,
|
||||||
|
@ -328,7 +331,7 @@ impl HeadlessExtensionStore {
|
||||||
version: extension.version,
|
version: extension.version,
|
||||||
dev: extension.dev,
|
dev: extension.dev,
|
||||||
},
|
},
|
||||||
PathBuf::from(envelope.payload.tmp_dir),
|
PathBuf::from_proto(envelope.payload.tmp_dir),
|
||||||
cx,
|
cx,
|
||||||
)
|
)
|
||||||
})?
|
})?
|
||||||
|
|
|
@ -15,16 +15,14 @@ use std::{
|
||||||
};
|
};
|
||||||
use ui::{Context, LabelLike, ListItem, Window};
|
use ui::{Context, LabelLike, ListItem, Window};
|
||||||
use ui::{HighlightedLabel, ListItemSpacing, prelude::*};
|
use ui::{HighlightedLabel, ListItemSpacing, prelude::*};
|
||||||
use util::{maybe, paths::compare_paths};
|
use util::{
|
||||||
|
maybe,
|
||||||
|
paths::{PathStyle, compare_paths},
|
||||||
|
};
|
||||||
use workspace::Workspace;
|
use workspace::Workspace;
|
||||||
|
|
||||||
pub(crate) struct OpenPathPrompt;
|
pub(crate) struct OpenPathPrompt;
|
||||||
|
|
||||||
#[cfg(target_os = "windows")]
|
|
||||||
const PROMPT_ROOT: &str = "C:\\";
|
|
||||||
#[cfg(not(target_os = "windows"))]
|
|
||||||
const PROMPT_ROOT: &str = "/";
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct OpenPathDelegate {
|
pub struct OpenPathDelegate {
|
||||||
tx: Option<oneshot::Sender<Option<Vec<PathBuf>>>>,
|
tx: Option<oneshot::Sender<Option<Vec<PathBuf>>>>,
|
||||||
|
@ -34,6 +32,8 @@ pub struct OpenPathDelegate {
|
||||||
string_matches: Vec<StringMatch>,
|
string_matches: Vec<StringMatch>,
|
||||||
cancel_flag: Arc<AtomicBool>,
|
cancel_flag: Arc<AtomicBool>,
|
||||||
should_dismiss: bool,
|
should_dismiss: bool,
|
||||||
|
prompt_root: String,
|
||||||
|
path_style: PathStyle,
|
||||||
replace_prompt: Task<()>,
|
replace_prompt: Task<()>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -42,6 +42,7 @@ impl OpenPathDelegate {
|
||||||
tx: oneshot::Sender<Option<Vec<PathBuf>>>,
|
tx: oneshot::Sender<Option<Vec<PathBuf>>>,
|
||||||
lister: DirectoryLister,
|
lister: DirectoryLister,
|
||||||
creating_path: bool,
|
creating_path: bool,
|
||||||
|
path_style: PathStyle,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
tx: Some(tx),
|
tx: Some(tx),
|
||||||
|
@ -53,6 +54,11 @@ impl OpenPathDelegate {
|
||||||
string_matches: Vec::new(),
|
string_matches: Vec::new(),
|
||||||
cancel_flag: Arc::new(AtomicBool::new(false)),
|
cancel_flag: Arc::new(AtomicBool::new(false)),
|
||||||
should_dismiss: true,
|
should_dismiss: true,
|
||||||
|
prompt_root: match path_style {
|
||||||
|
PathStyle::Posix => "/".to_string(),
|
||||||
|
PathStyle::Windows => "C:\\".to_string(),
|
||||||
|
},
|
||||||
|
path_style,
|
||||||
replace_prompt: Task::ready(()),
|
replace_prompt: Task::ready(()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -185,7 +191,8 @@ impl OpenPathPrompt {
|
||||||
cx: &mut Context<Workspace>,
|
cx: &mut Context<Workspace>,
|
||||||
) {
|
) {
|
||||||
workspace.toggle_modal(window, cx, |window, cx| {
|
workspace.toggle_modal(window, cx, |window, cx| {
|
||||||
let delegate = OpenPathDelegate::new(tx, lister.clone(), creating_path);
|
let delegate =
|
||||||
|
OpenPathDelegate::new(tx, lister.clone(), creating_path, PathStyle::current());
|
||||||
let picker = Picker::uniform_list(delegate, window, cx).width(rems(34.));
|
let picker = Picker::uniform_list(delegate, window, cx).width(rems(34.));
|
||||||
let query = lister.default_query(cx);
|
let query = lister.default_query(cx);
|
||||||
picker.set_query(query, window, cx);
|
picker.set_query(query, window, cx);
|
||||||
|
@ -226,18 +233,7 @@ impl PickerDelegate for OpenPathDelegate {
|
||||||
cx: &mut Context<Picker<Self>>,
|
cx: &mut Context<Picker<Self>>,
|
||||||
) -> Task<()> {
|
) -> Task<()> {
|
||||||
let lister = &self.lister;
|
let lister = &self.lister;
|
||||||
let last_item = Path::new(&query)
|
let (dir, suffix) = get_dir_and_suffix(query, self.path_style);
|
||||||
.file_name()
|
|
||||||
.unwrap_or_default()
|
|
||||||
.to_string_lossy();
|
|
||||||
let (mut dir, suffix) = if let Some(dir) = query.strip_suffix(last_item.as_ref()) {
|
|
||||||
(dir.to_string(), last_item.into_owned())
|
|
||||||
} else {
|
|
||||||
(query, String::new())
|
|
||||||
};
|
|
||||||
if dir == "" {
|
|
||||||
dir = PROMPT_ROOT.to_string();
|
|
||||||
}
|
|
||||||
|
|
||||||
let query = match &self.directory_state {
|
let query = match &self.directory_state {
|
||||||
DirectoryState::List { parent_path, .. } => {
|
DirectoryState::List { parent_path, .. } => {
|
||||||
|
@ -266,6 +262,7 @@ impl PickerDelegate for OpenPathDelegate {
|
||||||
self.cancel_flag = Arc::new(AtomicBool::new(false));
|
self.cancel_flag = Arc::new(AtomicBool::new(false));
|
||||||
let cancel_flag = self.cancel_flag.clone();
|
let cancel_flag = self.cancel_flag.clone();
|
||||||
|
|
||||||
|
let parent_path_is_root = self.prompt_root == dir;
|
||||||
cx.spawn_in(window, async move |this, cx| {
|
cx.spawn_in(window, async move |this, cx| {
|
||||||
if let Some(query) = query {
|
if let Some(query) = query {
|
||||||
let paths = query.await;
|
let paths = query.await;
|
||||||
|
@ -279,7 +276,7 @@ impl PickerDelegate for OpenPathDelegate {
|
||||||
DirectoryState::None { create: false }
|
DirectoryState::None { create: false }
|
||||||
| DirectoryState::List { .. } => match paths {
|
| DirectoryState::List { .. } => match paths {
|
||||||
Ok(paths) => DirectoryState::List {
|
Ok(paths) => DirectoryState::List {
|
||||||
entries: path_candidates(&dir, paths),
|
entries: path_candidates(parent_path_is_root, paths),
|
||||||
parent_path: dir.clone(),
|
parent_path: dir.clone(),
|
||||||
error: None,
|
error: None,
|
||||||
},
|
},
|
||||||
|
@ -292,7 +289,7 @@ impl PickerDelegate for OpenPathDelegate {
|
||||||
DirectoryState::None { create: true }
|
DirectoryState::None { create: true }
|
||||||
| DirectoryState::Create { .. } => match paths {
|
| DirectoryState::Create { .. } => match paths {
|
||||||
Ok(paths) => {
|
Ok(paths) => {
|
||||||
let mut entries = path_candidates(&dir, paths);
|
let mut entries = path_candidates(parent_path_is_root, paths);
|
||||||
let mut exists = false;
|
let mut exists = false;
|
||||||
let mut is_dir = false;
|
let mut is_dir = false;
|
||||||
let mut new_id = None;
|
let mut new_id = None;
|
||||||
|
@ -488,6 +485,7 @@ impl PickerDelegate for OpenPathDelegate {
|
||||||
_: &mut Context<Picker<Self>>,
|
_: &mut Context<Picker<Self>>,
|
||||||
) -> Option<String> {
|
) -> Option<String> {
|
||||||
let candidate = self.get_entry(self.selected_index)?;
|
let candidate = self.get_entry(self.selected_index)?;
|
||||||
|
let path_style = self.path_style;
|
||||||
Some(
|
Some(
|
||||||
maybe!({
|
maybe!({
|
||||||
match &self.directory_state {
|
match &self.directory_state {
|
||||||
|
@ -496,7 +494,7 @@ impl PickerDelegate for OpenPathDelegate {
|
||||||
parent_path,
|
parent_path,
|
||||||
candidate.path.string,
|
candidate.path.string,
|
||||||
if candidate.is_dir {
|
if candidate.is_dir {
|
||||||
MAIN_SEPARATOR_STR
|
path_style.separator()
|
||||||
} else {
|
} else {
|
||||||
""
|
""
|
||||||
}
|
}
|
||||||
|
@ -506,7 +504,7 @@ impl PickerDelegate for OpenPathDelegate {
|
||||||
parent_path,
|
parent_path,
|
||||||
candidate.path.string,
|
candidate.path.string,
|
||||||
if candidate.is_dir {
|
if candidate.is_dir {
|
||||||
MAIN_SEPARATOR_STR
|
path_style.separator()
|
||||||
} else {
|
} else {
|
||||||
""
|
""
|
||||||
}
|
}
|
||||||
|
@ -527,8 +525,8 @@ impl PickerDelegate for OpenPathDelegate {
|
||||||
DirectoryState::None { .. } => return,
|
DirectoryState::None { .. } => return,
|
||||||
DirectoryState::List { parent_path, .. } => {
|
DirectoryState::List { parent_path, .. } => {
|
||||||
let confirmed_path =
|
let confirmed_path =
|
||||||
if parent_path == PROMPT_ROOT && candidate.path.string.is_empty() {
|
if parent_path == &self.prompt_root && candidate.path.string.is_empty() {
|
||||||
PathBuf::from(PROMPT_ROOT)
|
PathBuf::from(&self.prompt_root)
|
||||||
} else {
|
} else {
|
||||||
Path::new(self.lister.resolve_tilde(parent_path, cx).as_ref())
|
Path::new(self.lister.resolve_tilde(parent_path, cx).as_ref())
|
||||||
.join(&candidate.path.string)
|
.join(&candidate.path.string)
|
||||||
|
@ -548,8 +546,8 @@ impl PickerDelegate for OpenPathDelegate {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let prompted_path =
|
let prompted_path =
|
||||||
if parent_path == PROMPT_ROOT && user_input.file.string.is_empty() {
|
if parent_path == &self.prompt_root && user_input.file.string.is_empty() {
|
||||||
PathBuf::from(PROMPT_ROOT)
|
PathBuf::from(&self.prompt_root)
|
||||||
} else {
|
} else {
|
||||||
Path::new(self.lister.resolve_tilde(parent_path, cx).as_ref())
|
Path::new(self.lister.resolve_tilde(parent_path, cx).as_ref())
|
||||||
.join(&user_input.file.string)
|
.join(&user_input.file.string)
|
||||||
|
@ -652,8 +650,8 @@ impl PickerDelegate for OpenPathDelegate {
|
||||||
.inset(true)
|
.inset(true)
|
||||||
.toggle_state(selected)
|
.toggle_state(selected)
|
||||||
.child(HighlightedLabel::new(
|
.child(HighlightedLabel::new(
|
||||||
if parent_path == PROMPT_ROOT {
|
if parent_path == &self.prompt_root {
|
||||||
format!("{}{}", PROMPT_ROOT, candidate.path.string)
|
format!("{}{}", self.prompt_root, candidate.path.string)
|
||||||
} else {
|
} else {
|
||||||
candidate.path.string.clone()
|
candidate.path.string.clone()
|
||||||
},
|
},
|
||||||
|
@ -665,10 +663,10 @@ impl PickerDelegate for OpenPathDelegate {
|
||||||
user_input,
|
user_input,
|
||||||
..
|
..
|
||||||
} => {
|
} => {
|
||||||
let (label, delta) = if parent_path == PROMPT_ROOT {
|
let (label, delta) = if parent_path == &self.prompt_root {
|
||||||
(
|
(
|
||||||
format!("{}{}", PROMPT_ROOT, candidate.path.string),
|
format!("{}{}", self.prompt_root, candidate.path.string),
|
||||||
PROMPT_ROOT.len(),
|
self.prompt_root.len(),
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
(candidate.path.string.clone(), 0)
|
(candidate.path.string.clone(), 0)
|
||||||
|
@ -751,8 +749,11 @@ impl PickerDelegate for OpenPathDelegate {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn path_candidates(parent_path: &String, mut children: Vec<DirectoryItem>) -> Vec<CandidateInfo> {
|
fn path_candidates(
|
||||||
if *parent_path == PROMPT_ROOT {
|
parent_path_is_root: bool,
|
||||||
|
mut children: Vec<DirectoryItem>,
|
||||||
|
) -> Vec<CandidateInfo> {
|
||||||
|
if parent_path_is_root {
|
||||||
children.push(DirectoryItem {
|
children.push(DirectoryItem {
|
||||||
is_dir: true,
|
is_dir: true,
|
||||||
path: PathBuf::default(),
|
path: PathBuf::default(),
|
||||||
|
@ -769,3 +770,128 @@ fn path_candidates(parent_path: &String, mut children: Vec<DirectoryItem>) -> Ve
|
||||||
})
|
})
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
fn get_dir_and_suffix(query: String, path_style: PathStyle) -> (String, String) {
|
||||||
|
let last_item = Path::new(&query)
|
||||||
|
.file_name()
|
||||||
|
.unwrap_or_default()
|
||||||
|
.to_string_lossy();
|
||||||
|
let (mut dir, suffix) = if let Some(dir) = query.strip_suffix(last_item.as_ref()) {
|
||||||
|
(dir.to_string(), last_item.into_owned())
|
||||||
|
} else {
|
||||||
|
(query.to_string(), String::new())
|
||||||
|
};
|
||||||
|
match path_style {
|
||||||
|
PathStyle::Posix => {
|
||||||
|
if dir.is_empty() {
|
||||||
|
dir = "/".to_string();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
PathStyle::Windows => {
|
||||||
|
if dir.len() < 3 {
|
||||||
|
dir = "C:\\".to_string();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
(dir, suffix)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(target_os = "windows"))]
|
||||||
|
fn get_dir_and_suffix(query: String, path_style: PathStyle) -> (String, String) {
|
||||||
|
match path_style {
|
||||||
|
PathStyle::Posix => {
|
||||||
|
let (mut dir, suffix) = if let Some(index) = query.rfind('/') {
|
||||||
|
(query[..index].to_string(), query[index + 1..].to_string())
|
||||||
|
} else {
|
||||||
|
(query, String::new())
|
||||||
|
};
|
||||||
|
if !dir.ends_with('/') {
|
||||||
|
dir.push('/');
|
||||||
|
}
|
||||||
|
(dir, suffix)
|
||||||
|
}
|
||||||
|
PathStyle::Windows => {
|
||||||
|
let (mut dir, suffix) = if let Some(index) = query.rfind('\\') {
|
||||||
|
(query[..index].to_string(), query[index + 1..].to_string())
|
||||||
|
} else {
|
||||||
|
(query, String::new())
|
||||||
|
};
|
||||||
|
if dir.len() < 3 {
|
||||||
|
dir = "C:\\".to_string();
|
||||||
|
}
|
||||||
|
if !dir.ends_with('\\') {
|
||||||
|
dir.push('\\');
|
||||||
|
}
|
||||||
|
(dir, suffix)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use util::paths::PathStyle;
|
||||||
|
|
||||||
|
use crate::open_path_prompt::get_dir_and_suffix;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_get_dir_and_suffix_with_windows_style() {
|
||||||
|
let (dir, suffix) = get_dir_and_suffix("".into(), PathStyle::Windows);
|
||||||
|
assert_eq!(dir, "C:\\");
|
||||||
|
assert_eq!(suffix, "");
|
||||||
|
|
||||||
|
let (dir, suffix) = get_dir_and_suffix("C:".into(), PathStyle::Windows);
|
||||||
|
assert_eq!(dir, "C:\\");
|
||||||
|
assert_eq!(suffix, "");
|
||||||
|
|
||||||
|
let (dir, suffix) = get_dir_and_suffix("C:\\".into(), PathStyle::Windows);
|
||||||
|
assert_eq!(dir, "C:\\");
|
||||||
|
assert_eq!(suffix, "");
|
||||||
|
|
||||||
|
let (dir, suffix) = get_dir_and_suffix("C:\\Use".into(), PathStyle::Windows);
|
||||||
|
assert_eq!(dir, "C:\\");
|
||||||
|
assert_eq!(suffix, "Use");
|
||||||
|
|
||||||
|
let (dir, suffix) =
|
||||||
|
get_dir_and_suffix("C:\\Users\\Junkui\\Docum".into(), PathStyle::Windows);
|
||||||
|
assert_eq!(dir, "C:\\Users\\Junkui\\");
|
||||||
|
assert_eq!(suffix, "Docum");
|
||||||
|
|
||||||
|
let (dir, suffix) =
|
||||||
|
get_dir_and_suffix("C:\\Users\\Junkui\\Documents".into(), PathStyle::Windows);
|
||||||
|
assert_eq!(dir, "C:\\Users\\Junkui\\");
|
||||||
|
assert_eq!(suffix, "Documents");
|
||||||
|
|
||||||
|
let (dir, suffix) =
|
||||||
|
get_dir_and_suffix("C:\\Users\\Junkui\\Documents\\".into(), PathStyle::Windows);
|
||||||
|
assert_eq!(dir, "C:\\Users\\Junkui\\Documents\\");
|
||||||
|
assert_eq!(suffix, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_get_dir_and_suffix_with_posix_style() {
|
||||||
|
let (dir, suffix) = get_dir_and_suffix("".into(), PathStyle::Posix);
|
||||||
|
assert_eq!(dir, "/");
|
||||||
|
assert_eq!(suffix, "");
|
||||||
|
|
||||||
|
let (dir, suffix) = get_dir_and_suffix("/".into(), PathStyle::Posix);
|
||||||
|
assert_eq!(dir, "/");
|
||||||
|
assert_eq!(suffix, "");
|
||||||
|
|
||||||
|
let (dir, suffix) = get_dir_and_suffix("/Use".into(), PathStyle::Posix);
|
||||||
|
assert_eq!(dir, "/");
|
||||||
|
assert_eq!(suffix, "Use");
|
||||||
|
|
||||||
|
let (dir, suffix) = get_dir_and_suffix("/Users/Junkui/Docum".into(), PathStyle::Posix);
|
||||||
|
assert_eq!(dir, "/Users/Junkui/");
|
||||||
|
assert_eq!(suffix, "Docum");
|
||||||
|
|
||||||
|
let (dir, suffix) = get_dir_and_suffix("/Users/Junkui/Documents".into(), PathStyle::Posix);
|
||||||
|
assert_eq!(dir, "/Users/Junkui/");
|
||||||
|
assert_eq!(suffix, "Documents");
|
||||||
|
|
||||||
|
let (dir, suffix) = get_dir_and_suffix("/Users/Junkui/Documents/".into(), PathStyle::Posix);
|
||||||
|
assert_eq!(dir, "/Users/Junkui/Documents/");
|
||||||
|
assert_eq!(suffix, "");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -5,7 +5,7 @@ use picker::{Picker, PickerDelegate};
|
||||||
use project::Project;
|
use project::Project;
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
use ui::rems;
|
use ui::rems;
|
||||||
use util::path;
|
use util::{path, paths::PathStyle};
|
||||||
use workspace::{AppState, Workspace};
|
use workspace::{AppState, Workspace};
|
||||||
|
|
||||||
use crate::OpenPathDelegate;
|
use crate::OpenPathDelegate;
|
||||||
|
@ -37,7 +37,7 @@ async fn test_open_path_prompt(cx: &mut TestAppContext) {
|
||||||
|
|
||||||
let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
|
let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
|
||||||
|
|
||||||
let (picker, cx) = build_open_path_prompt(project, false, cx);
|
let (picker, cx) = build_open_path_prompt(project, false, PathStyle::current(), cx);
|
||||||
|
|
||||||
let query = path!("/root");
|
let query = path!("/root");
|
||||||
insert_query(query, &picker, cx).await;
|
insert_query(query, &picker, cx).await;
|
||||||
|
@ -111,7 +111,7 @@ async fn test_open_path_prompt_completion(cx: &mut TestAppContext) {
|
||||||
|
|
||||||
let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
|
let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
|
||||||
|
|
||||||
let (picker, cx) = build_open_path_prompt(project, false, cx);
|
let (picker, cx) = build_open_path_prompt(project, false, PathStyle::current(), cx);
|
||||||
|
|
||||||
// Confirm completion for the query "/root", since it's a directory, it should add a trailing slash.
|
// Confirm completion for the query "/root", since it's a directory, it should add a trailing slash.
|
||||||
let query = path!("/root");
|
let query = path!("/root");
|
||||||
|
@ -186,7 +186,7 @@ async fn test_open_path_prompt_completion(cx: &mut TestAppContext) {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg_attr(not(target_os = "windows"), ignore)]
|
||||||
async fn test_open_path_prompt_on_windows(cx: &mut TestAppContext) {
|
async fn test_open_path_prompt_on_windows(cx: &mut TestAppContext) {
|
||||||
let app_state = init_test(cx);
|
let app_state = init_test(cx);
|
||||||
app_state
|
app_state
|
||||||
|
@ -204,7 +204,7 @@ async fn test_open_path_prompt_on_windows(cx: &mut TestAppContext) {
|
||||||
|
|
||||||
let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
|
let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
|
||||||
|
|
||||||
let (picker, cx) = build_open_path_prompt(project, false, cx);
|
let (picker, cx) = build_open_path_prompt(project, false, PathStyle::current(), cx);
|
||||||
|
|
||||||
// Support both forward and backward slashes.
|
// Support both forward and backward slashes.
|
||||||
let query = "C:/root/";
|
let query = "C:/root/";
|
||||||
|
@ -251,6 +251,47 @@ async fn test_open_path_prompt_on_windows(cx: &mut TestAppContext) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
#[cfg_attr(not(target_os = "windows"), ignore)]
|
||||||
|
async fn test_open_path_prompt_on_windows_with_remote(cx: &mut TestAppContext) {
|
||||||
|
let app_state = init_test(cx);
|
||||||
|
app_state
|
||||||
|
.fs
|
||||||
|
.as_fake()
|
||||||
|
.insert_tree(
|
||||||
|
"/root",
|
||||||
|
json!({
|
||||||
|
"a": "A",
|
||||||
|
"dir1": {},
|
||||||
|
"dir2": {}
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
|
||||||
|
|
||||||
|
let (picker, cx) = build_open_path_prompt(project, false, PathStyle::Posix, cx);
|
||||||
|
|
||||||
|
let query = "/root/";
|
||||||
|
insert_query(query, &picker, cx).await;
|
||||||
|
assert_eq!(
|
||||||
|
collect_match_candidates(&picker, cx),
|
||||||
|
vec!["a", "dir1", "dir2"]
|
||||||
|
);
|
||||||
|
assert_eq!(confirm_completion(query, 0, &picker, cx), "/root/a");
|
||||||
|
|
||||||
|
// Confirm completion for the query "/root/d", selecting the second candidate "dir2", since it's a directory, it should add a trailing slash.
|
||||||
|
let query = "/root/d";
|
||||||
|
insert_query(query, &picker, cx).await;
|
||||||
|
assert_eq!(collect_match_candidates(&picker, cx), vec!["dir1", "dir2"]);
|
||||||
|
assert_eq!(confirm_completion(query, 1, &picker, cx), "/root/dir2/");
|
||||||
|
|
||||||
|
let query = "/root/d";
|
||||||
|
insert_query(query, &picker, cx).await;
|
||||||
|
assert_eq!(collect_match_candidates(&picker, cx), vec!["dir1", "dir2"]);
|
||||||
|
assert_eq!(confirm_completion(query, 0, &picker, cx), "/root/dir1/");
|
||||||
|
}
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
async fn test_new_path_prompt(cx: &mut TestAppContext) {
|
async fn test_new_path_prompt(cx: &mut TestAppContext) {
|
||||||
let app_state = init_test(cx);
|
let app_state = init_test(cx);
|
||||||
|
@ -278,7 +319,7 @@ async fn test_new_path_prompt(cx: &mut TestAppContext) {
|
||||||
|
|
||||||
let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
|
let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
|
||||||
|
|
||||||
let (picker, cx) = build_open_path_prompt(project, true, cx);
|
let (picker, cx) = build_open_path_prompt(project, true, PathStyle::current(), cx);
|
||||||
|
|
||||||
insert_query(path!("/root"), &picker, cx).await;
|
insert_query(path!("/root"), &picker, cx).await;
|
||||||
assert_eq!(collect_match_candidates(&picker, cx), vec!["root"]);
|
assert_eq!(collect_match_candidates(&picker, cx), vec!["root"]);
|
||||||
|
@ -315,11 +356,12 @@ fn init_test(cx: &mut TestAppContext) -> Arc<AppState> {
|
||||||
fn build_open_path_prompt(
|
fn build_open_path_prompt(
|
||||||
project: Entity<Project>,
|
project: Entity<Project>,
|
||||||
creating_path: bool,
|
creating_path: bool,
|
||||||
|
path_style: PathStyle,
|
||||||
cx: &mut TestAppContext,
|
cx: &mut TestAppContext,
|
||||||
) -> (Entity<Picker<OpenPathDelegate>>, &mut VisualTestContext) {
|
) -> (Entity<Picker<OpenPathDelegate>>, &mut VisualTestContext) {
|
||||||
let (tx, _) = futures::channel::oneshot::channel();
|
let (tx, _) = futures::channel::oneshot::channel();
|
||||||
let lister = project::DirectoryLister::Project(project.clone());
|
let lister = project::DirectoryLister::Project(project.clone());
|
||||||
let delegate = OpenPathDelegate::new(tx, lister.clone(), creating_path);
|
let delegate = OpenPathDelegate::new(tx, lister.clone(), creating_path, path_style);
|
||||||
|
|
||||||
let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
|
let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
|
||||||
(
|
(
|
||||||
|
|
25
crates/net/Cargo.toml
Normal file
25
crates/net/Cargo.toml
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
[package]
|
||||||
|
name = "net"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition.workspace = true
|
||||||
|
publish.workspace = true
|
||||||
|
license = "GPL-3.0-or-later"
|
||||||
|
|
||||||
|
[lints]
|
||||||
|
workspace = true
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
path = "src/net.rs"
|
||||||
|
doctest = false
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
smol.workspace = true
|
||||||
|
workspace-hack.workspace = true
|
||||||
|
|
||||||
|
[target.'cfg(target_os = "windows")'.dependencies]
|
||||||
|
anyhow.workspace = true
|
||||||
|
async-io = "2.4"
|
||||||
|
windows.workspace = true
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
tempfile.workspace = true
|
1
crates/net/LICENSE-GPL
Symbolic link
1
crates/net/LICENSE-GPL
Symbolic link
|
@ -0,0 +1 @@
|
||||||
|
../../LICENSE-GPL
|
69
crates/net/src/async_net.rs
Normal file
69
crates/net/src/async_net.rs
Normal file
|
@ -0,0 +1,69 @@
|
||||||
|
#[cfg(not(target_os = "windows"))]
|
||||||
|
pub use smol::net::unix::{UnixListener, UnixStream};
|
||||||
|
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
pub use windows::{UnixListener, UnixStream};
|
||||||
|
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
pub mod windows {
|
||||||
|
use std::{
|
||||||
|
io::Result,
|
||||||
|
path::Path,
|
||||||
|
pin::Pin,
|
||||||
|
task::{Context, Poll},
|
||||||
|
};
|
||||||
|
|
||||||
|
use smol::{
|
||||||
|
Async,
|
||||||
|
io::{AsyncRead, AsyncWrite},
|
||||||
|
};
|
||||||
|
|
||||||
|
pub struct UnixListener(Async<crate::UnixListener>);
|
||||||
|
|
||||||
|
impl UnixListener {
|
||||||
|
pub fn bind<P: AsRef<Path>>(path: P) -> Result<Self> {
|
||||||
|
Ok(UnixListener(Async::new(crate::UnixListener::bind(path)?)?))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn accept(&self) -> Result<(UnixStream, ())> {
|
||||||
|
let (sock, _) = self.0.read_with(|listener| listener.accept()).await?;
|
||||||
|
Ok((UnixStream(Async::new(sock)?), ()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct UnixStream(Async<crate::UnixStream>);
|
||||||
|
|
||||||
|
impl UnixStream {
|
||||||
|
pub async fn connect<P: AsRef<Path>>(path: P) -> Result<Self> {
|
||||||
|
Ok(UnixStream(Async::new(crate::UnixStream::connect(path)?)?))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AsyncRead for UnixStream {
|
||||||
|
fn poll_read(
|
||||||
|
mut self: Pin<&mut Self>,
|
||||||
|
cx: &mut Context<'_>,
|
||||||
|
buf: &mut [u8],
|
||||||
|
) -> Poll<Result<usize>> {
|
||||||
|
Pin::new(&mut self.0).poll_read(cx, buf)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AsyncWrite for UnixStream {
|
||||||
|
fn poll_write(
|
||||||
|
mut self: Pin<&mut Self>,
|
||||||
|
cx: &mut Context<'_>,
|
||||||
|
buf: &[u8],
|
||||||
|
) -> Poll<Result<usize>> {
|
||||||
|
Pin::new(&mut self.0).poll_write(cx, buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<()>> {
|
||||||
|
Pin::new(&mut self.0).poll_flush(cx)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn poll_close(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<()>> {
|
||||||
|
Pin::new(&mut self.0).poll_close(cx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
45
crates/net/src/listener.rs
Normal file
45
crates/net/src/listener.rs
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
use std::{
|
||||||
|
io::Result,
|
||||||
|
os::windows::io::{AsSocket, BorrowedSocket},
|
||||||
|
path::Path,
|
||||||
|
};
|
||||||
|
|
||||||
|
use windows::Win32::Networking::WinSock::{SOCKADDR_UN, SOMAXCONN, bind, listen};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
socket::UnixSocket,
|
||||||
|
stream::UnixStream,
|
||||||
|
util::{init, map_ret, sockaddr_un},
|
||||||
|
};
|
||||||
|
|
||||||
|
pub struct UnixListener(UnixSocket);
|
||||||
|
|
||||||
|
impl UnixListener {
|
||||||
|
pub fn bind<P: AsRef<Path>>(path: P) -> Result<Self> {
|
||||||
|
init();
|
||||||
|
let socket = UnixSocket::new()?;
|
||||||
|
let (addr, len) = sockaddr_un(path)?;
|
||||||
|
unsafe {
|
||||||
|
map_ret(bind(
|
||||||
|
socket.as_raw(),
|
||||||
|
&addr as *const _ as *const _,
|
||||||
|
len as i32,
|
||||||
|
))?;
|
||||||
|
map_ret(listen(socket.as_raw(), SOMAXCONN as _))?;
|
||||||
|
}
|
||||||
|
Ok(Self(socket))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn accept(&self) -> Result<(UnixStream, ())> {
|
||||||
|
let mut storage = SOCKADDR_UN::default();
|
||||||
|
let mut len = std::mem::size_of_val(&storage) as i32;
|
||||||
|
let raw = self.0.accept(&mut storage as *mut _ as *mut _, &mut len)?;
|
||||||
|
Ok((UnixStream::new(raw), ()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AsSocket for UnixListener {
|
||||||
|
fn as_socket(&self) -> BorrowedSocket<'_> {
|
||||||
|
unsafe { BorrowedSocket::borrow_raw(self.0.as_raw().0 as _) }
|
||||||
|
}
|
||||||
|
}
|
107
crates/net/src/net.rs
Normal file
107
crates/net/src/net.rs
Normal file
|
@ -0,0 +1,107 @@
|
||||||
|
pub mod async_net;
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
pub mod listener;
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
pub mod socket;
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
pub mod stream;
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
mod util;
|
||||||
|
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
pub use listener::*;
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
pub use socket::*;
|
||||||
|
#[cfg(not(target_os = "windows"))]
|
||||||
|
pub use std::os::unix::net::{UnixListener, UnixStream};
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
pub use stream::*;
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use std::io::{Read, Write};
|
||||||
|
|
||||||
|
use smol::io::{AsyncReadExt, AsyncWriteExt};
|
||||||
|
|
||||||
|
const SERVER_MESSAGE: &str = "Connection closed";
|
||||||
|
const CLIENT_MESSAGE: &str = "Hello, server!";
|
||||||
|
const BUFFER_SIZE: usize = 32;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_windows_listener() -> std::io::Result<()> {
|
||||||
|
use crate::{UnixListener, UnixStream};
|
||||||
|
|
||||||
|
let temp = tempfile::tempdir()?;
|
||||||
|
let socket = temp.path().join("socket.sock");
|
||||||
|
let listener = UnixListener::bind(&socket)?;
|
||||||
|
|
||||||
|
// Server
|
||||||
|
let server = std::thread::spawn(move || {
|
||||||
|
let (mut stream, _) = listener.accept().unwrap();
|
||||||
|
|
||||||
|
// Read data from the client
|
||||||
|
let mut buffer = [0; BUFFER_SIZE];
|
||||||
|
let bytes_read = stream.read(&mut buffer).unwrap();
|
||||||
|
let string = String::from_utf8_lossy(&buffer[..bytes_read]);
|
||||||
|
assert_eq!(string, CLIENT_MESSAGE);
|
||||||
|
|
||||||
|
// Send a message back to the client
|
||||||
|
stream.write_all(SERVER_MESSAGE.as_bytes()).unwrap();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Client
|
||||||
|
let mut client = UnixStream::connect(&socket)?;
|
||||||
|
|
||||||
|
// Send data to the server
|
||||||
|
client.write_all(CLIENT_MESSAGE.as_bytes())?;
|
||||||
|
let mut buffer = [0; BUFFER_SIZE];
|
||||||
|
|
||||||
|
// Read the response from the server
|
||||||
|
let bytes_read = client.read(&mut buffer)?;
|
||||||
|
let string = String::from_utf8_lossy(&buffer[..bytes_read]);
|
||||||
|
assert_eq!(string, SERVER_MESSAGE);
|
||||||
|
client.flush()?;
|
||||||
|
|
||||||
|
server.join().unwrap();
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_unix_listener() -> std::io::Result<()> {
|
||||||
|
use crate::async_net::{UnixListener, UnixStream};
|
||||||
|
|
||||||
|
smol::block_on(async {
|
||||||
|
let temp = tempfile::tempdir()?;
|
||||||
|
let socket = temp.path().join("socket.sock");
|
||||||
|
let listener = UnixListener::bind(&socket)?;
|
||||||
|
|
||||||
|
// Server
|
||||||
|
let server = smol::spawn(async move {
|
||||||
|
let (mut stream, _) = listener.accept().await.unwrap();
|
||||||
|
|
||||||
|
// Read data from the client
|
||||||
|
let mut buffer = [0; BUFFER_SIZE];
|
||||||
|
let bytes_read = stream.read(&mut buffer).await.unwrap();
|
||||||
|
let string = String::from_utf8_lossy(&buffer[..bytes_read]);
|
||||||
|
assert_eq!(string, CLIENT_MESSAGE);
|
||||||
|
|
||||||
|
// Send a message back to the client
|
||||||
|
stream.write_all(SERVER_MESSAGE.as_bytes()).await.unwrap();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Client
|
||||||
|
let mut client = UnixStream::connect(&socket).await?;
|
||||||
|
client.write_all(CLIENT_MESSAGE.as_bytes()).await?;
|
||||||
|
|
||||||
|
// Read the response from the server
|
||||||
|
let mut buffer = [0; BUFFER_SIZE];
|
||||||
|
let bytes_read = client.read(&mut buffer).await?;
|
||||||
|
let string = String::from_utf8_lossy(&buffer[..bytes_read]);
|
||||||
|
assert_eq!(string, "Connection closed");
|
||||||
|
client.flush().await?;
|
||||||
|
|
||||||
|
server.await;
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
59
crates/net/src/socket.rs
Normal file
59
crates/net/src/socket.rs
Normal file
|
@ -0,0 +1,59 @@
|
||||||
|
use std::io::{Error, ErrorKind, Result};
|
||||||
|
|
||||||
|
use windows::Win32::{
|
||||||
|
Foundation::{HANDLE, HANDLE_FLAG_INHERIT, HANDLE_FLAGS, SetHandleInformation},
|
||||||
|
Networking::WinSock::{
|
||||||
|
AF_UNIX, SEND_RECV_FLAGS, SOCK_STREAM, SOCKADDR, SOCKET, WSA_FLAG_OVERLAPPED,
|
||||||
|
WSAEWOULDBLOCK, WSASocketW, accept, closesocket, recv, send,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::util::map_ret;
|
||||||
|
|
||||||
|
pub struct UnixSocket(SOCKET);
|
||||||
|
|
||||||
|
impl UnixSocket {
|
||||||
|
pub fn new() -> Result<Self> {
|
||||||
|
unsafe {
|
||||||
|
let raw = WSASocketW(AF_UNIX as _, SOCK_STREAM.0, 0, None, 0, WSA_FLAG_OVERLAPPED)?;
|
||||||
|
SetHandleInformation(
|
||||||
|
HANDLE(raw.0 as _),
|
||||||
|
HANDLE_FLAG_INHERIT.0,
|
||||||
|
HANDLE_FLAGS::default(),
|
||||||
|
)?;
|
||||||
|
Ok(Self(raw))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn as_raw(&self) -> SOCKET {
|
||||||
|
self.0
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn accept(&self, storage: *mut SOCKADDR, len: &mut i32) -> Result<Self> {
|
||||||
|
match unsafe { accept(self.0, Some(storage), Some(len)) } {
|
||||||
|
Ok(sock) => Ok(Self(sock)),
|
||||||
|
Err(err) => {
|
||||||
|
let wsa_err = unsafe { windows::Win32::Networking::WinSock::WSAGetLastError().0 };
|
||||||
|
if wsa_err == WSAEWOULDBLOCK.0 {
|
||||||
|
Err(Error::new(ErrorKind::WouldBlock, "accept would block"))
|
||||||
|
} else {
|
||||||
|
Err(err.into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn recv(&self, buf: &mut [u8]) -> Result<usize> {
|
||||||
|
map_ret(unsafe { recv(self.0, buf, SEND_RECV_FLAGS::default()) })
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn send(&self, buf: &[u8]) -> Result<usize> {
|
||||||
|
map_ret(unsafe { send(self.0, buf, SEND_RECV_FLAGS::default()) })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for UnixSocket {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
unsafe { closesocket(self.0) };
|
||||||
|
}
|
||||||
|
}
|
60
crates/net/src/stream.rs
Normal file
60
crates/net/src/stream.rs
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
use std::{
|
||||||
|
io::{Read, Result, Write},
|
||||||
|
os::windows::io::{AsSocket, BorrowedSocket},
|
||||||
|
path::Path,
|
||||||
|
};
|
||||||
|
|
||||||
|
use async_io::IoSafe;
|
||||||
|
use windows::Win32::Networking::WinSock::connect;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
socket::UnixSocket,
|
||||||
|
util::{init, map_ret, sockaddr_un},
|
||||||
|
};
|
||||||
|
|
||||||
|
pub struct UnixStream(UnixSocket);
|
||||||
|
|
||||||
|
unsafe impl IoSafe for UnixStream {}
|
||||||
|
|
||||||
|
impl UnixStream {
|
||||||
|
pub fn new(socket: UnixSocket) -> Self {
|
||||||
|
Self(socket)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn connect<P: AsRef<Path>>(path: P) -> Result<Self> {
|
||||||
|
init();
|
||||||
|
unsafe {
|
||||||
|
let inner = UnixSocket::new()?;
|
||||||
|
let (addr, len) = sockaddr_un(path)?;
|
||||||
|
|
||||||
|
map_ret(connect(
|
||||||
|
inner.as_raw(),
|
||||||
|
&addr as *const _ as *const _,
|
||||||
|
len as i32,
|
||||||
|
))?;
|
||||||
|
Ok(Self(inner))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Read for UnixStream {
|
||||||
|
fn read(&mut self, buf: &mut [u8]) -> Result<usize> {
|
||||||
|
self.0.recv(buf)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Write for UnixStream {
|
||||||
|
fn write(&mut self, buf: &[u8]) -> Result<usize> {
|
||||||
|
self.0.send(buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn flush(&mut self) -> Result<()> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AsSocket for UnixStream {
|
||||||
|
fn as_socket(&self) -> BorrowedSocket<'_> {
|
||||||
|
unsafe { BorrowedSocket::borrow_raw(self.0.as_raw().0 as _) }
|
||||||
|
}
|
||||||
|
}
|
76
crates/net/src/util.rs
Normal file
76
crates/net/src/util.rs
Normal file
|
@ -0,0 +1,76 @@
|
||||||
|
use std::{
|
||||||
|
io::{Error, ErrorKind, Result},
|
||||||
|
path::Path,
|
||||||
|
sync::Once,
|
||||||
|
};
|
||||||
|
|
||||||
|
use windows::Win32::Networking::WinSock::{
|
||||||
|
ADDRESS_FAMILY, AF_UNIX, SOCKADDR_UN, SOCKET_ERROR, WSAGetLastError, WSAStartup,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub(crate) fn init() {
|
||||||
|
static ONCE: Once = Once::new();
|
||||||
|
|
||||||
|
ONCE.call_once(|| unsafe {
|
||||||
|
let mut wsa_data = std::mem::zeroed();
|
||||||
|
let result = WSAStartup(0x202, &mut wsa_data);
|
||||||
|
if result != 0 {
|
||||||
|
panic!("WSAStartup failed: {}", result);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// https://devblogs.microsoft.com/commandline/af_unix-comes-to-windows/
|
||||||
|
pub(crate) fn sockaddr_un<P: AsRef<Path>>(path: P) -> Result<(SOCKADDR_UN, usize)> {
|
||||||
|
let mut addr = SOCKADDR_UN::default();
|
||||||
|
addr.sun_family = ADDRESS_FAMILY(AF_UNIX);
|
||||||
|
|
||||||
|
let bytes = path
|
||||||
|
.as_ref()
|
||||||
|
.to_str()
|
||||||
|
.map(|s| s.as_bytes())
|
||||||
|
.ok_or(ErrorKind::InvalidInput)?;
|
||||||
|
|
||||||
|
if bytes.contains(&0) {
|
||||||
|
return Err(Error::new(
|
||||||
|
ErrorKind::InvalidInput,
|
||||||
|
"paths may not contain interior null bytes",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if bytes.len() >= addr.sun_path.len() {
|
||||||
|
return Err(Error::new(
|
||||||
|
ErrorKind::InvalidInput,
|
||||||
|
"path must be shorter than SUN_LEN",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
unsafe {
|
||||||
|
std::ptr::copy_nonoverlapping(
|
||||||
|
bytes.as_ptr(),
|
||||||
|
addr.sun_path.as_mut_ptr().cast(),
|
||||||
|
bytes.len(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut len = sun_path_offset(&addr) + bytes.len();
|
||||||
|
match bytes.first() {
|
||||||
|
Some(&0) | None => {}
|
||||||
|
Some(_) => len += 1,
|
||||||
|
}
|
||||||
|
Ok((addr, len))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn map_ret(ret: i32) -> Result<usize> {
|
||||||
|
if ret == SOCKET_ERROR {
|
||||||
|
Err(Error::from_raw_os_error(unsafe { WSAGetLastError().0 }))
|
||||||
|
} else {
|
||||||
|
Ok(ret as usize)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn sun_path_offset(addr: &SOCKADDR_UN) -> usize {
|
||||||
|
// Work with an actual instance of the type since using a null pointer is UB
|
||||||
|
let base = addr as *const _ as usize;
|
||||||
|
let path = &addr.sun_path as *const _ as usize;
|
||||||
|
path - base
|
||||||
|
}
|
|
@ -33,7 +33,7 @@ use http_client::HttpClient;
|
||||||
use language::{Buffer, LanguageToolchainStore, language_settings::InlayHintKind};
|
use language::{Buffer, LanguageToolchainStore, language_settings::InlayHintKind};
|
||||||
use node_runtime::NodeRuntime;
|
use node_runtime::NodeRuntime;
|
||||||
|
|
||||||
use remote::SshRemoteClient;
|
use remote::{SshRemoteClient, ssh_session::SshArgs};
|
||||||
use rpc::{
|
use rpc::{
|
||||||
AnyProtoClient, TypedEnvelope,
|
AnyProtoClient, TypedEnvelope,
|
||||||
proto::{self},
|
proto::{self},
|
||||||
|
@ -253,10 +253,15 @@ impl DapStore {
|
||||||
cx.spawn(async move |_, cx| {
|
cx.spawn(async move |_, cx| {
|
||||||
let response = request.await?;
|
let response = request.await?;
|
||||||
let binary = DebugAdapterBinary::from_proto(response)?;
|
let binary = DebugAdapterBinary::from_proto(response)?;
|
||||||
let mut ssh_command = ssh_client.read_with(cx, |ssh, _| {
|
let (mut ssh_command, envs, path_style) =
|
||||||
anyhow::Ok(SshCommand {
|
ssh_client.read_with(cx, |ssh, _| {
|
||||||
arguments: ssh.ssh_args().context("SSH arguments not found")?,
|
let (SshArgs { arguments, envs }, path_style) =
|
||||||
})
|
ssh.ssh_info().context("SSH arguments not found")?;
|
||||||
|
anyhow::Ok((
|
||||||
|
SshCommand { arguments },
|
||||||
|
envs.unwrap_or_default(),
|
||||||
|
path_style,
|
||||||
|
))
|
||||||
})??;
|
})??;
|
||||||
|
|
||||||
let mut connection = None;
|
let mut connection = None;
|
||||||
|
@ -282,12 +287,13 @@ impl DapStore {
|
||||||
binary.cwd.as_deref(),
|
binary.cwd.as_deref(),
|
||||||
binary.envs,
|
binary.envs,
|
||||||
None,
|
None,
|
||||||
|
path_style,
|
||||||
);
|
);
|
||||||
|
|
||||||
Ok(DebugAdapterBinary {
|
Ok(DebugAdapterBinary {
|
||||||
command: Some(program),
|
command: Some(program),
|
||||||
arguments: args,
|
arguments: args,
|
||||||
envs: HashMap::default(),
|
envs,
|
||||||
cwd: None,
|
cwd: None,
|
||||||
connection,
|
connection,
|
||||||
request_args: binary.request_args,
|
request_args: binary.request_args,
|
||||||
|
|
|
@ -117,7 +117,7 @@ use text::{Anchor, BufferId, Point};
|
||||||
use toolchain_store::EmptyToolchainStore;
|
use toolchain_store::EmptyToolchainStore;
|
||||||
use util::{
|
use util::{
|
||||||
ResultExt as _,
|
ResultExt as _,
|
||||||
paths::{SanitizedPath, compare_paths},
|
paths::{PathStyle, RemotePathBuf, SanitizedPath, compare_paths},
|
||||||
};
|
};
|
||||||
use worktree::{CreatedEntry, Snapshot, Traversal};
|
use worktree::{CreatedEntry, Snapshot, Traversal};
|
||||||
pub use worktree::{
|
pub use worktree::{
|
||||||
|
@ -1159,9 +1159,11 @@ impl Project {
|
||||||
let snippets =
|
let snippets =
|
||||||
SnippetProvider::new(fs.clone(), BTreeSet::from_iter([global_snippets_dir]), cx);
|
SnippetProvider::new(fs.clone(), BTreeSet::from_iter([global_snippets_dir]), cx);
|
||||||
|
|
||||||
let ssh_proto = ssh.read(cx).proto_client();
|
let (ssh_proto, path_style) =
|
||||||
let worktree_store =
|
ssh.read_with(cx, |ssh, _| (ssh.proto_client(), ssh.path_style()));
|
||||||
cx.new(|_| WorktreeStore::remote(false, ssh_proto.clone(), SSH_PROJECT_ID));
|
let worktree_store = cx.new(|_| {
|
||||||
|
WorktreeStore::remote(false, ssh_proto.clone(), SSH_PROJECT_ID, path_style)
|
||||||
|
});
|
||||||
cx.subscribe(&worktree_store, Self::on_worktree_store_event)
|
cx.subscribe(&worktree_store, Self::on_worktree_store_event)
|
||||||
.detach();
|
.detach();
|
||||||
|
|
||||||
|
@ -1410,8 +1412,15 @@ impl Project {
|
||||||
let remote_id = response.payload.project_id;
|
let remote_id = response.payload.project_id;
|
||||||
let role = response.payload.role();
|
let role = response.payload.role();
|
||||||
|
|
||||||
|
// todo(zjk)
|
||||||
|
// Set the proper path style based on the remote
|
||||||
let worktree_store = cx.new(|_| {
|
let worktree_store = cx.new(|_| {
|
||||||
WorktreeStore::remote(true, client.clone().into(), response.payload.project_id)
|
WorktreeStore::remote(
|
||||||
|
true,
|
||||||
|
client.clone().into(),
|
||||||
|
response.payload.project_id,
|
||||||
|
PathStyle::Posix,
|
||||||
|
)
|
||||||
})?;
|
})?;
|
||||||
let buffer_store = cx.new(|cx| {
|
let buffer_store = cx.new(|cx| {
|
||||||
BufferStore::remote(worktree_store.clone(), client.clone().into(), remote_id, cx)
|
BufferStore::remote(worktree_store.clone(), client.clone().into(), remote_id, cx)
|
||||||
|
@ -4039,7 +4048,8 @@ impl Project {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
} else if let Some(ssh_client) = self.ssh_client.as_ref() {
|
} else if let Some(ssh_client) = self.ssh_client.as_ref() {
|
||||||
let request_path = Path::new(path);
|
let path_style = ssh_client.read(cx).path_style();
|
||||||
|
let request_path = RemotePathBuf::from_str(path, path_style);
|
||||||
let request = ssh_client
|
let request = ssh_client
|
||||||
.read(cx)
|
.read(cx)
|
||||||
.proto_client()
|
.proto_client()
|
||||||
|
|
|
@ -4,6 +4,7 @@ use collections::HashMap;
|
||||||
use gpui::{AnyWindowHandle, App, AppContext as _, Context, Entity, Task, WeakEntity};
|
use gpui::{AnyWindowHandle, App, AppContext as _, Context, Entity, Task, WeakEntity};
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
use language::LanguageName;
|
use language::LanguageName;
|
||||||
|
use remote::ssh_session::SshArgs;
|
||||||
use settings::{Settings, SettingsLocation};
|
use settings::{Settings, SettingsLocation};
|
||||||
use smol::channel::bounded;
|
use smol::channel::bounded;
|
||||||
use std::{
|
use std::{
|
||||||
|
@ -17,7 +18,10 @@ use terminal::{
|
||||||
TaskState, TaskStatus, Terminal, TerminalBuilder,
|
TaskState, TaskStatus, Terminal, TerminalBuilder,
|
||||||
terminal_settings::{self, TerminalSettings, VenvSettings},
|
terminal_settings::{self, TerminalSettings, VenvSettings},
|
||||||
};
|
};
|
||||||
use util::ResultExt;
|
use util::{
|
||||||
|
ResultExt,
|
||||||
|
paths::{PathStyle, RemotePathBuf},
|
||||||
|
};
|
||||||
|
|
||||||
pub struct Terminals {
|
pub struct Terminals {
|
||||||
pub(crate) local_handles: Vec<WeakEntity<terminal::Terminal>>,
|
pub(crate) local_handles: Vec<WeakEntity<terminal::Terminal>>,
|
||||||
|
@ -47,6 +51,13 @@ impl SshCommand {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct SshDetails {
|
||||||
|
pub host: String,
|
||||||
|
pub ssh_command: SshCommand,
|
||||||
|
pub envs: Option<HashMap<String, String>>,
|
||||||
|
pub path_style: PathStyle,
|
||||||
|
}
|
||||||
|
|
||||||
impl Project {
|
impl Project {
|
||||||
pub fn active_project_directory(&self, cx: &App) -> Option<Arc<Path>> {
|
pub fn active_project_directory(&self, cx: &App) -> Option<Arc<Path>> {
|
||||||
let worktree = self
|
let worktree = self
|
||||||
|
@ -68,14 +79,16 @@ impl Project {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn ssh_details(&self, cx: &App) -> Option<(String, SshCommand)> {
|
pub fn ssh_details(&self, cx: &App) -> Option<SshDetails> {
|
||||||
if let Some(ssh_client) = &self.ssh_client {
|
if let Some(ssh_client) = &self.ssh_client {
|
||||||
let ssh_client = ssh_client.read(cx);
|
let ssh_client = ssh_client.read(cx);
|
||||||
if let Some(args) = ssh_client.ssh_args() {
|
if let Some((SshArgs { arguments, envs }, path_style)) = ssh_client.ssh_info() {
|
||||||
return Some((
|
return Some(SshDetails {
|
||||||
ssh_client.connection_options().host.clone(),
|
host: ssh_client.connection_options().host.clone(),
|
||||||
SshCommand { arguments: args },
|
ssh_command: SshCommand { arguments },
|
||||||
));
|
envs,
|
||||||
|
path_style,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -158,17 +171,26 @@ impl Project {
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
env.extend(settings.env.clone());
|
env.extend(settings.env.clone());
|
||||||
|
|
||||||
match &self.ssh_details(cx) {
|
match self.ssh_details(cx) {
|
||||||
Some((_, ssh_command)) => {
|
Some(SshDetails {
|
||||||
let (command, args) = wrap_for_ssh(
|
|
||||||
ssh_command,
|
ssh_command,
|
||||||
|
envs,
|
||||||
|
path_style,
|
||||||
|
..
|
||||||
|
}) => {
|
||||||
|
let (command, args) = wrap_for_ssh(
|
||||||
|
&ssh_command,
|
||||||
Some((&command, &args)),
|
Some((&command, &args)),
|
||||||
path.as_deref(),
|
path.as_deref(),
|
||||||
env,
|
env,
|
||||||
None,
|
None,
|
||||||
|
path_style,
|
||||||
);
|
);
|
||||||
let mut command = std::process::Command::new(command);
|
let mut command = std::process::Command::new(command);
|
||||||
command.args(args);
|
command.args(args);
|
||||||
|
if let Some(envs) = envs {
|
||||||
|
command.envs(envs);
|
||||||
|
}
|
||||||
command
|
command
|
||||||
}
|
}
|
||||||
None => {
|
None => {
|
||||||
|
@ -202,6 +224,7 @@ impl Project {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let ssh_details = this.ssh_details(cx);
|
let ssh_details = this.ssh_details(cx);
|
||||||
|
let is_ssh_terminal = ssh_details.is_some();
|
||||||
|
|
||||||
let mut settings_location = None;
|
let mut settings_location = None;
|
||||||
if let Some(path) = path.as_ref() {
|
if let Some(path) = path.as_ref() {
|
||||||
|
@ -226,11 +249,7 @@ impl Project {
|
||||||
// precedence.
|
// precedence.
|
||||||
env.extend(settings.env.clone());
|
env.extend(settings.env.clone());
|
||||||
|
|
||||||
let local_path = if ssh_details.is_none() {
|
let local_path = if is_ssh_terminal { None } else { path.clone() };
|
||||||
path.clone()
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut python_venv_activate_command = None;
|
let mut python_venv_activate_command = None;
|
||||||
|
|
||||||
|
@ -241,8 +260,13 @@ impl Project {
|
||||||
this.python_activate_command(python_venv_directory, &settings.detect_venv);
|
this.python_activate_command(python_venv_directory, &settings.detect_venv);
|
||||||
}
|
}
|
||||||
|
|
||||||
match &ssh_details {
|
match ssh_details {
|
||||||
Some((host, ssh_command)) => {
|
Some(SshDetails {
|
||||||
|
host,
|
||||||
|
ssh_command,
|
||||||
|
envs,
|
||||||
|
path_style,
|
||||||
|
}) => {
|
||||||
log::debug!("Connecting to a remote server: {ssh_command:?}");
|
log::debug!("Connecting to a remote server: {ssh_command:?}");
|
||||||
|
|
||||||
// Alacritty sets its terminfo to `alacritty`, this requiring hosts to have it installed
|
// Alacritty sets its terminfo to `alacritty`, this requiring hosts to have it installed
|
||||||
|
@ -252,9 +276,18 @@ impl Project {
|
||||||
env.entry("TERM".to_string())
|
env.entry("TERM".to_string())
|
||||||
.or_insert_with(|| "xterm-256color".to_string());
|
.or_insert_with(|| "xterm-256color".to_string());
|
||||||
|
|
||||||
let (program, args) =
|
let (program, args) = wrap_for_ssh(
|
||||||
wrap_for_ssh(&ssh_command, None, path.as_deref(), env, None);
|
&ssh_command,
|
||||||
|
None,
|
||||||
|
path.as_deref(),
|
||||||
|
env,
|
||||||
|
None,
|
||||||
|
path_style,
|
||||||
|
);
|
||||||
env = HashMap::default();
|
env = HashMap::default();
|
||||||
|
if let Some(envs) = envs {
|
||||||
|
env.extend(envs);
|
||||||
|
}
|
||||||
(
|
(
|
||||||
Option::<TaskState>::None,
|
Option::<TaskState>::None,
|
||||||
Shell::WithArguments {
|
Shell::WithArguments {
|
||||||
|
@ -290,8 +323,13 @@ impl Project {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
match &ssh_details {
|
match ssh_details {
|
||||||
Some((host, ssh_command)) => {
|
Some(SshDetails {
|
||||||
|
host,
|
||||||
|
ssh_command,
|
||||||
|
envs,
|
||||||
|
path_style,
|
||||||
|
}) => {
|
||||||
log::debug!("Connecting to a remote server: {ssh_command:?}");
|
log::debug!("Connecting to a remote server: {ssh_command:?}");
|
||||||
env.entry("TERM".to_string())
|
env.entry("TERM".to_string())
|
||||||
.or_insert_with(|| "xterm-256color".to_string());
|
.or_insert_with(|| "xterm-256color".to_string());
|
||||||
|
@ -304,8 +342,12 @@ impl Project {
|
||||||
path.as_deref(),
|
path.as_deref(),
|
||||||
env,
|
env,
|
||||||
python_venv_directory.as_deref(),
|
python_venv_directory.as_deref(),
|
||||||
|
path_style,
|
||||||
);
|
);
|
||||||
env = HashMap::default();
|
env = HashMap::default();
|
||||||
|
if let Some(envs) = envs {
|
||||||
|
env.extend(envs);
|
||||||
|
}
|
||||||
(
|
(
|
||||||
task_state,
|
task_state,
|
||||||
Shell::WithArguments {
|
Shell::WithArguments {
|
||||||
|
@ -343,7 +385,7 @@ impl Project {
|
||||||
settings.cursor_shape.unwrap_or_default(),
|
settings.cursor_shape.unwrap_or_default(),
|
||||||
settings.alternate_scroll,
|
settings.alternate_scroll,
|
||||||
settings.max_scroll_history_lines,
|
settings.max_scroll_history_lines,
|
||||||
ssh_details.is_some(),
|
is_ssh_terminal,
|
||||||
window,
|
window,
|
||||||
completion_tx,
|
completion_tx,
|
||||||
cx,
|
cx,
|
||||||
|
@ -533,6 +575,7 @@ pub fn wrap_for_ssh(
|
||||||
path: Option<&Path>,
|
path: Option<&Path>,
|
||||||
env: HashMap<String, String>,
|
env: HashMap<String, String>,
|
||||||
venv_directory: Option<&Path>,
|
venv_directory: Option<&Path>,
|
||||||
|
path_style: PathStyle,
|
||||||
) -> (String, Vec<String>) {
|
) -> (String, Vec<String>) {
|
||||||
let to_run = if let Some((command, args)) = command {
|
let to_run = if let Some((command, args)) = command {
|
||||||
// DEFAULT_REMOTE_SHELL is '"${SHELL:-sh}"' so must not be escaped
|
// DEFAULT_REMOTE_SHELL is '"${SHELL:-sh}"' so must not be escaped
|
||||||
|
@ -555,24 +598,25 @@ pub fn wrap_for_ssh(
|
||||||
}
|
}
|
||||||
if let Some(venv_directory) = venv_directory {
|
if let Some(venv_directory) = venv_directory {
|
||||||
if let Ok(str) = shlex::try_quote(venv_directory.to_string_lossy().as_ref()) {
|
if let Ok(str) = shlex::try_quote(venv_directory.to_string_lossy().as_ref()) {
|
||||||
env_changes.push_str(&format!("PATH={}:$PATH ", str));
|
let path = RemotePathBuf::new(PathBuf::from(str.to_string()), path_style).to_string();
|
||||||
|
env_changes.push_str(&format!("PATH={}:$PATH ", path));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let commands = if let Some(path) = path {
|
let commands = if let Some(path) = path {
|
||||||
let path_string = path.to_string_lossy().to_string();
|
let path = RemotePathBuf::new(path.to_path_buf(), path_style).to_string();
|
||||||
// shlex will wrap the command in single quotes (''), disabling ~ expansion,
|
// shlex will wrap the command in single quotes (''), disabling ~ expansion,
|
||||||
// replace ith with something that works
|
// replace ith with something that works
|
||||||
let tilde_prefix = "~/";
|
let tilde_prefix = "~/";
|
||||||
if path.starts_with(tilde_prefix) {
|
if path.starts_with(tilde_prefix) {
|
||||||
let trimmed_path = path_string
|
let trimmed_path = path
|
||||||
.trim_start_matches("/")
|
.trim_start_matches("/")
|
||||||
.trim_start_matches("~")
|
.trim_start_matches("~")
|
||||||
.trim_start_matches("/");
|
.trim_start_matches("/");
|
||||||
|
|
||||||
format!("cd \"$HOME/{trimmed_path}\"; {env_changes} {to_run}")
|
format!("cd \"$HOME/{trimmed_path}\"; {env_changes} {to_run}")
|
||||||
} else {
|
} else {
|
||||||
format!("cd {path:?}; {env_changes} {to_run}")
|
format!("cd {path}; {env_changes} {to_run}")
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
format!("cd; {env_changes} {to_run}")
|
format!("cd; {env_changes} {to_run}")
|
||||||
|
|
|
@ -25,7 +25,10 @@ use smol::{
|
||||||
stream::StreamExt,
|
stream::StreamExt,
|
||||||
};
|
};
|
||||||
use text::ReplicaId;
|
use text::ReplicaId;
|
||||||
use util::{ResultExt, paths::SanitizedPath};
|
use util::{
|
||||||
|
ResultExt,
|
||||||
|
paths::{PathStyle, RemotePathBuf, SanitizedPath},
|
||||||
|
};
|
||||||
use worktree::{
|
use worktree::{
|
||||||
Entry, ProjectEntryId, UpdatedEntriesSet, UpdatedGitRepositoriesSet, Worktree, WorktreeId,
|
Entry, ProjectEntryId, UpdatedEntriesSet, UpdatedGitRepositoriesSet, Worktree, WorktreeId,
|
||||||
WorktreeSettings,
|
WorktreeSettings,
|
||||||
|
@ -46,6 +49,7 @@ enum WorktreeStoreState {
|
||||||
Remote {
|
Remote {
|
||||||
upstream_client: AnyProtoClient,
|
upstream_client: AnyProtoClient,
|
||||||
upstream_project_id: u64,
|
upstream_project_id: u64,
|
||||||
|
path_style: PathStyle,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -100,6 +104,7 @@ impl WorktreeStore {
|
||||||
retain_worktrees: bool,
|
retain_worktrees: bool,
|
||||||
upstream_client: AnyProtoClient,
|
upstream_client: AnyProtoClient,
|
||||||
upstream_project_id: u64,
|
upstream_project_id: u64,
|
||||||
|
path_style: PathStyle,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
next_entry_id: Default::default(),
|
next_entry_id: Default::default(),
|
||||||
|
@ -111,6 +116,7 @@ impl WorktreeStore {
|
||||||
state: WorktreeStoreState::Remote {
|
state: WorktreeStoreState::Remote {
|
||||||
upstream_client,
|
upstream_client,
|
||||||
upstream_project_id,
|
upstream_project_id,
|
||||||
|
path_style,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -214,17 +220,16 @@ impl WorktreeStore {
|
||||||
if !self.loading_worktrees.contains_key(&abs_path) {
|
if !self.loading_worktrees.contains_key(&abs_path) {
|
||||||
let task = match &self.state {
|
let task = match &self.state {
|
||||||
WorktreeStoreState::Remote {
|
WorktreeStoreState::Remote {
|
||||||
upstream_client, ..
|
upstream_client,
|
||||||
|
path_style,
|
||||||
|
..
|
||||||
} => {
|
} => {
|
||||||
if upstream_client.is_via_collab() {
|
if upstream_client.is_via_collab() {
|
||||||
Task::ready(Err(Arc::new(anyhow!("cannot create worktrees via collab"))))
|
Task::ready(Err(Arc::new(anyhow!("cannot create worktrees via collab"))))
|
||||||
} else {
|
} else {
|
||||||
self.create_ssh_worktree(
|
let abs_path =
|
||||||
upstream_client.clone(),
|
RemotePathBuf::new(abs_path.as_path().to_path_buf(), *path_style);
|
||||||
abs_path.clone(),
|
self.create_ssh_worktree(upstream_client.clone(), abs_path, visible, cx)
|
||||||
visible,
|
|
||||||
cx,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
WorktreeStoreState::Local { fs } => {
|
WorktreeStoreState::Local { fs } => {
|
||||||
|
@ -250,11 +255,12 @@ impl WorktreeStore {
|
||||||
fn create_ssh_worktree(
|
fn create_ssh_worktree(
|
||||||
&mut self,
|
&mut self,
|
||||||
client: AnyProtoClient,
|
client: AnyProtoClient,
|
||||||
abs_path: impl Into<SanitizedPath>,
|
abs_path: RemotePathBuf,
|
||||||
visible: bool,
|
visible: bool,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) -> Task<Result<Entity<Worktree>, Arc<anyhow::Error>>> {
|
) -> Task<Result<Entity<Worktree>, Arc<anyhow::Error>>> {
|
||||||
let mut abs_path = Into::<SanitizedPath>::into(abs_path).to_string();
|
let path_style = abs_path.path_style();
|
||||||
|
let mut abs_path = abs_path.to_string();
|
||||||
// If we start with `/~` that means the ssh path was something like `ssh://user@host/~/home-dir-folder/`
|
// If we start with `/~` that means the ssh path was something like `ssh://user@host/~/home-dir-folder/`
|
||||||
// in which case want to strip the leading the `/`.
|
// in which case want to strip the leading the `/`.
|
||||||
// On the host-side, the `~` will get expanded.
|
// On the host-side, the `~` will get expanded.
|
||||||
|
@ -265,10 +271,11 @@ impl WorktreeStore {
|
||||||
if abs_path.is_empty() {
|
if abs_path.is_empty() {
|
||||||
abs_path = "~/".to_string();
|
abs_path = "~/".to_string();
|
||||||
}
|
}
|
||||||
|
|
||||||
cx.spawn(async move |this, cx| {
|
cx.spawn(async move |this, cx| {
|
||||||
let this = this.upgrade().context("Dropped worktree store")?;
|
let this = this.upgrade().context("Dropped worktree store")?;
|
||||||
|
|
||||||
let path = Path::new(abs_path.as_str());
|
let path = RemotePathBuf::new(abs_path.into(), path_style);
|
||||||
let response = client
|
let response = client
|
||||||
.request(proto::AddWorktree {
|
.request(proto::AddWorktree {
|
||||||
project_id: SSH_PROJECT_ID,
|
project_id: SSH_PROJECT_ID,
|
||||||
|
|
|
@ -27,3 +27,4 @@ prost-build.workspace = true
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
collections = { workspace = true, features = ["test-support"] }
|
collections = { workspace = true, features = ["test-support"] }
|
||||||
|
typed-path = "0.11"
|
||||||
|
|
|
@ -127,51 +127,46 @@ pub trait ToProto {
|
||||||
fn to_proto(self) -> String;
|
fn to_proto(self) -> String;
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FromProto for PathBuf {
|
#[inline]
|
||||||
|
fn from_proto_path(proto: String) -> PathBuf {
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
fn from_proto(proto: String) -> Self {
|
let proto = proto.replace('/', "\\");
|
||||||
proto.split("/").collect()
|
|
||||||
}
|
PathBuf::from(proto)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn to_proto_path(path: &Path) -> String {
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
let proto = path.to_string_lossy().replace('\\', "/");
|
||||||
|
|
||||||
#[cfg(not(target_os = "windows"))]
|
#[cfg(not(target_os = "windows"))]
|
||||||
|
let proto = path.to_string_lossy().to_string();
|
||||||
|
|
||||||
|
proto
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromProto for PathBuf {
|
||||||
fn from_proto(proto: String) -> Self {
|
fn from_proto(proto: String) -> Self {
|
||||||
PathBuf::from(proto)
|
from_proto_path(proto)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FromProto for Arc<Path> {
|
impl FromProto for Arc<Path> {
|
||||||
fn from_proto(proto: String) -> Self {
|
fn from_proto(proto: String) -> Self {
|
||||||
PathBuf::from_proto(proto).into()
|
from_proto_path(proto).into()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ToProto for PathBuf {
|
impl ToProto for PathBuf {
|
||||||
#[cfg(target_os = "windows")]
|
|
||||||
fn to_proto(self) -> String {
|
fn to_proto(self) -> String {
|
||||||
self.components()
|
to_proto_path(&self)
|
||||||
.map(|comp| comp.as_os_str().to_string_lossy().to_string())
|
|
||||||
.collect::<Vec<_>>()
|
|
||||||
.join("/")
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(not(target_os = "windows"))]
|
|
||||||
fn to_proto(self) -> String {
|
|
||||||
self.to_string_lossy().to_string()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ToProto for &Path {
|
impl ToProto for &Path {
|
||||||
#[cfg(target_os = "windows")]
|
|
||||||
fn to_proto(self) -> String {
|
fn to_proto(self) -> String {
|
||||||
self.components()
|
to_proto_path(self)
|
||||||
.map(|comp| comp.as_os_str().to_string_lossy().to_string())
|
|
||||||
.collect::<Vec<_>>()
|
|
||||||
.join("/")
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(not(target_os = "windows"))]
|
|
||||||
fn to_proto(self) -> String {
|
|
||||||
self.to_string_lossy().to_string()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -214,3 +209,103 @@ impl<T: RequestMessage> TypedEnvelope<T> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use typed_path::{UnixPath, UnixPathBuf, WindowsPath, WindowsPathBuf};
|
||||||
|
|
||||||
|
fn windows_path_from_proto(proto: String) -> WindowsPathBuf {
|
||||||
|
let proto = proto.replace('/', "\\");
|
||||||
|
WindowsPathBuf::from(proto)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn unix_path_from_proto(proto: String) -> UnixPathBuf {
|
||||||
|
UnixPathBuf::from(proto)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn windows_path_to_proto(path: &WindowsPath) -> String {
|
||||||
|
path.to_string_lossy().replace('\\', "/")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn unix_path_to_proto(path: &UnixPath) -> String {
|
||||||
|
path.to_string_lossy().to_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_path_proto_interop() {
|
||||||
|
const WINDOWS_PATHS: &[&str] = &[
|
||||||
|
"C:\\Users\\User\\Documents\\file.txt",
|
||||||
|
"C:/Program Files/App/app.exe",
|
||||||
|
"projects\\zed\\crates\\proto\\src\\typed_envelope.rs",
|
||||||
|
"projects/my project/src/main.rs",
|
||||||
|
];
|
||||||
|
const UNIX_PATHS: &[&str] = &[
|
||||||
|
"/home/user/documents/file.txt",
|
||||||
|
"/usr/local/bin/my app/app",
|
||||||
|
"projects/zed/crates/proto/src/typed_envelope.rs",
|
||||||
|
"projects/my project/src/main.rs",
|
||||||
|
];
|
||||||
|
|
||||||
|
// Windows path to proto and back
|
||||||
|
for &windows_path_str in WINDOWS_PATHS {
|
||||||
|
let windows_path = WindowsPathBuf::from(windows_path_str);
|
||||||
|
let proto = windows_path_to_proto(&windows_path);
|
||||||
|
let recovered_path = windows_path_from_proto(proto);
|
||||||
|
assert_eq!(windows_path, recovered_path);
|
||||||
|
assert_eq!(
|
||||||
|
recovered_path.to_string_lossy(),
|
||||||
|
windows_path_str.replace('/', "\\")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// Unix path to proto and back
|
||||||
|
for &unix_path_str in UNIX_PATHS {
|
||||||
|
let unix_path = UnixPathBuf::from(unix_path_str);
|
||||||
|
let proto = unix_path_to_proto(&unix_path);
|
||||||
|
let recovered_path = unix_path_from_proto(proto);
|
||||||
|
assert_eq!(unix_path, recovered_path);
|
||||||
|
assert_eq!(recovered_path.to_string_lossy(), unix_path_str);
|
||||||
|
}
|
||||||
|
// Windows host, Unix client, host sends Windows path to client
|
||||||
|
for &windows_path_str in WINDOWS_PATHS {
|
||||||
|
let windows_host_path = WindowsPathBuf::from(windows_path_str);
|
||||||
|
let proto = windows_path_to_proto(&windows_host_path);
|
||||||
|
let unix_client_received_path = unix_path_from_proto(proto);
|
||||||
|
let proto = unix_path_to_proto(&unix_client_received_path);
|
||||||
|
let windows_host_recovered_path = windows_path_from_proto(proto);
|
||||||
|
assert_eq!(windows_host_path, windows_host_recovered_path);
|
||||||
|
assert_eq!(
|
||||||
|
windows_host_recovered_path.to_string_lossy(),
|
||||||
|
windows_path_str.replace('/', "\\")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// Unix host, Windows client, host sends Unix path to client
|
||||||
|
for &unix_path_str in UNIX_PATHS {
|
||||||
|
let unix_host_path = UnixPathBuf::from(unix_path_str);
|
||||||
|
let proto = unix_path_to_proto(&unix_host_path);
|
||||||
|
let windows_client_received_path = windows_path_from_proto(proto);
|
||||||
|
let proto = windows_path_to_proto(&windows_client_received_path);
|
||||||
|
let unix_host_recovered_path = unix_path_from_proto(proto);
|
||||||
|
assert_eq!(unix_host_path, unix_host_recovered_path);
|
||||||
|
assert_eq!(unix_host_recovered_path.to_string_lossy(), unix_path_str);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// todo(zjk)
|
||||||
|
#[test]
|
||||||
|
fn test_unsolved_case() {
|
||||||
|
// Unix host, Windows client
|
||||||
|
// The Windows client receives a Unix path with backslashes in it, then
|
||||||
|
// sends it back to the host.
|
||||||
|
// This currently fails.
|
||||||
|
let unix_path = UnixPathBuf::from("/home/user/projects/my\\project/src/main.rs");
|
||||||
|
let proto = unix_path_to_proto(&unix_path);
|
||||||
|
let windows_client_received_path = windows_path_from_proto(proto);
|
||||||
|
let proto = windows_path_to_proto(&windows_client_received_path);
|
||||||
|
let unix_host_recovered_path = unix_path_from_proto(proto);
|
||||||
|
assert_ne!(unix_path, unix_host_recovered_path);
|
||||||
|
assert_eq!(
|
||||||
|
unix_host_recovered_path.to_string_lossy(),
|
||||||
|
"/home/user/projects/my/project/src/main.rs"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -28,9 +28,8 @@ use paths::user_ssh_config_file;
|
||||||
use picker::Picker;
|
use picker::Picker;
|
||||||
use project::Fs;
|
use project::Fs;
|
||||||
use project::Project;
|
use project::Project;
|
||||||
use remote::SshConnectionOptions;
|
|
||||||
use remote::SshRemoteClient;
|
|
||||||
use remote::ssh_session::ConnectionIdentifier;
|
use remote::ssh_session::ConnectionIdentifier;
|
||||||
|
use remote::{SshConnectionOptions, SshRemoteClient};
|
||||||
use settings::Settings;
|
use settings::Settings;
|
||||||
use settings::SettingsStore;
|
use settings::SettingsStore;
|
||||||
use settings::update_settings_file;
|
use settings::update_settings_file;
|
||||||
|
@ -42,7 +41,10 @@ use ui::{
|
||||||
IconButtonShape, List, ListItem, ListSeparator, Modal, ModalHeader, Scrollbar, ScrollbarState,
|
IconButtonShape, List, ListItem, ListSeparator, Modal, ModalHeader, Scrollbar, ScrollbarState,
|
||||||
Section, Tooltip, prelude::*,
|
Section, Tooltip, prelude::*,
|
||||||
};
|
};
|
||||||
use util::ResultExt;
|
use util::{
|
||||||
|
ResultExt,
|
||||||
|
paths::{PathStyle, RemotePathBuf},
|
||||||
|
};
|
||||||
use workspace::OpenOptions;
|
use workspace::OpenOptions;
|
||||||
use workspace::Toast;
|
use workspace::Toast;
|
||||||
use workspace::notifications::NotificationId;
|
use workspace::notifications::NotificationId;
|
||||||
|
@ -142,20 +144,21 @@ impl ProjectPicker {
|
||||||
ix: usize,
|
ix: usize,
|
||||||
connection: SshConnectionOptions,
|
connection: SshConnectionOptions,
|
||||||
project: Entity<Project>,
|
project: Entity<Project>,
|
||||||
home_dir: PathBuf,
|
home_dir: RemotePathBuf,
|
||||||
|
path_style: PathStyle,
|
||||||
workspace: WeakEntity<Workspace>,
|
workspace: WeakEntity<Workspace>,
|
||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut Context<RemoteServerProjects>,
|
cx: &mut Context<RemoteServerProjects>,
|
||||||
) -> Entity<Self> {
|
) -> Entity<Self> {
|
||||||
let (tx, rx) = oneshot::channel();
|
let (tx, rx) = oneshot::channel();
|
||||||
let lister = project::DirectoryLister::Project(project.clone());
|
let lister = project::DirectoryLister::Project(project.clone());
|
||||||
let delegate = file_finder::OpenPathDelegate::new(tx, lister, false);
|
let delegate = file_finder::OpenPathDelegate::new(tx, lister, false, path_style);
|
||||||
|
|
||||||
let picker = cx.new(|cx| {
|
let picker = cx.new(|cx| {
|
||||||
let picker = Picker::uniform_list(delegate, window, cx)
|
let picker = Picker::uniform_list(delegate, window, cx)
|
||||||
.width(rems(34.))
|
.width(rems(34.))
|
||||||
.modal(false);
|
.modal(false);
|
||||||
picker.set_query(home_dir.to_string_lossy().to_string(), window, cx);
|
picker.set_query(home_dir.to_string(), window, cx);
|
||||||
picker
|
picker
|
||||||
});
|
});
|
||||||
let connection_string = connection.connection_string().into();
|
let connection_string = connection.connection_string().into();
|
||||||
|
@ -422,7 +425,8 @@ impl RemoteServerProjects {
|
||||||
ix: usize,
|
ix: usize,
|
||||||
connection_options: remote::SshConnectionOptions,
|
connection_options: remote::SshConnectionOptions,
|
||||||
project: Entity<Project>,
|
project: Entity<Project>,
|
||||||
home_dir: PathBuf,
|
home_dir: RemotePathBuf,
|
||||||
|
path_style: PathStyle,
|
||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
workspace: WeakEntity<Workspace>,
|
workspace: WeakEntity<Workspace>,
|
||||||
|
@ -435,6 +439,7 @@ impl RemoteServerProjects {
|
||||||
connection_options,
|
connection_options,
|
||||||
project,
|
project,
|
||||||
home_dir,
|
home_dir,
|
||||||
|
path_style,
|
||||||
workspace,
|
workspace,
|
||||||
window,
|
window,
|
||||||
cx,
|
cx,
|
||||||
|
@ -589,7 +594,9 @@ impl RemoteServerProjects {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
let project = cx.update(|_, cx| {
|
let (path_style, project) = cx.update(|_, cx| {
|
||||||
|
(
|
||||||
|
session.read(cx).path_style(),
|
||||||
project::Project::ssh(
|
project::Project::ssh(
|
||||||
session,
|
session,
|
||||||
app_state.client.clone(),
|
app_state.client.clone(),
|
||||||
|
@ -598,6 +605,7 @@ impl RemoteServerProjects {
|
||||||
app_state.languages.clone(),
|
app_state.languages.clone(),
|
||||||
app_state.fs.clone(),
|
app_state.fs.clone(),
|
||||||
cx,
|
cx,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
|
@ -605,7 +613,13 @@ impl RemoteServerProjects {
|
||||||
.read_with(cx, |project, cx| project.resolve_abs_path("~", cx))?
|
.read_with(cx, |project, cx| project.resolve_abs_path("~", cx))?
|
||||||
.await
|
.await
|
||||||
.and_then(|path| path.into_abs_path())
|
.and_then(|path| path.into_abs_path())
|
||||||
.unwrap_or(PathBuf::from("/"));
|
.map(|path| RemotePathBuf::new(path, path_style))
|
||||||
|
.unwrap_or_else(|| match path_style {
|
||||||
|
PathStyle::Posix => RemotePathBuf::from_str("/", PathStyle::Posix),
|
||||||
|
PathStyle::Windows => {
|
||||||
|
RemotePathBuf::from_str("C:\\", PathStyle::Windows)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
workspace
|
workspace
|
||||||
.update_in(cx, |workspace, window, cx| {
|
.update_in(cx, |workspace, window, cx| {
|
||||||
|
@ -617,6 +631,7 @@ impl RemoteServerProjects {
|
||||||
connection_options,
|
connection_options,
|
||||||
project,
|
project,
|
||||||
home_dir,
|
home_dir,
|
||||||
|
path_style,
|
||||||
window,
|
window,
|
||||||
cx,
|
cx,
|
||||||
weak,
|
weak,
|
||||||
|
|
|
@ -49,7 +49,10 @@ use std::{
|
||||||
time::{Duration, Instant},
|
time::{Duration, Instant},
|
||||||
};
|
};
|
||||||
use tempfile::TempDir;
|
use tempfile::TempDir;
|
||||||
use util::ResultExt;
|
use util::{
|
||||||
|
ResultExt,
|
||||||
|
paths::{PathStyle, RemotePathBuf},
|
||||||
|
};
|
||||||
|
|
||||||
#[derive(
|
#[derive(
|
||||||
Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, serde::Serialize, serde::Deserialize,
|
Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, serde::Serialize, serde::Deserialize,
|
||||||
|
@ -59,7 +62,10 @@ pub struct SshProjectId(pub u64);
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct SshSocket {
|
pub struct SshSocket {
|
||||||
connection_options: SshConnectionOptions,
|
connection_options: SshConnectionOptions,
|
||||||
|
#[cfg(not(target_os = "windows"))]
|
||||||
socket_path: PathBuf,
|
socket_path: PathBuf,
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
envs: HashMap<String, String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Hash, Deserialize, Serialize, JsonSchema)]
|
#[derive(Debug, Clone, PartialEq, Eq, Hash, Deserialize, Serialize, JsonSchema)]
|
||||||
|
@ -85,6 +91,11 @@ pub struct SshConnectionOptions {
|
||||||
pub upload_binary_over_ssh: bool,
|
pub upload_binary_over_ssh: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct SshArgs {
|
||||||
|
pub arguments: Vec<String>,
|
||||||
|
pub envs: Option<HashMap<String, String>>,
|
||||||
|
}
|
||||||
|
|
||||||
#[macro_export]
|
#[macro_export]
|
||||||
macro_rules! shell_script {
|
macro_rules! shell_script {
|
||||||
($fmt:expr, $($name:ident = $arg:expr),+ $(,)?) => {{
|
($fmt:expr, $($name:ident = $arg:expr),+ $(,)?) => {{
|
||||||
|
@ -338,6 +349,28 @@ pub trait SshClientDelegate: Send + Sync {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SshSocket {
|
impl SshSocket {
|
||||||
|
#[cfg(not(target_os = "windows"))]
|
||||||
|
fn new(options: SshConnectionOptions, socket_path: PathBuf) -> Result<Self> {
|
||||||
|
Ok(Self {
|
||||||
|
connection_options: options,
|
||||||
|
socket_path,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
fn new(options: SshConnectionOptions, temp_dir: &TempDir, secret: String) -> Result<Self> {
|
||||||
|
let askpass_script = temp_dir.path().join("askpass.bat");
|
||||||
|
std::fs::write(&askpass_script, "@ECHO OFF\necho %ZED_SSH_ASKPASS%")?;
|
||||||
|
let mut envs = HashMap::default();
|
||||||
|
envs.insert("SSH_ASKPASS_REQUIRE".into(), "force".into());
|
||||||
|
envs.insert("SSH_ASKPASS".into(), askpass_script.display().to_string());
|
||||||
|
envs.insert("ZED_SSH_ASKPASS".into(), secret);
|
||||||
|
Ok(Self {
|
||||||
|
connection_options: options,
|
||||||
|
envs,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// :WARNING: ssh unquotes arguments when executing on the remote :WARNING:
|
// :WARNING: ssh unquotes arguments when executing on the remote :WARNING:
|
||||||
// e.g. $ ssh host sh -c 'ls -l' is equivalent to $ ssh host sh -c ls -l
|
// e.g. $ ssh host sh -c 'ls -l' is equivalent to $ ssh host sh -c ls -l
|
||||||
// and passes -l as an argument to sh, not to ls.
|
// and passes -l as an argument to sh, not to ls.
|
||||||
|
@ -375,6 +408,7 @@ impl SshSocket {
|
||||||
Ok(String::from_utf8_lossy(&output.stdout).to_string())
|
Ok(String::from_utf8_lossy(&output.stdout).to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(not(target_os = "windows"))]
|
||||||
fn ssh_options<'a>(&self, command: &'a mut process::Command) -> &'a mut process::Command {
|
fn ssh_options<'a>(&self, command: &'a mut process::Command) -> &'a mut process::Command {
|
||||||
command
|
command
|
||||||
.stdin(Stdio::piped())
|
.stdin(Stdio::piped())
|
||||||
|
@ -384,14 +418,68 @@ impl SshSocket {
|
||||||
.arg(format!("ControlPath={}", self.socket_path.display()))
|
.arg(format!("ControlPath={}", self.socket_path.display()))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn ssh_args(&self) -> Vec<String> {
|
#[cfg(target_os = "windows")]
|
||||||
vec![
|
fn ssh_options<'a>(&self, command: &'a mut process::Command) -> &'a mut process::Command {
|
||||||
|
command
|
||||||
|
.stdin(Stdio::piped())
|
||||||
|
.stdout(Stdio::piped())
|
||||||
|
.stderr(Stdio::piped())
|
||||||
|
.envs(self.envs.clone())
|
||||||
|
}
|
||||||
|
|
||||||
|
// On Windows, we need to use `SSH_ASKPASS` to provide the password to ssh.
|
||||||
|
// On Linux, we use the `ControlPath` option to create a socket file that ssh can use to
|
||||||
|
#[cfg(not(target_os = "windows"))]
|
||||||
|
fn ssh_args(&self) -> SshArgs {
|
||||||
|
SshArgs {
|
||||||
|
arguments: vec![
|
||||||
"-o".to_string(),
|
"-o".to_string(),
|
||||||
"ControlMaster=no".to_string(),
|
"ControlMaster=no".to_string(),
|
||||||
"-o".to_string(),
|
"-o".to_string(),
|
||||||
format!("ControlPath={}", self.socket_path.display()),
|
format!("ControlPath={}", self.socket_path.display()),
|
||||||
self.connection_options.ssh_url(),
|
self.connection_options.ssh_url(),
|
||||||
]
|
],
|
||||||
|
envs: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
fn ssh_args(&self) -> SshArgs {
|
||||||
|
SshArgs {
|
||||||
|
arguments: vec![self.connection_options.ssh_url()],
|
||||||
|
envs: Some(self.envs.clone()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn platform(&self) -> Result<SshPlatform> {
|
||||||
|
let uname = self.run_command("sh", &["-c", "uname -sm"]).await?;
|
||||||
|
let Some((os, arch)) = uname.split_once(" ") else {
|
||||||
|
anyhow::bail!("unknown uname: {uname:?}")
|
||||||
|
};
|
||||||
|
|
||||||
|
let os = match os.trim() {
|
||||||
|
"Darwin" => "macos",
|
||||||
|
"Linux" => "linux",
|
||||||
|
_ => anyhow::bail!(
|
||||||
|
"Prebuilt remote servers are not yet available for {os:?}. See https://zed.dev/docs/remote-development"
|
||||||
|
),
|
||||||
|
};
|
||||||
|
// exclude armv5,6,7 as they are 32-bit.
|
||||||
|
let arch = if arch.starts_with("armv8")
|
||||||
|
|| arch.starts_with("armv9")
|
||||||
|
|| arch.starts_with("arm64")
|
||||||
|
|| arch.starts_with("aarch64")
|
||||||
|
{
|
||||||
|
"aarch64"
|
||||||
|
} else if arch.starts_with("x86") {
|
||||||
|
"x86_64"
|
||||||
|
} else {
|
||||||
|
anyhow::bail!(
|
||||||
|
"Prebuilt remote servers are not yet available for {arch:?}. See https://zed.dev/docs/remote-development"
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(SshPlatform { os, arch })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -560,6 +648,7 @@ pub struct SshRemoteClient {
|
||||||
client: Arc<ChannelClient>,
|
client: Arc<ChannelClient>,
|
||||||
unique_identifier: String,
|
unique_identifier: String,
|
||||||
connection_options: SshConnectionOptions,
|
connection_options: SshConnectionOptions,
|
||||||
|
path_style: PathStyle,
|
||||||
state: Arc<Mutex<Option<State>>>,
|
state: Arc<Mutex<Option<State>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -620,22 +709,25 @@ impl SshRemoteClient {
|
||||||
|
|
||||||
let client =
|
let client =
|
||||||
cx.update(|cx| ChannelClient::new(incoming_rx, outgoing_tx, cx, "client"))?;
|
cx.update(|cx| ChannelClient::new(incoming_rx, outgoing_tx, cx, "client"))?;
|
||||||
let this = cx.new(|_| Self {
|
|
||||||
client: client.clone(),
|
|
||||||
unique_identifier: unique_identifier.clone(),
|
|
||||||
connection_options: connection_options.clone(),
|
|
||||||
state: Arc::new(Mutex::new(Some(State::Connecting))),
|
|
||||||
})?;
|
|
||||||
|
|
||||||
let ssh_connection = cx
|
let ssh_connection = cx
|
||||||
.update(|cx| {
|
.update(|cx| {
|
||||||
cx.update_default_global(|pool: &mut ConnectionPool, cx| {
|
cx.update_default_global(|pool: &mut ConnectionPool, cx| {
|
||||||
pool.connect(connection_options, &delegate, cx)
|
pool.connect(connection_options.clone(), &delegate, cx)
|
||||||
})
|
})
|
||||||
})?
|
})?
|
||||||
.await
|
.await
|
||||||
.map_err(|e| e.cloned())?;
|
.map_err(|e| e.cloned())?;
|
||||||
|
|
||||||
|
let path_style = ssh_connection.path_style();
|
||||||
|
let this = cx.new(|_| Self {
|
||||||
|
client: client.clone(),
|
||||||
|
unique_identifier: unique_identifier.clone(),
|
||||||
|
connection_options,
|
||||||
|
path_style,
|
||||||
|
state: Arc::new(Mutex::new(Some(State::Connecting))),
|
||||||
|
})?;
|
||||||
|
|
||||||
let io_task = ssh_connection.start_proxy(
|
let io_task = ssh_connection.start_proxy(
|
||||||
unique_identifier,
|
unique_identifier,
|
||||||
false,
|
false,
|
||||||
|
@ -1065,18 +1157,18 @@ impl SshRemoteClient {
|
||||||
self.client.subscribe_to_entity(remote_id, entity);
|
self.client.subscribe_to_entity(remote_id, entity);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn ssh_args(&self) -> Option<Vec<String>> {
|
pub fn ssh_info(&self) -> Option<(SshArgs, PathStyle)> {
|
||||||
self.state
|
self.state
|
||||||
.lock()
|
.lock()
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.and_then(|state| state.ssh_connection())
|
.and_then(|state| state.ssh_connection())
|
||||||
.map(|ssh_connection| ssh_connection.ssh_args())
|
.map(|ssh_connection| (ssh_connection.ssh_args(), ssh_connection.path_style()))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn upload_directory(
|
pub fn upload_directory(
|
||||||
&self,
|
&self,
|
||||||
src_path: PathBuf,
|
src_path: PathBuf,
|
||||||
dest_path: PathBuf,
|
dest_path: RemotePathBuf,
|
||||||
cx: &App,
|
cx: &App,
|
||||||
) -> Task<Result<()>> {
|
) -> Task<Result<()>> {
|
||||||
let state = self.state.lock();
|
let state = self.state.lock();
|
||||||
|
@ -1110,6 +1202,10 @@ impl SshRemoteClient {
|
||||||
self.connection_state() == ConnectionState::Disconnected
|
self.connection_state() == ConnectionState::Disconnected
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn path_style(&self) -> PathStyle {
|
||||||
|
self.path_style
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(any(test, feature = "test-support"))]
|
#[cfg(any(test, feature = "test-support"))]
|
||||||
pub fn simulate_disconnect(&self, client_cx: &mut App) -> Task<()> {
|
pub fn simulate_disconnect(&self, client_cx: &mut App) -> Task<()> {
|
||||||
let opts = self.connection_options();
|
let opts = self.connection_options();
|
||||||
|
@ -1288,12 +1384,19 @@ trait RemoteConnection: Send + Sync {
|
||||||
delegate: Arc<dyn SshClientDelegate>,
|
delegate: Arc<dyn SshClientDelegate>,
|
||||||
cx: &mut AsyncApp,
|
cx: &mut AsyncApp,
|
||||||
) -> Task<Result<i32>>;
|
) -> Task<Result<i32>>;
|
||||||
fn upload_directory(&self, src_path: PathBuf, dest_path: PathBuf, cx: &App)
|
fn upload_directory(
|
||||||
-> Task<Result<()>>;
|
&self,
|
||||||
|
src_path: PathBuf,
|
||||||
|
dest_path: RemotePathBuf,
|
||||||
|
cx: &App,
|
||||||
|
) -> Task<Result<()>>;
|
||||||
async fn kill(&self) -> Result<()>;
|
async fn kill(&self) -> Result<()>;
|
||||||
fn has_been_killed(&self) -> bool;
|
fn has_been_killed(&self) -> bool;
|
||||||
fn ssh_args(&self) -> Vec<String>;
|
/// On Windows, we need to use `SSH_ASKPASS` to provide the password to ssh.
|
||||||
|
/// On Linux, we use the `ControlPath` option to create a socket file that ssh can use to
|
||||||
|
fn ssh_args(&self) -> SshArgs;
|
||||||
fn connection_options(&self) -> SshConnectionOptions;
|
fn connection_options(&self) -> SshConnectionOptions;
|
||||||
|
fn path_style(&self) -> PathStyle;
|
||||||
|
|
||||||
#[cfg(any(test, feature = "test-support"))]
|
#[cfg(any(test, feature = "test-support"))]
|
||||||
fn simulate_disconnect(&self, _: &AsyncApp) {}
|
fn simulate_disconnect(&self, _: &AsyncApp) {}
|
||||||
|
@ -1302,7 +1405,9 @@ trait RemoteConnection: Send + Sync {
|
||||||
struct SshRemoteConnection {
|
struct SshRemoteConnection {
|
||||||
socket: SshSocket,
|
socket: SshSocket,
|
||||||
master_process: Mutex<Option<Child>>,
|
master_process: Mutex<Option<Child>>,
|
||||||
remote_binary_path: Option<PathBuf>,
|
remote_binary_path: Option<RemotePathBuf>,
|
||||||
|
ssh_platform: SshPlatform,
|
||||||
|
ssh_path_style: PathStyle,
|
||||||
_temp_dir: TempDir,
|
_temp_dir: TempDir,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1321,7 +1426,7 @@ impl RemoteConnection for SshRemoteConnection {
|
||||||
self.master_process.lock().is_none()
|
self.master_process.lock().is_none()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn ssh_args(&self) -> Vec<String> {
|
fn ssh_args(&self) -> SshArgs {
|
||||||
self.socket.ssh_args()
|
self.socket.ssh_args()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1332,7 +1437,7 @@ impl RemoteConnection for SshRemoteConnection {
|
||||||
fn upload_directory(
|
fn upload_directory(
|
||||||
&self,
|
&self,
|
||||||
src_path: PathBuf,
|
src_path: PathBuf,
|
||||||
dest_path: PathBuf,
|
dest_path: RemotePathBuf,
|
||||||
cx: &App,
|
cx: &App,
|
||||||
) -> Task<Result<()>> {
|
) -> Task<Result<()>> {
|
||||||
let mut command = util::command::new_smol_command("scp");
|
let mut command = util::command::new_smol_command("scp");
|
||||||
|
@ -1352,7 +1457,7 @@ impl RemoteConnection for SshRemoteConnection {
|
||||||
.arg(format!(
|
.arg(format!(
|
||||||
"{}:{}",
|
"{}:{}",
|
||||||
self.socket.connection_options.scp_url(),
|
self.socket.connection_options.scp_url(),
|
||||||
dest_path.display()
|
dest_path.to_string()
|
||||||
))
|
))
|
||||||
.output();
|
.output();
|
||||||
|
|
||||||
|
@ -1363,7 +1468,7 @@ impl RemoteConnection for SshRemoteConnection {
|
||||||
output.status.success(),
|
output.status.success(),
|
||||||
"failed to upload directory {} -> {}: {}",
|
"failed to upload directory {} -> {}: {}",
|
||||||
src_path.display(),
|
src_path.display(),
|
||||||
dest_path.display(),
|
dest_path.to_string(),
|
||||||
String::from_utf8_lossy(&output.stderr)
|
String::from_utf8_lossy(&output.stderr)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -1389,7 +1494,7 @@ impl RemoteConnection for SshRemoteConnection {
|
||||||
|
|
||||||
let mut start_proxy_command = shell_script!(
|
let mut start_proxy_command = shell_script!(
|
||||||
"exec {binary_path} proxy --identifier {identifier}",
|
"exec {binary_path} proxy --identifier {identifier}",
|
||||||
binary_path = &remote_binary_path.to_string_lossy(),
|
binary_path = &remote_binary_path.to_string(),
|
||||||
identifier = &unique_identifier,
|
identifier = &unique_identifier,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -1432,19 +1537,13 @@ impl RemoteConnection for SshRemoteConnection {
|
||||||
&cx,
|
&cx,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn path_style(&self) -> PathStyle {
|
||||||
|
self.ssh_path_style
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SshRemoteConnection {
|
impl SshRemoteConnection {
|
||||||
#[cfg(not(unix))]
|
|
||||||
async fn new(
|
|
||||||
_connection_options: SshConnectionOptions,
|
|
||||||
_delegate: Arc<dyn SshClientDelegate>,
|
|
||||||
_cx: &mut AsyncApp,
|
|
||||||
) -> Result<Self> {
|
|
||||||
anyhow::bail!("ssh is not supported on this platform");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(unix)]
|
|
||||||
async fn new(
|
async fn new(
|
||||||
connection_options: SshConnectionOptions,
|
connection_options: SshConnectionOptions,
|
||||||
delegate: Arc<dyn SshClientDelegate>,
|
delegate: Arc<dyn SshClientDelegate>,
|
||||||
|
@ -1470,27 +1569,38 @@ impl SshRemoteConnection {
|
||||||
// Start the master SSH process, which does not do anything except for establish
|
// Start the master SSH process, which does not do anything except for establish
|
||||||
// the connection and keep it open, allowing other ssh commands to reuse it
|
// the connection and keep it open, allowing other ssh commands to reuse it
|
||||||
// via a control socket.
|
// via a control socket.
|
||||||
|
#[cfg(not(target_os = "windows"))]
|
||||||
let socket_path = temp_dir.path().join("ssh.sock");
|
let socket_path = temp_dir.path().join("ssh.sock");
|
||||||
|
|
||||||
let mut master_process = process::Command::new("ssh")
|
let mut master_process = {
|
||||||
.stdin(Stdio::null())
|
#[cfg(not(target_os = "windows"))]
|
||||||
.stdout(Stdio::piped())
|
let args = [
|
||||||
.stderr(Stdio::piped())
|
|
||||||
.env("SSH_ASKPASS_REQUIRE", "force")
|
|
||||||
.env("SSH_ASKPASS", &askpass.script_path())
|
|
||||||
.args(connection_options.additional_args())
|
|
||||||
.args([
|
|
||||||
"-N",
|
"-N",
|
||||||
"-o",
|
"-o",
|
||||||
"ControlPersist=no",
|
"ControlPersist=no",
|
||||||
"-o",
|
"-o",
|
||||||
"ControlMaster=yes",
|
"ControlMaster=yes",
|
||||||
"-o",
|
"-o",
|
||||||
])
|
];
|
||||||
.arg(format!("ControlPath={}", socket_path.display()))
|
// On Windows, `ControlMaster` and `ControlPath` are not supported:
|
||||||
.arg(&url)
|
// https://github.com/PowerShell/Win32-OpenSSH/issues/405
|
||||||
|
// https://github.com/PowerShell/Win32-OpenSSH/wiki/Project-Scope
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
let args = ["-N"];
|
||||||
|
let mut master_process = process::Command::new("ssh");
|
||||||
|
master_process
|
||||||
.kill_on_drop(true)
|
.kill_on_drop(true)
|
||||||
.spawn()?;
|
.stdin(Stdio::null())
|
||||||
|
.stdout(Stdio::piped())
|
||||||
|
.stderr(Stdio::piped())
|
||||||
|
.env("SSH_ASKPASS_REQUIRE", "force")
|
||||||
|
.env("SSH_ASKPASS", askpass.script_path())
|
||||||
|
.args(connection_options.additional_args())
|
||||||
|
.args(args);
|
||||||
|
#[cfg(not(target_os = "windows"))]
|
||||||
|
master_process.arg(format!("ControlPath={}", socket_path.display()));
|
||||||
|
master_process.arg(&url).spawn()?
|
||||||
|
};
|
||||||
// Wait for this ssh process to close its stdout, indicating that authentication
|
// Wait for this ssh process to close its stdout, indicating that authentication
|
||||||
// has completed.
|
// has completed.
|
||||||
let mut stdout = master_process.stdout.take().unwrap();
|
let mut stdout = master_process.stdout.take().unwrap();
|
||||||
|
@ -1529,11 +1639,16 @@ impl SshRemoteConnection {
|
||||||
anyhow::bail!(error_message);
|
anyhow::bail!(error_message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(not(target_os = "windows"))]
|
||||||
|
let socket = SshSocket::new(connection_options, socket_path)?;
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
let socket = SshSocket::new(connection_options, &temp_dir, askpass.get_password())?;
|
||||||
drop(askpass);
|
drop(askpass);
|
||||||
|
|
||||||
let socket = SshSocket {
|
let ssh_platform = socket.platform().await?;
|
||||||
connection_options,
|
let ssh_path_style = match ssh_platform.os {
|
||||||
socket_path,
|
"windows" => PathStyle::Windows,
|
||||||
|
_ => PathStyle::Posix,
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut this = Self {
|
let mut this = Self {
|
||||||
|
@ -1541,6 +1656,8 @@ impl SshRemoteConnection {
|
||||||
master_process: Mutex::new(Some(master_process)),
|
master_process: Mutex::new(Some(master_process)),
|
||||||
_temp_dir: temp_dir,
|
_temp_dir: temp_dir,
|
||||||
remote_binary_path: None,
|
remote_binary_path: None,
|
||||||
|
ssh_path_style,
|
||||||
|
ssh_platform,
|
||||||
};
|
};
|
||||||
|
|
||||||
let (release_channel, version, commit) = cx.update(|cx| {
|
let (release_channel, version, commit) = cx.update(|cx| {
|
||||||
|
@ -1558,37 +1675,6 @@ impl SshRemoteConnection {
|
||||||
Ok(this)
|
Ok(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn platform(&self) -> Result<SshPlatform> {
|
|
||||||
let uname = self.socket.run_command("sh", &["-c", "uname -sm"]).await?;
|
|
||||||
let Some((os, arch)) = uname.split_once(" ") else {
|
|
||||||
anyhow::bail!("unknown uname: {uname:?}")
|
|
||||||
};
|
|
||||||
|
|
||||||
let os = match os.trim() {
|
|
||||||
"Darwin" => "macos",
|
|
||||||
"Linux" => "linux",
|
|
||||||
_ => anyhow::bail!(
|
|
||||||
"Prebuilt remote servers are not yet available for {os:?}. See https://zed.dev/docs/remote-development"
|
|
||||||
),
|
|
||||||
};
|
|
||||||
// exclude armv5,6,7 as they are 32-bit.
|
|
||||||
let arch = if arch.starts_with("armv8")
|
|
||||||
|| arch.starts_with("armv9")
|
|
||||||
|| arch.starts_with("arm64")
|
|
||||||
|| arch.starts_with("aarch64")
|
|
||||||
{
|
|
||||||
"aarch64"
|
|
||||||
} else if arch.starts_with("x86") {
|
|
||||||
"x86_64"
|
|
||||||
} else {
|
|
||||||
anyhow::bail!(
|
|
||||||
"Prebuilt remote servers are not yet available for {arch:?}. See https://zed.dev/docs/remote-development"
|
|
||||||
)
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(SshPlatform { os, arch })
|
|
||||||
}
|
|
||||||
|
|
||||||
fn multiplex(
|
fn multiplex(
|
||||||
mut ssh_proxy_process: Child,
|
mut ssh_proxy_process: Child,
|
||||||
incoming_tx: UnboundedSender<Envelope>,
|
incoming_tx: UnboundedSender<Envelope>,
|
||||||
|
@ -1699,11 +1785,10 @@ impl SshRemoteConnection {
|
||||||
version: SemanticVersion,
|
version: SemanticVersion,
|
||||||
commit: Option<AppCommitSha>,
|
commit: Option<AppCommitSha>,
|
||||||
cx: &mut AsyncApp,
|
cx: &mut AsyncApp,
|
||||||
) -> Result<PathBuf> {
|
) -> Result<RemotePathBuf> {
|
||||||
let version_str = match release_channel {
|
let version_str = match release_channel {
|
||||||
ReleaseChannel::Nightly => {
|
ReleaseChannel::Nightly => {
|
||||||
let commit = commit.map(|s| s.full()).unwrap_or_default();
|
let commit = commit.map(|s| s.full()).unwrap_or_default();
|
||||||
|
|
||||||
format!("{}-{}", version, commit)
|
format!("{}-{}", version, commit)
|
||||||
}
|
}
|
||||||
ReleaseChannel::Dev => "build".to_string(),
|
ReleaseChannel::Dev => "build".to_string(),
|
||||||
|
@ -1714,19 +1799,23 @@ impl SshRemoteConnection {
|
||||||
release_channel.dev_name(),
|
release_channel.dev_name(),
|
||||||
version_str
|
version_str
|
||||||
);
|
);
|
||||||
let dst_path = paths::remote_server_dir_relative().join(binary_name);
|
let dst_path = RemotePathBuf::new(
|
||||||
|
paths::remote_server_dir_relative().join(binary_name),
|
||||||
|
self.ssh_path_style,
|
||||||
|
);
|
||||||
|
|
||||||
let build_remote_server = std::env::var("ZED_BUILD_REMOTE_SERVER").ok();
|
let build_remote_server = std::env::var("ZED_BUILD_REMOTE_SERVER").ok();
|
||||||
#[cfg(debug_assertions)]
|
#[cfg(debug_assertions)]
|
||||||
if let Some(build_remote_server) = build_remote_server {
|
if let Some(build_remote_server) = build_remote_server {
|
||||||
let src_path = self
|
let src_path = self.build_local(build_remote_server, delegate, cx).await?;
|
||||||
.build_local(build_remote_server, self.platform().await?, delegate, cx)
|
let tmp_path = RemotePathBuf::new(
|
||||||
.await?;
|
paths::remote_server_dir_relative().join(format!(
|
||||||
let tmp_path = paths::remote_server_dir_relative().join(format!(
|
|
||||||
"download-{}-{}",
|
"download-{}-{}",
|
||||||
std::process::id(),
|
std::process::id(),
|
||||||
src_path.file_name().unwrap().to_string_lossy()
|
src_path.file_name().unwrap().to_string_lossy()
|
||||||
));
|
)),
|
||||||
|
self.ssh_path_style,
|
||||||
|
);
|
||||||
self.upload_local_server_binary(&src_path, &tmp_path, delegate, cx)
|
self.upload_local_server_binary(&src_path, &tmp_path, delegate, cx)
|
||||||
.await?;
|
.await?;
|
||||||
self.extract_server_binary(&dst_path, &tmp_path, delegate, cx)
|
self.extract_server_binary(&dst_path, &tmp_path, delegate, cx)
|
||||||
|
@ -1736,7 +1825,7 @@ impl SshRemoteConnection {
|
||||||
|
|
||||||
if self
|
if self
|
||||||
.socket
|
.socket
|
||||||
.run_command(&dst_path.to_string_lossy(), &["version"])
|
.run_command(&dst_path.to_string(), &["version"])
|
||||||
.await
|
.await
|
||||||
.is_ok()
|
.is_ok()
|
||||||
{
|
{
|
||||||
|
@ -1754,16 +1843,17 @@ impl SshRemoteConnection {
|
||||||
_ => Ok(Some(AppVersion::global(cx))),
|
_ => Ok(Some(AppVersion::global(cx))),
|
||||||
})??;
|
})??;
|
||||||
|
|
||||||
let platform = self.platform().await?;
|
let tmp_path_gz = RemotePathBuf::new(
|
||||||
|
PathBuf::from(format!(
|
||||||
let tmp_path_gz = PathBuf::from(format!(
|
|
||||||
"{}-download-{}.gz",
|
"{}-download-{}.gz",
|
||||||
dst_path.to_string_lossy(),
|
dst_path.to_string(),
|
||||||
std::process::id()
|
std::process::id()
|
||||||
));
|
)),
|
||||||
|
self.ssh_path_style,
|
||||||
|
);
|
||||||
if !self.socket.connection_options.upload_binary_over_ssh {
|
if !self.socket.connection_options.upload_binary_over_ssh {
|
||||||
if let Some((url, body)) = delegate
|
if let Some((url, body)) = delegate
|
||||||
.get_download_params(platform, release_channel, wanted_version, cx)
|
.get_download_params(self.ssh_platform, release_channel, wanted_version, cx)
|
||||||
.await?
|
.await?
|
||||||
{
|
{
|
||||||
match self
|
match self
|
||||||
|
@ -1786,7 +1876,7 @@ impl SshRemoteConnection {
|
||||||
}
|
}
|
||||||
|
|
||||||
let src_path = delegate
|
let src_path = delegate
|
||||||
.download_server_binary_locally(platform, release_channel, wanted_version, cx)
|
.download_server_binary_locally(self.ssh_platform, release_channel, wanted_version, cx)
|
||||||
.await?;
|
.await?;
|
||||||
self.upload_local_server_binary(&src_path, &tmp_path_gz, delegate, cx)
|
self.upload_local_server_binary(&src_path, &tmp_path_gz, delegate, cx)
|
||||||
.await?;
|
.await?;
|
||||||
|
@ -1799,7 +1889,7 @@ impl SshRemoteConnection {
|
||||||
&self,
|
&self,
|
||||||
url: &str,
|
url: &str,
|
||||||
body: &str,
|
body: &str,
|
||||||
tmp_path_gz: &Path,
|
tmp_path_gz: &RemotePathBuf,
|
||||||
delegate: &Arc<dyn SshClientDelegate>,
|
delegate: &Arc<dyn SshClientDelegate>,
|
||||||
cx: &mut AsyncApp,
|
cx: &mut AsyncApp,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
|
@ -1809,10 +1899,7 @@ impl SshRemoteConnection {
|
||||||
"sh",
|
"sh",
|
||||||
&[
|
&[
|
||||||
"-c",
|
"-c",
|
||||||
&shell_script!(
|
&shell_script!("mkdir -p {parent}", parent = parent.to_string().as_ref()),
|
||||||
"mkdir -p {parent}",
|
|
||||||
parent = parent.to_string_lossy().as_ref()
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
@ -1835,7 +1922,7 @@ impl SshRemoteConnection {
|
||||||
&body,
|
&body,
|
||||||
&url,
|
&url,
|
||||||
"-o",
|
"-o",
|
||||||
&tmp_path_gz.to_string_lossy(),
|
&tmp_path_gz.to_string(),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
|
@ -1857,7 +1944,7 @@ impl SshRemoteConnection {
|
||||||
&body,
|
&body,
|
||||||
&url,
|
&url,
|
||||||
"-O",
|
"-O",
|
||||||
&tmp_path_gz.to_string_lossy(),
|
&tmp_path_gz.to_string(),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
|
@ -1880,7 +1967,7 @@ impl SshRemoteConnection {
|
||||||
async fn upload_local_server_binary(
|
async fn upload_local_server_binary(
|
||||||
&self,
|
&self,
|
||||||
src_path: &Path,
|
src_path: &Path,
|
||||||
tmp_path_gz: &Path,
|
tmp_path_gz: &RemotePathBuf,
|
||||||
delegate: &Arc<dyn SshClientDelegate>,
|
delegate: &Arc<dyn SshClientDelegate>,
|
||||||
cx: &mut AsyncApp,
|
cx: &mut AsyncApp,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
|
@ -1890,10 +1977,7 @@ impl SshRemoteConnection {
|
||||||
"sh",
|
"sh",
|
||||||
&[
|
&[
|
||||||
"-c",
|
"-c",
|
||||||
&shell_script!(
|
&shell_script!("mkdir -p {parent}", parent = parent.to_string().as_ref()),
|
||||||
"mkdir -p {parent}",
|
|
||||||
parent = parent.to_string_lossy().as_ref()
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
@ -1918,33 +2002,33 @@ impl SshRemoteConnection {
|
||||||
|
|
||||||
async fn extract_server_binary(
|
async fn extract_server_binary(
|
||||||
&self,
|
&self,
|
||||||
dst_path: &Path,
|
dst_path: &RemotePathBuf,
|
||||||
tmp_path: &Path,
|
tmp_path: &RemotePathBuf,
|
||||||
delegate: &Arc<dyn SshClientDelegate>,
|
delegate: &Arc<dyn SshClientDelegate>,
|
||||||
cx: &mut AsyncApp,
|
cx: &mut AsyncApp,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
delegate.set_status(Some("Extracting remote development server"), cx);
|
delegate.set_status(Some("Extracting remote development server"), cx);
|
||||||
let server_mode = 0o755;
|
let server_mode = 0o755;
|
||||||
|
|
||||||
let orig_tmp_path = tmp_path.to_string_lossy();
|
let orig_tmp_path = tmp_path.to_string();
|
||||||
let script = if let Some(tmp_path) = orig_tmp_path.strip_suffix(".gz") {
|
let script = if let Some(tmp_path) = orig_tmp_path.strip_suffix(".gz") {
|
||||||
shell_script!(
|
shell_script!(
|
||||||
"gunzip -f {orig_tmp_path} && chmod {server_mode} {tmp_path} && mv {tmp_path} {dst_path}",
|
"gunzip -f {orig_tmp_path} && chmod {server_mode} {tmp_path} && mv {tmp_path} {dst_path}",
|
||||||
server_mode = &format!("{:o}", server_mode),
|
server_mode = &format!("{:o}", server_mode),
|
||||||
dst_path = &dst_path.to_string_lossy()
|
dst_path = &dst_path.to_string(),
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
shell_script!(
|
shell_script!(
|
||||||
"chmod {server_mode} {orig_tmp_path} && mv {orig_tmp_path} {dst_path}",
|
"chmod {server_mode} {orig_tmp_path} && mv {orig_tmp_path} {dst_path}",
|
||||||
server_mode = &format!("{:o}", server_mode),
|
server_mode = &format!("{:o}", server_mode),
|
||||||
dst_path = &dst_path.to_string_lossy()
|
dst_path = &dst_path.to_string()
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
self.socket.run_command("sh", &["-c", &script]).await?;
|
self.socket.run_command("sh", &["-c", &script]).await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn upload_file(&self, src_path: &Path, dest_path: &Path) -> Result<()> {
|
async fn upload_file(&self, src_path: &Path, dest_path: &RemotePathBuf) -> Result<()> {
|
||||||
log::debug!("uploading file {:?} to {:?}", src_path, dest_path);
|
log::debug!("uploading file {:?} to {:?}", src_path, dest_path);
|
||||||
let mut command = util::command::new_smol_command("scp");
|
let mut command = util::command::new_smol_command("scp");
|
||||||
let output = self
|
let output = self
|
||||||
|
@ -1961,7 +2045,7 @@ impl SshRemoteConnection {
|
||||||
.arg(format!(
|
.arg(format!(
|
||||||
"{}:{}",
|
"{}:{}",
|
||||||
self.socket.connection_options.scp_url(),
|
self.socket.connection_options.scp_url(),
|
||||||
dest_path.display()
|
dest_path.to_string()
|
||||||
))
|
))
|
||||||
.output()
|
.output()
|
||||||
.await?;
|
.await?;
|
||||||
|
@ -1970,7 +2054,7 @@ impl SshRemoteConnection {
|
||||||
output.status.success(),
|
output.status.success(),
|
||||||
"failed to upload file {} -> {}: {}",
|
"failed to upload file {} -> {}: {}",
|
||||||
src_path.display(),
|
src_path.display(),
|
||||||
dest_path.display(),
|
dest_path.to_string(),
|
||||||
String::from_utf8_lossy(&output.stderr)
|
String::from_utf8_lossy(&output.stderr)
|
||||||
);
|
);
|
||||||
Ok(())
|
Ok(())
|
||||||
|
@ -1980,7 +2064,6 @@ impl SshRemoteConnection {
|
||||||
async fn build_local(
|
async fn build_local(
|
||||||
&self,
|
&self,
|
||||||
build_remote_server: String,
|
build_remote_server: String,
|
||||||
platform: SshPlatform,
|
|
||||||
delegate: &Arc<dyn SshClientDelegate>,
|
delegate: &Arc<dyn SshClientDelegate>,
|
||||||
cx: &mut AsyncApp,
|
cx: &mut AsyncApp,
|
||||||
) -> Result<PathBuf> {
|
) -> Result<PathBuf> {
|
||||||
|
@ -1999,7 +2082,9 @@ impl SshRemoteConnection {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
if platform.arch == std::env::consts::ARCH && platform.os == std::env::consts::OS {
|
if self.ssh_platform.arch == std::env::consts::ARCH
|
||||||
|
&& self.ssh_platform.os == std::env::consts::OS
|
||||||
|
{
|
||||||
delegate.set_status(Some("Building remote server binary from source"), cx);
|
delegate.set_status(Some("Building remote server binary from source"), cx);
|
||||||
log::info!("building remote server binary from source");
|
log::info!("building remote server binary from source");
|
||||||
run_cmd(Command::new("cargo").args([
|
run_cmd(Command::new("cargo").args([
|
||||||
|
@ -2025,12 +2110,15 @@ impl SshRemoteConnection {
|
||||||
let path = std::env::current_dir()?.join("target/remote_server/debug/remote_server.gz");
|
let path = std::env::current_dir()?.join("target/remote_server/debug/remote_server.gz");
|
||||||
return Ok(path);
|
return Ok(path);
|
||||||
}
|
}
|
||||||
let Some(triple) = platform.triple() else {
|
let Some(triple) = self.ssh_platform.triple() else {
|
||||||
anyhow::bail!("can't cross compile for: {:?}", platform);
|
anyhow::bail!("can't cross compile for: {:?}", self.ssh_platform);
|
||||||
};
|
};
|
||||||
smol::fs::create_dir_all("target/remote_server").await?;
|
smol::fs::create_dir_all("target/remote_server").await?;
|
||||||
|
|
||||||
if build_remote_server.contains("cross") {
|
if build_remote_server.contains("cross") {
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
use util::paths::SanitizedPath;
|
||||||
|
|
||||||
delegate.set_status(Some("Installing cross.rs for cross-compilation"), cx);
|
delegate.set_status(Some("Installing cross.rs for cross-compilation"), cx);
|
||||||
log::info!("installing cross");
|
log::info!("installing cross");
|
||||||
run_cmd(Command::new("cargo").args([
|
run_cmd(Command::new("cargo").args([
|
||||||
|
@ -2049,6 +2137,13 @@ impl SshRemoteConnection {
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
log::info!("building remote server binary from source for {}", &triple);
|
log::info!("building remote server binary from source for {}", &triple);
|
||||||
|
|
||||||
|
// On Windows, the binding needs to be set to the canonical path
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
let src =
|
||||||
|
SanitizedPath::from(smol::fs::canonicalize("./target").await?).to_glob_string();
|
||||||
|
#[cfg(not(target_os = "windows"))]
|
||||||
|
let src = "./target";
|
||||||
run_cmd(
|
run_cmd(
|
||||||
Command::new("cross")
|
Command::new("cross")
|
||||||
.args([
|
.args([
|
||||||
|
@ -2064,7 +2159,7 @@ impl SshRemoteConnection {
|
||||||
])
|
])
|
||||||
.env(
|
.env(
|
||||||
"CROSS_CONTAINER_OPTS",
|
"CROSS_CONTAINER_OPTS",
|
||||||
"--mount type=bind,src=./target,dst=/app/target",
|
format!("--mount type=bind,src={src},dst=/app/target"),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
@ -2074,10 +2169,19 @@ impl SshRemoteConnection {
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
if which.is_err() {
|
if which.is_err() {
|
||||||
|
#[cfg(not(target_os = "windows"))]
|
||||||
|
{
|
||||||
anyhow::bail!(
|
anyhow::bail!(
|
||||||
"zig not found on $PATH, install zig (see https://ziglang.org/learn/getting-started or use zigup) or pass ZED_BUILD_REMOTE_SERVER=cross to use cross"
|
"zig not found on $PATH, install zig (see https://ziglang.org/learn/getting-started or use zigup) or pass ZED_BUILD_REMOTE_SERVER=cross to use cross"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
{
|
||||||
|
anyhow::bail!(
|
||||||
|
"zig not found on $PATH, install zig (use `winget install -e --id zig.zig` or see https://ziglang.org/learn/getting-started or use zigup) or pass ZED_BUILD_REMOTE_SERVER=cross to use cross"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
delegate.set_status(Some("Adding rustup target for cross-compilation"), cx);
|
delegate.set_status(Some("Adding rustup target for cross-compilation"), cx);
|
||||||
log::info!("adding rustup target");
|
log::info!("adding rustup target");
|
||||||
|
@ -2112,12 +2216,31 @@ impl SshRemoteConnection {
|
||||||
if !build_remote_server.contains("nocompress") {
|
if !build_remote_server.contains("nocompress") {
|
||||||
delegate.set_status(Some("Compressing binary"), cx);
|
delegate.set_status(Some("Compressing binary"), cx);
|
||||||
|
|
||||||
|
#[cfg(not(target_os = "windows"))]
|
||||||
|
{
|
||||||
run_cmd(Command::new("gzip").args([
|
run_cmd(Command::new("gzip").args([
|
||||||
"-9",
|
"-9",
|
||||||
"-f",
|
"-f",
|
||||||
&format!("target/remote_server/{}/debug/remote_server", triple),
|
&format!("target/remote_server/{}/debug/remote_server", triple),
|
||||||
]))
|
]))
|
||||||
.await?;
|
.await?;
|
||||||
|
}
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
{
|
||||||
|
// On Windows, we use 7z to compress the binary
|
||||||
|
let seven_zip = which::which("7z.exe").context("7z.exe not found on $PATH, install it (e.g. with `winget install -e --id 7zip.7zip`) or, if you don't want this behaviour, set $env:ZED_BUILD_REMOTE_SERVER=\"nocompress\"")?;
|
||||||
|
let gz_path = format!("target/remote_server/{}/debug/remote_server.gz", triple);
|
||||||
|
if smol::fs::metadata(&gz_path).await.is_ok() {
|
||||||
|
smol::fs::remove_file(&gz_path).await?;
|
||||||
|
}
|
||||||
|
run_cmd(Command::new(seven_zip).args([
|
||||||
|
"a",
|
||||||
|
"-tgzip",
|
||||||
|
&gz_path,
|
||||||
|
&format!("target/remote_server/{}/debug/remote_server", triple),
|
||||||
|
]))
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
|
||||||
path = std::env::current_dir()?.join(format!(
|
path = std::env::current_dir()?.join(format!(
|
||||||
"target/remote_server/{triple}/debug/remote_server.gz"
|
"target/remote_server/{triple}/debug/remote_server.gz"
|
||||||
|
@ -2450,9 +2573,11 @@ mod fake {
|
||||||
use gpui::{App, AppContext as _, AsyncApp, SemanticVersion, Task, TestAppContext};
|
use gpui::{App, AppContext as _, AsyncApp, SemanticVersion, Task, TestAppContext};
|
||||||
use release_channel::ReleaseChannel;
|
use release_channel::ReleaseChannel;
|
||||||
use rpc::proto::Envelope;
|
use rpc::proto::Envelope;
|
||||||
|
use util::paths::{PathStyle, RemotePathBuf};
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
ChannelClient, RemoteConnection, SshClientDelegate, SshConnectionOptions, SshPlatform,
|
ChannelClient, RemoteConnection, SshArgs, SshClientDelegate, SshConnectionOptions,
|
||||||
|
SshPlatform,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub(super) struct FakeRemoteConnection {
|
pub(super) struct FakeRemoteConnection {
|
||||||
|
@ -2488,13 +2613,17 @@ mod fake {
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
fn ssh_args(&self) -> Vec<String> {
|
fn ssh_args(&self) -> SshArgs {
|
||||||
Vec::new()
|
SshArgs {
|
||||||
|
arguments: Vec::new(),
|
||||||
|
envs: None,
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn upload_directory(
|
fn upload_directory(
|
||||||
&self,
|
&self,
|
||||||
_src_path: PathBuf,
|
_src_path: PathBuf,
|
||||||
_dest_path: PathBuf,
|
_dest_path: RemotePathBuf,
|
||||||
_cx: &App,
|
_cx: &App,
|
||||||
) -> Task<Result<()>> {
|
) -> Task<Result<()>> {
|
||||||
unreachable!()
|
unreachable!()
|
||||||
|
@ -2513,7 +2642,6 @@ mod fake {
|
||||||
|
|
||||||
fn start_proxy(
|
fn start_proxy(
|
||||||
&self,
|
&self,
|
||||||
|
|
||||||
_unique_identifier: String,
|
_unique_identifier: String,
|
||||||
_reconnect: bool,
|
_reconnect: bool,
|
||||||
mut client_incoming_tx: mpsc::UnboundedSender<Envelope>,
|
mut client_incoming_tx: mpsc::UnboundedSender<Envelope>,
|
||||||
|
@ -2551,6 +2679,10 @@ mod fake {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn path_style(&self) -> PathStyle {
|
||||||
|
PathStyle::current()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) struct Delegate;
|
pub(super) struct Delegate;
|
||||||
|
|
|
@ -166,6 +166,98 @@ impl<T: AsRef<Path>> From<T> for SanitizedPath {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
pub enum PathStyle {
|
||||||
|
Posix,
|
||||||
|
Windows,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PathStyle {
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
pub const fn current() -> Self {
|
||||||
|
PathStyle::Windows
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(target_os = "windows"))]
|
||||||
|
pub const fn current() -> Self {
|
||||||
|
PathStyle::Posix
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn separator(&self) -> &str {
|
||||||
|
match self {
|
||||||
|
PathStyle::Posix => "/",
|
||||||
|
PathStyle::Windows => "\\",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct RemotePathBuf {
|
||||||
|
inner: PathBuf,
|
||||||
|
style: PathStyle,
|
||||||
|
string: String, // Cached string representation
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RemotePathBuf {
|
||||||
|
pub fn new(path: PathBuf, style: PathStyle) -> Self {
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
let string = match style {
|
||||||
|
PathStyle::Posix => path.to_string_lossy().replace('\\', "/"),
|
||||||
|
PathStyle::Windows => path.to_string_lossy().into(),
|
||||||
|
};
|
||||||
|
#[cfg(not(target_os = "windows"))]
|
||||||
|
let string = match style {
|
||||||
|
PathStyle::Posix => path.to_string_lossy().to_string(),
|
||||||
|
PathStyle::Windows => path.to_string_lossy().replace('/', "\\"),
|
||||||
|
};
|
||||||
|
Self {
|
||||||
|
inner: path,
|
||||||
|
style,
|
||||||
|
string,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn from_str(path: &str, style: PathStyle) -> Self {
|
||||||
|
let path_buf = PathBuf::from(path);
|
||||||
|
Self::new(path_buf, style)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn to_string(&self) -> String {
|
||||||
|
self.string.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
pub fn to_proto(self) -> String {
|
||||||
|
match self.path_style() {
|
||||||
|
PathStyle::Posix => self.to_string(),
|
||||||
|
PathStyle::Windows => self.inner.to_string_lossy().replace('\\', "/"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(target_os = "windows"))]
|
||||||
|
pub fn to_proto(self) -> String {
|
||||||
|
match self.path_style() {
|
||||||
|
PathStyle::Posix => self.inner.to_string_lossy().to_string(),
|
||||||
|
PathStyle::Windows => self.to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn as_path(&self) -> &Path {
|
||||||
|
&self.inner
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn path_style(&self) -> PathStyle {
|
||||||
|
self.style
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parent(&self) -> Option<RemotePathBuf> {
|
||||||
|
self.inner
|
||||||
|
.parent()
|
||||||
|
.map(|p| RemotePathBuf::new(p.to_path_buf(), self.style))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// A delimiter to use in `path_query:row_number:column_number` strings parsing.
|
/// A delimiter to use in `path_query:row_number:column_number` strings parsing.
|
||||||
pub const FILE_ROW_COLUMN_DELIMITER: char = ':';
|
pub const FILE_ROW_COLUMN_DELIMITER: char = ':';
|
||||||
|
|
||||||
|
|
|
@ -167,16 +167,6 @@ pub fn main() {
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
util::prevent_root_execution();
|
util::prevent_root_execution();
|
||||||
|
|
||||||
// Check if there is a pending installer
|
|
||||||
// If there is, run the installer and exit
|
|
||||||
// And we don't want to run the installer if we are not the first instance
|
|
||||||
#[cfg(target_os = "windows")]
|
|
||||||
let is_first_instance = crate::zed::windows_only_instance::is_first_instance();
|
|
||||||
#[cfg(target_os = "windows")]
|
|
||||||
if is_first_instance && auto_update::check_pending_installation() {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let args = Args::parse();
|
let args = Args::parse();
|
||||||
|
|
||||||
// `zed --askpass` Makes zed operate in nc/netcat mode for use with askpass
|
// `zed --askpass` Makes zed operate in nc/netcat mode for use with askpass
|
||||||
|
@ -191,6 +181,16 @@ pub fn main() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if there is a pending installer
|
||||||
|
// If there is, run the installer and exit
|
||||||
|
// And we don't want to run the installer if we are not the first instance
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
let is_first_instance = crate::zed::windows_only_instance::is_first_instance();
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
if is_first_instance && auto_update::check_pending_installation() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if args.dump_all_actions {
|
if args.dump_all_actions {
|
||||||
dump_all_gpui_actions();
|
dump_all_gpui_actions();
|
||||||
return;
|
return;
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue