logiguard fork: GPUI xdg-activation keyboard-focus serial fix
Some checks failed
Update All Top Ranking Issues / update_top_ranking_issues (push) Has been cancelled
Triage Project Sync (#84) / Sync triage project (push) Has been cancelled
release_nightly / notify_on_failure (push) Has been cancelled
release_nightly / check_style (push) Has been cancelled
release_nightly / run_tests_windows (push) Has been cancelled
release_nightly / clippy_windows (push) Has been cancelled
release_nightly / bundle_linux_aarch64 (push) Has been cancelled
release_nightly / bundle_linux_x86_64 (push) Has been cancelled
release_nightly / bundle_mac_aarch64 (push) Has been cancelled
release_nightly / bundle_mac_x86_64 (push) Has been cancelled
release_nightly / bundle_windows_aarch64 (push) Has been cancelled
release_nightly / bundle_windows_x86_64 (push) Has been cancelled
release_nightly / build_nix_linux_x86_64 (push) Has been cancelled
release_nightly / build_nix_mac_aarch64 (push) Has been cancelled
release_nightly / update_nightly_tag (push) Has been cancelled
Hotfix Review Monitor / check-hotfix-reviews (push) Has been cancelled
Stale PR Review Reminder / check-stale-prs (push) Has been cancelled
Update Weekly Top Ranking Issues / update_top_ranking_issues (push) Has been cancelled
Bump collab-staging Tag / update-collab-staging-tag (push) Has been cancelled
compliance_check / scheduled_compliance_check (push) Has been cancelled
Some checks failed
Update All Top Ranking Issues / update_top_ranking_issues (push) Has been cancelled
Triage Project Sync (#84) / Sync triage project (push) Has been cancelled
release_nightly / notify_on_failure (push) Has been cancelled
release_nightly / check_style (push) Has been cancelled
release_nightly / run_tests_windows (push) Has been cancelled
release_nightly / clippy_windows (push) Has been cancelled
release_nightly / bundle_linux_aarch64 (push) Has been cancelled
release_nightly / bundle_linux_x86_64 (push) Has been cancelled
release_nightly / bundle_mac_aarch64 (push) Has been cancelled
release_nightly / bundle_mac_x86_64 (push) Has been cancelled
release_nightly / bundle_windows_aarch64 (push) Has been cancelled
release_nightly / bundle_windows_x86_64 (push) Has been cancelled
release_nightly / build_nix_linux_x86_64 (push) Has been cancelled
release_nightly / build_nix_mac_aarch64 (push) Has been cancelled
release_nightly / update_nightly_tag (push) Has been cancelled
Hotfix Review Monitor / check-hotfix-reviews (push) Has been cancelled
Stale PR Review Reminder / check-stale-prs (push) Has been cancelled
Update Weekly Top Ranking Issues / update_top_ranking_issues (push) Has been cancelled
Bump collab-staging Tag / update-collab-staging-tag (push) Has been cancelled
compliance_check / scheduled_compliance_check (push) Has been cancelled
Single-commit orphan branch: full zed-industries/zed @ 8c74db0 source tree with a 3-file patch applied (no upstream history). Patch (crates/gpui_linux/src/linux/wayland/): - serial.rs: add SerialKind::KeyboardEnter - client.rs: store wl_keyboard.enter serial; add latest_serial_of() - window.rs: activate() uses keyboard-enter serial (Mutter focus gate) Mutter honors window activation only when the token carries the keyboard- focus serial from wl_keyboard.enter; GPUI used a stale mouse-press serial. See docs/tray-window-focus-wayland.md in logiguard. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
152
crates/http_client/src/async_body.rs
Normal file
152
crates/http_client/src/async_body.rs
Normal file
@@ -0,0 +1,152 @@
|
||||
use std::{
|
||||
io::{Cursor, Read},
|
||||
pin::Pin,
|
||||
task::Poll,
|
||||
};
|
||||
|
||||
use bytes::Bytes;
|
||||
use futures::AsyncRead;
|
||||
use http_body::{Body, Frame};
|
||||
use serde::Serialize;
|
||||
|
||||
/// Based on the implementation of AsyncBody in
|
||||
/// <https://github.com/sagebind/isahc/blob/5c533f1ef4d6bdf1fd291b5103c22110f41d0bf0/src/body/mod.rs>.
|
||||
pub struct AsyncBody(pub Inner);
|
||||
|
||||
pub enum Inner {
|
||||
/// An empty body.
|
||||
Empty,
|
||||
|
||||
/// A body stored in memory.
|
||||
Bytes(std::io::Cursor<Bytes>),
|
||||
|
||||
/// An asynchronous reader.
|
||||
AsyncReader(Pin<Box<dyn futures::AsyncRead + Send + Sync>>),
|
||||
}
|
||||
|
||||
impl AsyncBody {
|
||||
/// Create a new empty body.
|
||||
///
|
||||
/// An empty body represents the *absence* of a body, which is semantically
|
||||
/// different than the presence of a body of zero length.
|
||||
pub fn empty() -> Self {
|
||||
Self(Inner::Empty)
|
||||
}
|
||||
/// Create a streaming body that reads from the given reader.
|
||||
pub fn from_reader<R>(read: R) -> Self
|
||||
where
|
||||
R: AsyncRead + Send + Sync + 'static,
|
||||
{
|
||||
Self(Inner::AsyncReader(Box::pin(read)))
|
||||
}
|
||||
|
||||
pub fn from_bytes(bytes: Bytes) -> Self {
|
||||
Self(Inner::Bytes(Cursor::new(bytes)))
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for AsyncBody {
|
||||
fn default() -> Self {
|
||||
Self(Inner::Empty)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<()> for AsyncBody {
|
||||
fn from(_: ()) -> Self {
|
||||
Self(Inner::Empty)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Bytes> for AsyncBody {
|
||||
fn from(bytes: Bytes) -> Self {
|
||||
Self::from_bytes(bytes)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Vec<u8>> for AsyncBody {
|
||||
fn from(body: Vec<u8>) -> Self {
|
||||
Self::from_bytes(body.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<String> for AsyncBody {
|
||||
fn from(body: String) -> Self {
|
||||
Self::from_bytes(body.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&'static [u8]> for AsyncBody {
|
||||
#[inline]
|
||||
fn from(s: &'static [u8]) -> Self {
|
||||
Self::from_bytes(Bytes::from_static(s))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&'static str> for AsyncBody {
|
||||
#[inline]
|
||||
fn from(s: &'static str) -> Self {
|
||||
Self::from_bytes(Bytes::from_static(s.as_bytes()))
|
||||
}
|
||||
}
|
||||
|
||||
/// Newtype wrapper that serializes a value as JSON into an `AsyncBody`.
|
||||
pub struct Json<T: Serialize>(pub T);
|
||||
|
||||
impl<T: Serialize> From<Json<T>> for AsyncBody {
|
||||
fn from(json: Json<T>) -> Self {
|
||||
Self::from_bytes(
|
||||
serde_json::to_vec(&json.0)
|
||||
.expect("failed to serialize JSON")
|
||||
.into(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Into<Self>> From<Option<T>> for AsyncBody {
|
||||
fn from(body: Option<T>) -> Self {
|
||||
match body {
|
||||
Some(body) => body.into(),
|
||||
None => Self::empty(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl futures::AsyncRead for AsyncBody {
|
||||
fn poll_read(
|
||||
self: Pin<&mut Self>,
|
||||
cx: &mut std::task::Context<'_>,
|
||||
buf: &mut [u8],
|
||||
) -> std::task::Poll<std::io::Result<usize>> {
|
||||
// SAFETY: Standard Enum pin projection
|
||||
let inner = unsafe { &mut self.get_unchecked_mut().0 };
|
||||
match inner {
|
||||
Inner::Empty => Poll::Ready(Ok(0)),
|
||||
// Blocking call is over an in-memory buffer
|
||||
Inner::Bytes(cursor) => Poll::Ready(cursor.read(buf)),
|
||||
Inner::AsyncReader(async_reader) => {
|
||||
AsyncRead::poll_read(async_reader.as_mut(), cx, buf)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Body for AsyncBody {
|
||||
type Data = Bytes;
|
||||
type Error = std::io::Error;
|
||||
|
||||
fn poll_frame(
|
||||
mut self: Pin<&mut Self>,
|
||||
cx: &mut std::task::Context<'_>,
|
||||
) -> Poll<Option<Result<Frame<Self::Data>, Self::Error>>> {
|
||||
let mut buffer = vec![0; 8192];
|
||||
match AsyncRead::poll_read(self.as_mut(), cx, &mut buffer) {
|
||||
Poll::Ready(Ok(0)) => Poll::Ready(None),
|
||||
Poll::Ready(Ok(n)) => {
|
||||
let data = Bytes::copy_from_slice(&buffer[..n]);
|
||||
Poll::Ready(Some(Ok(Frame::data(data))))
|
||||
}
|
||||
Poll::Ready(Err(e)) => Poll::Ready(Some(Err(e))),
|
||||
Poll::Pending => Poll::Pending,
|
||||
}
|
||||
}
|
||||
}
|
||||
194
crates/http_client/src/github.rs
Normal file
194
crates/http_client/src/github.rs
Normal file
@@ -0,0 +1,194 @@
|
||||
use crate::{HttpClient, HttpRequestExt};
|
||||
use anyhow::{Context as _, Result, anyhow, bail};
|
||||
use futures::AsyncReadExt;
|
||||
use http::Request;
|
||||
use serde::Deserialize;
|
||||
use std::sync::Arc;
|
||||
use url::Url;
|
||||
|
||||
const GITHUB_API_URL: &str = "https://api.github.com";
|
||||
|
||||
pub struct GitHubLspBinaryVersion {
|
||||
pub name: String,
|
||||
pub url: String,
|
||||
pub digest: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
pub struct GithubRelease {
|
||||
pub tag_name: String,
|
||||
#[serde(rename = "prerelease")]
|
||||
pub pre_release: bool,
|
||||
pub assets: Vec<GithubReleaseAsset>,
|
||||
pub tarball_url: String,
|
||||
pub zipball_url: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
pub struct GithubReleaseAsset {
|
||||
pub name: String,
|
||||
pub browser_download_url: String,
|
||||
pub digest: Option<String>,
|
||||
}
|
||||
|
||||
pub async fn latest_github_release(
|
||||
repo_name_with_owner: &str,
|
||||
require_assets: bool,
|
||||
pre_release: bool,
|
||||
http: Arc<dyn HttpClient>,
|
||||
) -> anyhow::Result<GithubRelease> {
|
||||
let url = format!("{GITHUB_API_URL}/repos/{repo_name_with_owner}/releases");
|
||||
|
||||
let request = Request::get(&url)
|
||||
.follow_redirects(crate::RedirectPolicy::FollowAll)
|
||||
.when_some(std::env::var("GITHUB_TOKEN").ok(), |builder, token| {
|
||||
builder.header("Authorization", format!("Bearer {}", token))
|
||||
})
|
||||
.body(Default::default())?;
|
||||
|
||||
let mut response = http
|
||||
.send(request)
|
||||
.await
|
||||
.context("error fetching latest release")?;
|
||||
|
||||
let mut body = Vec::new();
|
||||
response
|
||||
.body_mut()
|
||||
.read_to_end(&mut body)
|
||||
.await
|
||||
.context("error reading latest release")?;
|
||||
|
||||
if response.status().is_client_error() {
|
||||
let text = String::from_utf8_lossy(body.as_slice());
|
||||
bail!(
|
||||
"status error {}, response: {text:?}",
|
||||
response.status().as_u16()
|
||||
);
|
||||
}
|
||||
|
||||
let releases = match serde_json::from_slice::<Vec<GithubRelease>>(body.as_slice()) {
|
||||
Ok(releases) => releases,
|
||||
|
||||
Err(err) => {
|
||||
log::error!("Error deserializing: {err:?}");
|
||||
log::error!(
|
||||
"GitHub API response text: {:?}",
|
||||
String::from_utf8_lossy(body.as_slice())
|
||||
);
|
||||
anyhow::bail!("error deserializing latest release: {err:?}");
|
||||
}
|
||||
};
|
||||
|
||||
let mut release = releases
|
||||
.into_iter()
|
||||
.filter(|release| !require_assets || !release.assets.is_empty())
|
||||
.find(|release| release.pre_release == pre_release)
|
||||
.context("finding a prerelease")?;
|
||||
release.assets.iter_mut().for_each(|asset| {
|
||||
if let Some(digest) = &mut asset.digest
|
||||
&& let Some(stripped) = digest.strip_prefix("sha256:")
|
||||
{
|
||||
*digest = stripped.to_owned();
|
||||
}
|
||||
});
|
||||
Ok(release)
|
||||
}
|
||||
|
||||
pub async fn get_release_by_tag_name(
|
||||
repo_name_with_owner: &str,
|
||||
tag: &str,
|
||||
http: Arc<dyn HttpClient>,
|
||||
) -> anyhow::Result<GithubRelease> {
|
||||
let url = format!("{GITHUB_API_URL}/repos/{repo_name_with_owner}/releases/tags/{tag}");
|
||||
|
||||
let request = Request::get(&url)
|
||||
.follow_redirects(crate::RedirectPolicy::FollowAll)
|
||||
.when_some(std::env::var("GITHUB_TOKEN").ok(), |builder, token| {
|
||||
builder.header("Authorization", format!("Bearer {}", token))
|
||||
})
|
||||
.body(Default::default())?;
|
||||
|
||||
let mut response = http
|
||||
.send(request)
|
||||
.await
|
||||
.context("error fetching latest release")?;
|
||||
|
||||
let mut body = Vec::new();
|
||||
let status = response.status();
|
||||
response
|
||||
.body_mut()
|
||||
.read_to_end(&mut body)
|
||||
.await
|
||||
.context("error reading latest release")?;
|
||||
|
||||
if status.is_client_error() {
|
||||
let text = String::from_utf8_lossy(body.as_slice());
|
||||
bail!(
|
||||
"status error {}, response: {text:?}",
|
||||
response.status().as_u16()
|
||||
);
|
||||
}
|
||||
|
||||
let release = serde_json::from_slice::<GithubRelease>(body.as_slice()).map_err(|err| {
|
||||
log::error!("Error deserializing: {err:?}");
|
||||
log::error!(
|
||||
"GitHub API response text: {:?}",
|
||||
String::from_utf8_lossy(body.as_slice())
|
||||
);
|
||||
anyhow!("error deserializing GitHub release: {err:?}")
|
||||
})?;
|
||||
|
||||
Ok(release)
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
|
||||
pub enum AssetKind {
|
||||
TarGz,
|
||||
TarBz2,
|
||||
Gz,
|
||||
Zip,
|
||||
}
|
||||
|
||||
pub fn build_asset_url(repo_name_with_owner: &str, tag: &str, kind: AssetKind) -> Result<String> {
|
||||
let mut url = Url::parse(&format!(
|
||||
"https://github.com/{repo_name_with_owner}/archive/refs/tags",
|
||||
))?;
|
||||
// We're pushing this here, because tags may contain `/` and other characters
|
||||
// that need to be escaped.
|
||||
let asset_filename = format!(
|
||||
"{tag}.{extension}",
|
||||
extension = match kind {
|
||||
AssetKind::TarGz => "tar.gz",
|
||||
AssetKind::TarBz2 => "tar.bz2",
|
||||
AssetKind::Gz => "gz",
|
||||
AssetKind::Zip => "zip",
|
||||
}
|
||||
);
|
||||
url.path_segments_mut()
|
||||
.map_err(|()| anyhow!("cannot modify url path segments"))?
|
||||
.push(&asset_filename);
|
||||
Ok(url.to_string())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::github::{AssetKind, build_asset_url};
|
||||
|
||||
#[test]
|
||||
fn test_build_asset_url() {
|
||||
let tag = "release/2.3.5";
|
||||
let repo_name_with_owner = "microsoft/vscode-eslint";
|
||||
|
||||
let tarball = build_asset_url(repo_name_with_owner, tag, AssetKind::TarGz).unwrap();
|
||||
assert_eq!(
|
||||
tarball,
|
||||
"https://github.com/microsoft/vscode-eslint/archive/refs/tags/release%2F2.3.5.tar.gz"
|
||||
);
|
||||
|
||||
let zip = build_asset_url(repo_name_with_owner, tag, AssetKind::Zip).unwrap();
|
||||
assert_eq!(
|
||||
zip,
|
||||
"https://github.com/microsoft/vscode-eslint/archive/refs/tags/release%2F2.3.5.zip"
|
||||
);
|
||||
}
|
||||
}
|
||||
292
crates/http_client/src/github_download.rs
Normal file
292
crates/http_client/src/github_download.rs
Normal file
@@ -0,0 +1,292 @@
|
||||
use std::{
|
||||
path::{Path, PathBuf},
|
||||
pin::Pin,
|
||||
task::Poll,
|
||||
};
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use async_compression::futures::bufread::{BzDecoder, GzipDecoder};
|
||||
use futures::{AsyncRead, AsyncSeek, AsyncSeekExt, AsyncWrite, io::BufReader};
|
||||
use sha2::{Digest, Sha256};
|
||||
|
||||
use crate::{HttpClient, github::AssetKind};
|
||||
|
||||
#[derive(serde::Deserialize, serde::Serialize, Debug)]
|
||||
pub struct GithubBinaryMetadata {
|
||||
pub metadata_version: u64,
|
||||
pub digest: Option<String>,
|
||||
}
|
||||
|
||||
impl GithubBinaryMetadata {
|
||||
pub async fn read_from_file(metadata_path: &Path) -> Result<GithubBinaryMetadata> {
|
||||
let metadata_content = async_fs::read_to_string(metadata_path)
|
||||
.await
|
||||
.with_context(|| format!("reading metadata file at {metadata_path:?}"))?;
|
||||
serde_json::from_str(&metadata_content)
|
||||
.with_context(|| format!("parsing metadata file at {metadata_path:?}"))
|
||||
}
|
||||
|
||||
pub async fn write_to_file(&self, metadata_path: &Path) -> Result<()> {
|
||||
let metadata_content = serde_json::to_string(self)
|
||||
.with_context(|| format!("serializing metadata for {metadata_path:?}"))?;
|
||||
async_fs::write(metadata_path, metadata_content.as_bytes())
|
||||
.await
|
||||
.with_context(|| format!("writing metadata file at {metadata_path:?}"))?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn download_server_binary(
|
||||
http_client: &dyn HttpClient,
|
||||
url: &str,
|
||||
digest: Option<&str>,
|
||||
destination_path: &Path,
|
||||
asset_kind: AssetKind,
|
||||
) -> Result<(), anyhow::Error> {
|
||||
log::info!("downloading github artifact from {url}");
|
||||
let Some(destination_parent) = destination_path.parent() else {
|
||||
anyhow::bail!("destination path has no parent: {destination_path:?}");
|
||||
};
|
||||
|
||||
let staging_path = staging_path(destination_parent, asset_kind)?;
|
||||
let mut response = http_client
|
||||
.get(url, Default::default(), true)
|
||||
.await
|
||||
.with_context(|| format!("downloading release from {url}"))?;
|
||||
let body = response.body_mut();
|
||||
|
||||
if let Err(err) = extract_to_staging(body, digest, url, &staging_path, asset_kind).await {
|
||||
cleanup_staging_path(&staging_path, asset_kind).await;
|
||||
return Err(err);
|
||||
}
|
||||
|
||||
if let Err(err) = finalize_download(&staging_path, destination_path).await {
|
||||
cleanup_staging_path(&staging_path, asset_kind).await;
|
||||
return Err(err);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn extract_to_staging(
|
||||
body: impl AsyncRead + Unpin,
|
||||
digest: Option<&str>,
|
||||
url: &str,
|
||||
staging_path: &Path,
|
||||
asset_kind: AssetKind,
|
||||
) -> Result<()> {
|
||||
match digest {
|
||||
Some(expected_sha_256) => {
|
||||
let temp_asset_file = tempfile::NamedTempFile::new()
|
||||
.with_context(|| format!("creating a temporary file for {url}"))?;
|
||||
let (temp_asset_file, _temp_guard) = temp_asset_file.into_parts();
|
||||
let mut writer = HashingWriter {
|
||||
writer: async_fs::File::from(temp_asset_file),
|
||||
hasher: Sha256::new(),
|
||||
};
|
||||
futures::io::copy(&mut BufReader::new(body), &mut writer)
|
||||
.await
|
||||
.with_context(|| {
|
||||
format!("saving archive contents into the temporary file for {url}")
|
||||
})?;
|
||||
let asset_sha_256 = format!("{:x}", writer.hasher.finalize());
|
||||
|
||||
anyhow::ensure!(
|
||||
asset_sha_256 == expected_sha_256,
|
||||
"{url} asset got SHA-256 mismatch. Expected: {expected_sha_256}, Got: {asset_sha_256}",
|
||||
);
|
||||
writer
|
||||
.writer
|
||||
.seek(std::io::SeekFrom::Start(0))
|
||||
.await
|
||||
.with_context(|| format!("seeking temporary file for {url}"))?;
|
||||
stream_file_archive(&mut writer.writer, url, staging_path, asset_kind)
|
||||
.await
|
||||
.with_context(|| {
|
||||
format!("extracting downloaded asset for {url} into {staging_path:?}")
|
||||
})?;
|
||||
}
|
||||
None => {
|
||||
stream_response_archive(body, url, staging_path, asset_kind)
|
||||
.await
|
||||
.with_context(|| {
|
||||
format!("extracting response for asset {url} into {staging_path:?}")
|
||||
})?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn staging_path(parent: &Path, asset_kind: AssetKind) -> Result<PathBuf> {
|
||||
match asset_kind {
|
||||
AssetKind::TarGz | AssetKind::TarBz2 | AssetKind::Zip => {
|
||||
let dir = tempfile::Builder::new()
|
||||
.prefix(".tmp-github-download-")
|
||||
.tempdir_in(parent)
|
||||
.with_context(|| format!("creating staging directory in {parent:?}"))?;
|
||||
Ok(dir.keep())
|
||||
}
|
||||
AssetKind::Gz => {
|
||||
let path = tempfile::Builder::new()
|
||||
.prefix(".tmp-github-download-")
|
||||
.tempfile_in(parent)
|
||||
.with_context(|| format!("creating staging file in {parent:?}"))?
|
||||
.into_temp_path()
|
||||
.keep()
|
||||
.with_context(|| format!("persisting staging file in {parent:?}"))?;
|
||||
Ok(path)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn cleanup_staging_path(staging_path: &Path, asset_kind: AssetKind) {
|
||||
match asset_kind {
|
||||
AssetKind::TarGz | AssetKind::TarBz2 | AssetKind::Zip => {
|
||||
if let Err(err) = async_fs::remove_dir_all(staging_path).await {
|
||||
log::warn!("failed to remove staging directory {staging_path:?}: {err:?}");
|
||||
}
|
||||
}
|
||||
AssetKind::Gz => {
|
||||
if let Err(err) = async_fs::remove_file(staging_path).await {
|
||||
log::warn!("failed to remove staging file {staging_path:?}: {err:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn finalize_download(staging_path: &Path, destination_path: &Path) -> Result<()> {
|
||||
_ = async_fs::remove_dir_all(destination_path).await;
|
||||
async_fs::rename(staging_path, destination_path)
|
||||
.await
|
||||
.with_context(|| format!("renaming {staging_path:?} to {destination_path:?}"))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn stream_response_archive(
|
||||
response: impl AsyncRead + Unpin,
|
||||
url: &str,
|
||||
destination_path: &Path,
|
||||
asset_kind: AssetKind,
|
||||
) -> Result<()> {
|
||||
match asset_kind {
|
||||
AssetKind::TarGz => extract_tar_gz(destination_path, url, response).await?,
|
||||
AssetKind::TarBz2 => extract_tar_bz2(destination_path, url, response).await?,
|
||||
AssetKind::Gz => extract_gz(destination_path, url, response).await?,
|
||||
AssetKind::Zip => {
|
||||
util::archive::extract_zip(destination_path, response).await?;
|
||||
}
|
||||
};
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn stream_file_archive(
|
||||
file_archive: impl AsyncRead + AsyncSeek + Unpin,
|
||||
url: &str,
|
||||
destination_path: &Path,
|
||||
asset_kind: AssetKind,
|
||||
) -> Result<()> {
|
||||
match asset_kind {
|
||||
AssetKind::TarGz => extract_tar_gz(destination_path, url, file_archive).await?,
|
||||
AssetKind::TarBz2 => extract_tar_bz2(destination_path, url, file_archive).await?,
|
||||
AssetKind::Gz => extract_gz(destination_path, url, file_archive).await?,
|
||||
#[cfg(not(windows))]
|
||||
AssetKind::Zip => {
|
||||
util::archive::extract_seekable_zip(destination_path, file_archive).await?;
|
||||
}
|
||||
#[cfg(windows)]
|
||||
AssetKind::Zip => {
|
||||
util::archive::extract_zip(destination_path, file_archive).await?;
|
||||
}
|
||||
};
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn extract_tar_gz(
|
||||
destination_path: &Path,
|
||||
url: &str,
|
||||
from: impl AsyncRead + Unpin,
|
||||
) -> Result<(), anyhow::Error> {
|
||||
let decompressed_bytes = GzipDecoder::new(BufReader::new(from));
|
||||
unpack_tar_archive(destination_path, url, decompressed_bytes).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn extract_tar_bz2(
|
||||
destination_path: &Path,
|
||||
url: &str,
|
||||
from: impl AsyncRead + Unpin,
|
||||
) -> Result<(), anyhow::Error> {
|
||||
let decompressed_bytes = BzDecoder::new(BufReader::new(from));
|
||||
unpack_tar_archive(destination_path, url, decompressed_bytes).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn unpack_tar_archive(
|
||||
destination_path: &Path,
|
||||
url: &str,
|
||||
archive_bytes: impl AsyncRead + Unpin,
|
||||
) -> Result<(), anyhow::Error> {
|
||||
// We don't need to set the modified time. It's irrelevant to downloaded
|
||||
// archive verification, and some filesystems return errors when asked to
|
||||
// apply it after extraction.
|
||||
let archive = async_tar::ArchiveBuilder::new(archive_bytes)
|
||||
.set_preserve_mtime(false)
|
||||
.build();
|
||||
archive
|
||||
.unpack(&destination_path)
|
||||
.await
|
||||
.with_context(|| format!("extracting {url} to {destination_path:?}"))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn extract_gz(
|
||||
destination_path: &Path,
|
||||
url: &str,
|
||||
from: impl AsyncRead + Unpin,
|
||||
) -> Result<(), anyhow::Error> {
|
||||
let mut decompressed_bytes = GzipDecoder::new(BufReader::new(from));
|
||||
let mut file = async_fs::File::create(&destination_path)
|
||||
.await
|
||||
.with_context(|| {
|
||||
format!("creating a file {destination_path:?} for a download from {url}")
|
||||
})?;
|
||||
futures::io::copy(&mut decompressed_bytes, &mut file)
|
||||
.await
|
||||
.with_context(|| format!("extracting {url} to {destination_path:?}"))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
struct HashingWriter<W: AsyncWrite + Unpin> {
|
||||
writer: W,
|
||||
hasher: Sha256,
|
||||
}
|
||||
|
||||
impl<W: AsyncWrite + Unpin> AsyncWrite for HashingWriter<W> {
|
||||
fn poll_write(
|
||||
mut self: Pin<&mut Self>,
|
||||
cx: &mut std::task::Context<'_>,
|
||||
buf: &[u8],
|
||||
) -> Poll<std::result::Result<usize, std::io::Error>> {
|
||||
match Pin::new(&mut self.writer).poll_write(cx, buf) {
|
||||
Poll::Ready(Ok(n)) => {
|
||||
self.hasher.update(&buf[..n]);
|
||||
Poll::Ready(Ok(n))
|
||||
}
|
||||
other => other,
|
||||
}
|
||||
}
|
||||
|
||||
fn poll_flush(
|
||||
mut self: Pin<&mut Self>,
|
||||
cx: &mut std::task::Context<'_>,
|
||||
) -> Poll<Result<(), std::io::Error>> {
|
||||
Pin::new(&mut self.writer).poll_flush(cx)
|
||||
}
|
||||
|
||||
fn poll_close(
|
||||
mut self: Pin<&mut Self>,
|
||||
cx: &mut std::task::Context<'_>,
|
||||
) -> Poll<std::result::Result<(), std::io::Error>> {
|
||||
Pin::new(&mut self.writer).poll_close(cx)
|
||||
}
|
||||
}
|
||||
445
crates/http_client/src/http_client.rs
Normal file
445
crates/http_client/src/http_client.rs
Normal file
@@ -0,0 +1,445 @@
|
||||
mod async_body;
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
pub mod github;
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
pub mod github_download;
|
||||
|
||||
pub use anyhow::{Result, anyhow};
|
||||
pub use async_body::{AsyncBody, Inner, Json};
|
||||
use derive_more::Deref;
|
||||
use http::HeaderValue;
|
||||
pub use http::{self, Method, Request, Response, StatusCode, Uri, request::Builder};
|
||||
|
||||
use futures::future::BoxFuture;
|
||||
use parking_lot::Mutex;
|
||||
use serde::Serialize;
|
||||
use std::sync::Arc;
|
||||
#[cfg(feature = "test-support")]
|
||||
use std::{any::type_name, fmt};
|
||||
pub use url::{Host, Url};
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub enum RedirectPolicy {
|
||||
#[default]
|
||||
NoFollow,
|
||||
FollowLimit(u32),
|
||||
FollowAll,
|
||||
}
|
||||
pub struct FollowRedirects(pub bool);
|
||||
|
||||
pub trait HttpRequestExt {
|
||||
/// Conditionally modify self with the given closure.
|
||||
fn when(self, condition: bool, then: impl FnOnce(Self) -> Self) -> Self
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
if condition { then(self) } else { self }
|
||||
}
|
||||
|
||||
/// Conditionally unwrap and modify self with the given closure, if the given option is Some.
|
||||
fn when_some<T>(self, option: Option<T>, then: impl FnOnce(Self, T) -> Self) -> Self
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
match option {
|
||||
Some(value) => then(self, value),
|
||||
None => self,
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether or not to follow redirects
|
||||
fn follow_redirects(self, follow: RedirectPolicy) -> Self;
|
||||
}
|
||||
|
||||
impl HttpRequestExt for http::request::Builder {
|
||||
fn follow_redirects(self, follow: RedirectPolicy) -> Self {
|
||||
self.extension(follow)
|
||||
}
|
||||
}
|
||||
|
||||
pub trait HttpClient: 'static + Send + Sync {
|
||||
fn user_agent(&self) -> Option<&HeaderValue>;
|
||||
|
||||
fn proxy(&self) -> Option<&Url>;
|
||||
|
||||
fn send(
|
||||
&self,
|
||||
req: http::Request<AsyncBody>,
|
||||
) -> BoxFuture<'static, anyhow::Result<Response<AsyncBody>>>;
|
||||
|
||||
fn get(
|
||||
&self,
|
||||
uri: &str,
|
||||
body: AsyncBody,
|
||||
follow_redirects: bool,
|
||||
) -> BoxFuture<'static, anyhow::Result<Response<AsyncBody>>> {
|
||||
let request = Builder::new()
|
||||
.uri(uri)
|
||||
.follow_redirects(if follow_redirects {
|
||||
RedirectPolicy::FollowAll
|
||||
} else {
|
||||
RedirectPolicy::NoFollow
|
||||
})
|
||||
.body(body);
|
||||
|
||||
match request {
|
||||
Ok(request) => self.send(request),
|
||||
Err(e) => Box::pin(async move { Err(e.into()) }),
|
||||
}
|
||||
}
|
||||
|
||||
fn post_json(
|
||||
&self,
|
||||
uri: &str,
|
||||
body: AsyncBody,
|
||||
) -> BoxFuture<'static, anyhow::Result<Response<AsyncBody>>> {
|
||||
let request = Builder::new()
|
||||
.uri(uri)
|
||||
.method(Method::POST)
|
||||
.header("Content-Type", "application/json")
|
||||
.body(body);
|
||||
|
||||
match request {
|
||||
Ok(request) => self.send(request),
|
||||
Err(e) => Box::pin(async move { Err(e.into()) }),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "test-support")]
|
||||
fn as_fake(&self) -> &FakeHttpClient {
|
||||
panic!("called as_fake on {}", type_name::<Self>())
|
||||
}
|
||||
}
|
||||
|
||||
/// An [`HttpClient`] that may have a proxy.
|
||||
#[derive(Deref)]
|
||||
pub struct HttpClientWithProxy {
|
||||
#[deref]
|
||||
client: Arc<dyn HttpClient>,
|
||||
proxy: Option<Url>,
|
||||
}
|
||||
|
||||
impl HttpClientWithProxy {
|
||||
/// Returns a new [`HttpClientWithProxy`] with the given proxy URL.
|
||||
pub fn new(client: Arc<dyn HttpClient>, proxy_url: Option<String>) -> Self {
|
||||
let proxy_url = proxy_url
|
||||
.and_then(|proxy| proxy.parse().ok())
|
||||
.or_else(read_proxy_from_env);
|
||||
|
||||
Self::new_url(client, proxy_url)
|
||||
}
|
||||
pub fn new_url(client: Arc<dyn HttpClient>, proxy_url: Option<Url>) -> Self {
|
||||
Self {
|
||||
client,
|
||||
proxy: proxy_url,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl HttpClient for HttpClientWithProxy {
|
||||
fn send(
|
||||
&self,
|
||||
req: Request<AsyncBody>,
|
||||
) -> BoxFuture<'static, anyhow::Result<Response<AsyncBody>>> {
|
||||
self.client.send(req)
|
||||
}
|
||||
|
||||
fn user_agent(&self) -> Option<&HeaderValue> {
|
||||
self.client.user_agent()
|
||||
}
|
||||
|
||||
fn proxy(&self) -> Option<&Url> {
|
||||
self.proxy.as_ref()
|
||||
}
|
||||
|
||||
#[cfg(feature = "test-support")]
|
||||
fn as_fake(&self) -> &FakeHttpClient {
|
||||
self.client.as_fake()
|
||||
}
|
||||
}
|
||||
|
||||
/// An [`HttpClient`] that has a base URL.
|
||||
#[derive(Deref)]
|
||||
pub struct HttpClientWithUrl {
|
||||
base_url: Mutex<String>,
|
||||
#[deref]
|
||||
client: HttpClientWithProxy,
|
||||
}
|
||||
|
||||
impl HttpClientWithUrl {
|
||||
/// Returns a new [`HttpClientWithUrl`] with the given base URL.
|
||||
pub fn new(
|
||||
client: Arc<dyn HttpClient>,
|
||||
base_url: impl Into<String>,
|
||||
proxy_url: Option<String>,
|
||||
) -> Self {
|
||||
let client = HttpClientWithProxy::new(client, proxy_url);
|
||||
|
||||
Self {
|
||||
base_url: Mutex::new(base_url.into()),
|
||||
client,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_url(
|
||||
client: Arc<dyn HttpClient>,
|
||||
base_url: impl Into<String>,
|
||||
proxy_url: Option<Url>,
|
||||
) -> Self {
|
||||
let client = HttpClientWithProxy::new_url(client, proxy_url);
|
||||
|
||||
Self {
|
||||
base_url: Mutex::new(base_url.into()),
|
||||
client,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the base URL.
|
||||
pub fn base_url(&self) -> String {
|
||||
self.base_url.lock().clone()
|
||||
}
|
||||
|
||||
/// Sets the base URL.
|
||||
pub fn set_base_url(&self, base_url: impl Into<String>) {
|
||||
let base_url = base_url.into();
|
||||
*self.base_url.lock() = base_url;
|
||||
}
|
||||
|
||||
/// Builds a URL using the given path.
|
||||
pub fn build_url(&self, path: &str) -> String {
|
||||
format!("{}{}", self.base_url(), path)
|
||||
}
|
||||
|
||||
/// Builds a Zed API URL using the given path.
|
||||
pub fn build_zed_api_url(&self, path: &str, query: &[(&str, &str)]) -> Result<Url> {
|
||||
let base_url = self.base_url();
|
||||
let base_api_url = match base_url.as_ref() {
|
||||
"https://zed.dev" => "https://api.zed.dev",
|
||||
"https://staging.zed.dev" => "https://api-staging.zed.dev",
|
||||
"http://localhost:3000" => "http://localhost:8080",
|
||||
other => other,
|
||||
};
|
||||
|
||||
Ok(Url::parse_with_params(
|
||||
&format!("{}{}", base_api_url, path),
|
||||
query,
|
||||
)?)
|
||||
}
|
||||
|
||||
/// Builds a Zed Cloud URL using the given path.
|
||||
pub fn build_zed_cloud_url(&self, path: &str) -> Result<Url> {
|
||||
let base_url = self.base_url();
|
||||
let base_api_url = match base_url.as_ref() {
|
||||
"https://zed.dev" => "https://cloud.zed.dev",
|
||||
"https://staging.zed.dev" => "https://cloud.zed.dev",
|
||||
"http://localhost:3000" => "http://localhost:8787",
|
||||
other => other,
|
||||
};
|
||||
|
||||
Ok(Url::parse(&format!("{}{}", base_api_url, path))?)
|
||||
}
|
||||
|
||||
/// Builds a Zed Cloud URL using the given path and query params.
|
||||
pub fn build_zed_cloud_url_with_query(&self, path: &str, query: impl Serialize) -> Result<Url> {
|
||||
let base_url = self.base_url();
|
||||
let base_api_url = match base_url.as_ref() {
|
||||
"https://zed.dev" => "https://cloud.zed.dev",
|
||||
"https://staging.zed.dev" => "https://cloud.zed.dev",
|
||||
"http://localhost:3000" => "http://localhost:8787",
|
||||
other => other,
|
||||
};
|
||||
let query = serde_urlencoded::to_string(&query)?;
|
||||
Ok(Url::parse(&format!("{}{}?{}", base_api_url, path, query))?)
|
||||
}
|
||||
|
||||
/// Builds a Zed LLM URL using the given path.
|
||||
pub fn build_zed_llm_url(&self, path: &str, query: &[(&str, &str)]) -> Result<Url> {
|
||||
let base_url = self.base_url();
|
||||
let base_api_url = match base_url.as_ref() {
|
||||
"https://zed.dev" => "https://cloud.zed.dev",
|
||||
"https://staging.zed.dev" => "https://llm-staging.zed.dev",
|
||||
"http://localhost:3000" => "http://localhost:8787",
|
||||
other => other,
|
||||
};
|
||||
|
||||
Ok(Url::parse_with_params(
|
||||
&format!("{}{}", base_api_url, path),
|
||||
query,
|
||||
)?)
|
||||
}
|
||||
}
|
||||
|
||||
impl HttpClient for HttpClientWithUrl {
|
||||
fn send(
|
||||
&self,
|
||||
req: Request<AsyncBody>,
|
||||
) -> BoxFuture<'static, anyhow::Result<Response<AsyncBody>>> {
|
||||
self.client.send(req)
|
||||
}
|
||||
|
||||
fn user_agent(&self) -> Option<&HeaderValue> {
|
||||
self.client.user_agent()
|
||||
}
|
||||
|
||||
fn proxy(&self) -> Option<&Url> {
|
||||
self.client.proxy.as_ref()
|
||||
}
|
||||
|
||||
#[cfg(feature = "test-support")]
|
||||
fn as_fake(&self) -> &FakeHttpClient {
|
||||
self.client.as_fake()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn read_proxy_from_env() -> Option<Url> {
|
||||
const ENV_VARS: &[&str] = &[
|
||||
"ALL_PROXY",
|
||||
"all_proxy",
|
||||
"HTTPS_PROXY",
|
||||
"https_proxy",
|
||||
"HTTP_PROXY",
|
||||
"http_proxy",
|
||||
];
|
||||
|
||||
ENV_VARS
|
||||
.iter()
|
||||
.find_map(|var| std::env::var(var).ok())
|
||||
.and_then(|env| env.parse().ok())
|
||||
}
|
||||
|
||||
pub fn read_no_proxy_from_env() -> Option<String> {
|
||||
const ENV_VARS: &[&str] = &["NO_PROXY", "no_proxy"];
|
||||
|
||||
ENV_VARS.iter().find_map(|var| std::env::var(var).ok())
|
||||
}
|
||||
|
||||
pub struct BlockedHttpClient;
|
||||
|
||||
impl BlockedHttpClient {
|
||||
pub fn new() -> Self {
|
||||
BlockedHttpClient
|
||||
}
|
||||
}
|
||||
|
||||
impl HttpClient for BlockedHttpClient {
|
||||
fn send(
|
||||
&self,
|
||||
_req: Request<AsyncBody>,
|
||||
) -> BoxFuture<'static, anyhow::Result<Response<AsyncBody>>> {
|
||||
Box::pin(async {
|
||||
Err(std::io::Error::new(
|
||||
std::io::ErrorKind::PermissionDenied,
|
||||
"BlockedHttpClient disallowed request",
|
||||
)
|
||||
.into())
|
||||
})
|
||||
}
|
||||
|
||||
fn user_agent(&self) -> Option<&HeaderValue> {
|
||||
None
|
||||
}
|
||||
|
||||
fn proxy(&self) -> Option<&Url> {
|
||||
None
|
||||
}
|
||||
|
||||
#[cfg(feature = "test-support")]
|
||||
fn as_fake(&self) -> &FakeHttpClient {
|
||||
panic!("called as_fake on {}", type_name::<Self>())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "test-support")]
|
||||
type FakeHttpHandler = Arc<
|
||||
dyn Fn(Request<AsyncBody>) -> BoxFuture<'static, anyhow::Result<Response<AsyncBody>>>
|
||||
+ Send
|
||||
+ Sync
|
||||
+ 'static,
|
||||
>;
|
||||
|
||||
#[cfg(feature = "test-support")]
|
||||
pub struct FakeHttpClient {
|
||||
handler: Mutex<Option<FakeHttpHandler>>,
|
||||
user_agent: HeaderValue,
|
||||
}
|
||||
|
||||
#[cfg(feature = "test-support")]
|
||||
impl FakeHttpClient {
|
||||
pub fn create<Fut, F>(handler: F) -> Arc<HttpClientWithUrl>
|
||||
where
|
||||
Fut: futures::Future<Output = anyhow::Result<Response<AsyncBody>>> + Send + 'static,
|
||||
F: Fn(Request<AsyncBody>) -> Fut + Send + Sync + 'static,
|
||||
{
|
||||
Arc::new(HttpClientWithUrl {
|
||||
base_url: Mutex::new("http://test.example".into()),
|
||||
client: HttpClientWithProxy {
|
||||
client: Arc::new(Self {
|
||||
handler: Mutex::new(Some(Arc::new(move |req| Box::pin(handler(req))))),
|
||||
user_agent: HeaderValue::from_static(type_name::<Self>()),
|
||||
}),
|
||||
proxy: None,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
pub fn with_404_response() -> Arc<HttpClientWithUrl> {
|
||||
log::warn!("Using fake HTTP client with 404 response");
|
||||
Self::create(|_| async move {
|
||||
Ok(Response::builder()
|
||||
.status(404)
|
||||
.body(Default::default())
|
||||
.unwrap())
|
||||
})
|
||||
}
|
||||
|
||||
pub fn with_200_response() -> Arc<HttpClientWithUrl> {
|
||||
log::warn!("Using fake HTTP client with 200 response");
|
||||
Self::create(|_| async move {
|
||||
Ok(Response::builder()
|
||||
.status(200)
|
||||
.body(Default::default())
|
||||
.unwrap())
|
||||
})
|
||||
}
|
||||
|
||||
pub fn replace_handler<Fut, F>(&self, new_handler: F)
|
||||
where
|
||||
Fut: futures::Future<Output = anyhow::Result<Response<AsyncBody>>> + Send + 'static,
|
||||
F: Fn(FakeHttpHandler, Request<AsyncBody>) -> Fut + Send + Sync + 'static,
|
||||
{
|
||||
let mut handler = self.handler.lock();
|
||||
let old_handler = handler.take().unwrap();
|
||||
*handler = Some(Arc::new(move |req| {
|
||||
Box::pin(new_handler(old_handler.clone(), req))
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "test-support")]
|
||||
impl fmt::Debug for FakeHttpClient {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("FakeHttpClient").finish()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "test-support")]
|
||||
impl HttpClient for FakeHttpClient {
|
||||
fn send(
|
||||
&self,
|
||||
req: Request<AsyncBody>,
|
||||
) -> BoxFuture<'static, anyhow::Result<Response<AsyncBody>>> {
|
||||
((self.handler.lock().as_ref().unwrap())(req)) as _
|
||||
}
|
||||
|
||||
fn user_agent(&self) -> Option<&HeaderValue> {
|
||||
Some(&self.user_agent)
|
||||
}
|
||||
|
||||
fn proxy(&self) -> Option<&Url> {
|
||||
None
|
||||
}
|
||||
|
||||
fn as_fake(&self) -> &FakeHttpClient {
|
||||
self
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user