This commit is contained in:
Zaenalos 2026-05-31 12:42:15 +02:00 committed by GitHub
commit 3c20723f04
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 79 additions and 71 deletions

View file

@ -20,12 +20,18 @@ use futures::{
}; };
use gpui::{AsyncApp, BackgroundExecutor, Task}; use gpui::{AsyncApp, BackgroundExecutor, Task};
use smol::fs; use smol::fs;
use util::{ResultExt as _, debug_panic, maybe, paths::PathExt, shell::ShellKind}; use util::{ResultExt as _, debug_panic, maybe, paths::PathExt};
#[cfg(not(target_os = "windows"))]
use util::shell::ShellKind;
/// Path to the program used for askpass /// Path to the program used for askpass
/// ///
/// On Unix and remote servers, this defaults to the current executable /// On Unix and remote servers, this defaults to the current executable.
/// On Windows, this is set to the CLI variant of zed /// On Windows, this must be set to the CLI variant of zed via set_askpass_program(),
/// because SSH_ASKPASS must point to a directly executable binary. The CLI binary
/// handles the ZED_ASKPASS_SOCKET env var to communicate with Zed over a Unix socket
/// without needing a wrapper script.
static ASKPASS_PROGRAM: OnceLock<std::path::PathBuf> = OnceLock::new(); static ASKPASS_PROGRAM: OnceLock<std::path::PathBuf> = OnceLock::new();
#[derive(PartialEq, Eq)] #[derive(PartialEq, Eq)]
@ -80,11 +86,8 @@ pub struct AskPassSession {
executor: BackgroundExecutor, executor: BackgroundExecutor,
} }
const ASKPASS_SCRIPT_NAME: &str = if cfg!(target_os = "windows") { #[cfg(not(target_os = "windows"))]
"askpass.ps1" const ASKPASS_SCRIPT_NAME: &str = "askpass.sh";
} else {
"askpass.sh"
};
impl AskPassSession { impl AskPassSession {
/// This will create a new AskPassSession. /// This will create a new AskPassSession.
@ -177,17 +180,34 @@ impl AskPassSession {
self.secret.lock().ok()?.clone() self.secret.lock().ok()?.clone()
} }
/// Returns the value to set as SSH_ASKPASS.
/// On Unix this is the path to the generated shell script.
/// On Windows this is the path to cli.exe directly — no script needed.
pub fn script_path(&self) -> impl AsRef<OsStr> { pub fn script_path(&self) -> impl AsRef<OsStr> {
self.askpass_task.script_path() self.askpass_task.script_path()
} }
/// Returns the socket path to set as ZED_ASKPASS_SOCKET.
///
/// On Windows, SSH_ASKPASS points directly to cli.exe. SSH passes only
/// the prompt string as argv[1] with no mechanism for extra arguments,
/// so the socket path is communicated via this environment variable instead.
/// cli.exe must check ZED_ASKPASS_SOCKET before clap parses args.
#[cfg(target_os = "windows")]
pub fn socket_path(&self) -> impl AsRef<OsStr> {
self.askpass_task.socket_path()
}
} }
pub struct PasswordProxy { pub struct PasswordProxy {
_task: Task<()>, _task: Task<()>,
#[cfg(not(target_os = "windows"))] /// On Unix: path to the generated .sh askpass script (set as SSH_ASKPASS).
/// On Windows: path to cli.exe (set as SSH_ASKPASS directly — no script needed).
askpass_script_path: std::path::PathBuf, askpass_script_path: std::path::PathBuf,
/// On Windows only: path to the Unix socket, passed as ZED_ASKPASS_SOCKET
/// so cli.exe can find it without --askpass argument parsing.
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
askpass_helper: String, askpass_socket_path: std::path::PathBuf,
} }
impl PasswordProxy { impl PasswordProxy {
@ -202,19 +222,21 @@ impl PasswordProxy {
) -> Result<Self> { ) -> Result<Self> {
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_SCRIPT_NAME);
let current_exec = let current_exec =
std::env::current_exe().context("Failed to determine current zed executable path.")?; std::env::current_exe().context("Failed to determine current zed executable path.")?;
// TODO: inferred from the use of powershell.exe in askpass_helper_script
let shell_kind = if cfg!(windows) {
ShellKind::PowerShell
} else {
ShellKind::Posix
};
let askpass_program = ASKPASS_PROGRAM.get_or_init(|| current_exec); let askpass_program = ASKPASS_PROGRAM.get_or_init(|| current_exec);
// Create an askpass script that communicates back to this process.
let askpass_script = generate_askpass_script(shell_kind, askpass_program, &askpass_socket)?; // Unix: SSH_ASKPASS = path to generated .sh script in temp dir.
// Windows: SSH_ASKPASS = path to cli.exe directly. No script is written.
#[cfg(not(target_os = "windows"))]
let askpass_script_path = temp_dir.path().join(ASKPASS_SCRIPT_NAME);
#[cfg(target_os = "windows")]
let askpass_script_path = askpass_program.to_path_buf();
#[cfg(target_os = "windows")]
let askpass_socket_path = askpass_socket.clone();
let _task = executor.spawn(async move { let _task = executor.spawn(async move {
maybe!(async move { maybe!(async move {
let listener = let listener =
@ -253,43 +275,44 @@ impl PasswordProxy {
.log_err(); .log_err();
}); });
fs::write(&askpass_script_path, askpass_script) // Unix only: write the shell script and mark it executable.
.await // On Windows cli.exe is invoked directly, so no script is needed.
.with_context(|| format!("creating askpass script at {askpass_script_path:?}"))?; #[cfg(not(target_os = "windows"))]
make_file_executable(&askpass_script_path) {
.await let askpass_script = generate_askpass_script(askpass_program, &askpass_socket)?;
.with_context(|| { fs::write(&askpass_script_path, askpass_script)
format!("marking askpass script executable at {askpass_script_path:?}") .await
})?; .with_context(|| format!("creating askpass script at {askpass_script_path:?}"))?;
// todo(shell): There might be no powershell on the system make_file_executable(&askpass_script_path)
#[cfg(target_os = "windows")] .await
let askpass_helper = format!( .with_context(|| {
"powershell.exe -ExecutionPolicy Bypass -File \"{}\"", format!("marking askpass script executable at {askpass_script_path:?}")
askpass_script_path.display() })?;
); }
Ok(Self { Ok(Self {
_task, _task,
#[cfg(not(target_os = "windows"))]
askpass_script_path, askpass_script_path,
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
askpass_helper, askpass_socket_path,
}) })
} }
pub fn script_path(&self) -> impl AsRef<OsStr> { pub fn script_path(&self) -> impl AsRef<OsStr> {
#[cfg(not(target_os = "windows"))] &self.askpass_script_path
{ }
&self.askpass_script_path
} #[cfg(target_os = "windows")]
#[cfg(target_os = "windows")] pub fn socket_path(&self) -> impl AsRef<OsStr> {
{ &self.askpass_socket_path
&self.askpass_helper
}
} }
} }
/// 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.
///
/// On Windows, the socket path is passed via ZED_ASKPASS_SOCKET rather than --askpass,
/// because SSH_ASKPASS points directly to cli.exe and SSH only passes the prompt as argv[1].
pub fn main(socket: &str) { pub fn main(socket: &str) {
use net::UnixStream; use net::UnixStream;
use std::io::{self, Read, Write}; use std::io::{self, Read, Write};
@ -340,13 +363,14 @@ pub fn set_askpass_program(path: std::path::PathBuf) {
} }
} }
#[inline] /// Generates the Unix shell askpass script.
/// Not used on Windows — cli.exe is invoked directly as SSH_ASKPASS.
#[cfg(not(target_os = "windows"))] #[cfg(not(target_os = "windows"))]
fn generate_askpass_script( fn generate_askpass_script(
shell_kind: ShellKind,
askpass_program: &std::path::Path, askpass_program: &std::path::Path,
askpass_socket: &std::path::Path, askpass_socket: &std::path::Path,
) -> Result<String> { ) -> Result<String> {
let shell_kind = ShellKind::Posix;
let askpass_program = shell_kind.prepend_command_prefix( let askpass_program = shell_kind.prepend_command_prefix(
askpass_program askpass_program
.to_str() .to_str()
@ -364,29 +388,3 @@ fn generate_askpass_script(
"{shebang}\n{print_args} | {askpass_program} --askpass={askpass_socket} 2> /dev/null \n", "{shebang}\n{print_args} | {askpass_program} --askpass={askpass_socket} 2> /dev/null \n",
)) ))
} }
#[inline]
#[cfg(target_os = "windows")]
fn generate_askpass_script(
shell_kind: ShellKind,
askpass_program: &std::path::Path,
askpass_socket: &std::path::Path,
) -> Result<String> {
let askpass_program = shell_kind.prepend_command_prefix(
askpass_program
.to_str()
.context("Askpass program is on a non-utf8 path")?,
);
let askpass_program = shell_kind
.try_quote_prefix_aware(&askpass_program)
.context("Failed to shell-escape Askpass program path")?;
let askpass_socket = askpass_socket
.try_shell_safe(shell_kind)
.context("Failed to shell-escape Askpass socket path")?;
Ok(format!(
r#"
$ErrorActionPreference = 'Stop';
($args -join [char]0) | {askpass_program} --askpass={askpass_socket} 2> $null
"#,
))
}

