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 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
///
/// On Unix and remote servers, this defaults to the current executable
/// On Windows, this is set to the CLI variant of zed
/// On Unix and remote servers, this defaults to the current executable.
/// 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();
#[derive(PartialEq, Eq)]
@ -80,11 +86,8 @@ pub struct AskPassSession {
executor: BackgroundExecutor,
}
const ASKPASS_SCRIPT_NAME: &str = if cfg!(target_os = "windows") {
"askpass.ps1"
} else {
"askpass.sh"
};
#[cfg(not(target_os = "windows"))]
const ASKPASS_SCRIPT_NAME: &str = "askpass.sh";
impl AskPassSession {
/// This will create a new AskPassSession.
@ -177,17 +180,34 @@ impl AskPassSession {
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> {
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 {
_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,
/// 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")]
askpass_helper: String,
askpass_socket_path: std::path::PathBuf,
}
impl PasswordProxy {
@ -202,19 +222,21 @@ impl PasswordProxy {
) -> Result<Self> {
let temp_dir = tempfile::Builder::new().prefix("zed-askpass").tempdir()?;
let askpass_socket = temp_dir.path().join("askpass.sock");
let askpass_script_path = temp_dir.path().join(ASKPASS_SCRIPT_NAME);
let current_exec =
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);
// 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 {
maybe!(async move {
let listener =
@ -253,43 +275,44 @@ impl PasswordProxy {
.log_err();
});
fs::write(&askpass_script_path, askpass_script)
.await
.with_context(|| format!("creating askpass script at {askpass_script_path:?}"))?;
make_file_executable(&askpass_script_path)
.await
.with_context(|| {
format!("marking askpass script executable at {askpass_script_path:?}")
})?;
// todo(shell): There might be no powershell on the system
#[cfg(target_os = "windows")]
let askpass_helper = format!(
"powershell.exe -ExecutionPolicy Bypass -File \"{}\"",
askpass_script_path.display()
);
// Unix only: write the shell script and mark it executable.
// On Windows cli.exe is invoked directly, so no script is needed.
#[cfg(not(target_os = "windows"))]
{
let askpass_script = generate_askpass_script(askpass_program, &askpass_socket)?;
fs::write(&askpass_script_path, askpass_script)
.await
.with_context(|| format!("creating askpass script at {askpass_script_path:?}"))?;
make_file_executable(&askpass_script_path)
.await
.with_context(|| {
format!("marking askpass script executable at {askpass_script_path:?}")
})?;
}
Ok(Self {
_task,
#[cfg(not(target_os = "windows"))]
askpass_script_path,
#[cfg(target_os = "windows")]
askpass_helper,
askpass_socket_path,
})
}
pub fn script_path(&self) -> impl AsRef<OsStr> {
#[cfg(not(target_os = "windows"))]
{
&self.askpass_script_path
}
#[cfg(target_os = "windows")]
{
&self.askpass_helper
}
&self.askpass_script_path
}
#[cfg(target_os = "windows")]
pub fn socket_path(&self) -> impl AsRef<OsStr> {
&self.askpass_socket_path
}
}
/// 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.
///
/// 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) {
use net::UnixStream;
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"))]
fn generate_askpass_script(
shell_kind: ShellKind,
askpass_program: &std::path::Path,
askpass_socket: &std::path::Path,
) -> Result<String> {
let shell_kind = ShellKind::Posix;
let askpass_program = shell_kind.prepend_command_prefix(
askpass_program
.to_str()
@ -364,29 +388,3 @@ fn generate_askpass_script(
"{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());
}
}
// 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();
// `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("SSH_ASKPASS", ask_pass.script_path())
.env("SSH_ASKPASS_REQUIRE", "force");
#[cfg(target_os = "windows")]
command.env("ZED_ASKPASS_SOCKET", ask_pass.socket_path());
let git_process = command.spawn()?;
run_askpass_command(ask_pass, git_process).await