View file

@ -487,6 +487,14 @@ fn run() -> Result<()> {
return mac_os::spawn_channel_cli(channel, std::env::args().skip(2).collect()); return mac_os::spawn_channel_cli(channel, std::env::args().skip(2).collect());
} }
} }
// Must happen before clap — SSH invokes cli.exe directly as SSH_ASKPASS
// and passes the socket path via env var to avoid argument parsing.
if let Ok(socket) = std::env::var("ZED_ASKPASS_SOCKET") {
askpass::main(&socket);
return Ok(());
}
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

View file

@ -3495,6 +3495,8 @@ async fn run_git_command(
.env("GIT_ASKPASS", ask_pass.script_path()) .env("GIT_ASKPASS", ask_pass.script_path())
.env("SSH_ASKPASS", ask_pass.script_path()) .env("SSH_ASKPASS", ask_pass.script_path())
.env("SSH_ASKPASS_REQUIRE", "force"); .env("SSH_ASKPASS_REQUIRE", "force");
#[cfg(target_os = "windows")]
command.env("ZED_ASKPASS_SOCKET", ask_pass.socket_path());
let git_process = command.spawn()?; let git_process = command.spawn()?;
run_askpass_command(ask_pass, git_process).await run_askpass_command(ask_pass, git_process).await