Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Installer Downloader] Detect native CPU arch #7709

Merged
merged 3 commits into from
Feb 27, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions installer-downloader/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ chrono = { workspace = true, features = ["clock"] }
fern = { version = "0.6", default-features = false }
log = { workspace = true }

talpid-platform-metadata = { path = "../talpid-platform-metadata" }
mullvad-update = { path = "../mullvad-update", features = ["client"] }

hex = "0.4"
Expand Down
11 changes: 10 additions & 1 deletion installer-downloader/src/cacao_impl/mod.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use std::sync::Mutex;

use cacao::appkit::App;
use installer_downloader::environment::{Environment, Error as EnvError};
use ui::{Action, AppImpl};

mod delegate;
Expand All @@ -9,8 +10,16 @@ mod ui;
pub fn main() {
let app = App::new("net.mullvad.downloader", AppImpl::default());

// Load "global" values and resources
let environment = match Environment::load() {
Ok(env) => env,
Err(EnvError::Arch) => {
unreachable!("The CPU architecture will always be retrievable on macOS")
}
};

let cb: Mutex<Option<ui::MainThreadCallback>> = Mutex::new(Some(Box::new(|self_| {
crate::controller::initialize_controller(self_);
crate::controller::initialize_controller(self_, environment);
})));
cacao::appkit::App::<ui::AppImpl, _>::dispatch_main(Action::QueueMain(cb));

Expand Down
23 changes: 16 additions & 7 deletions installer-downloader/src/controller.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
//! This module implements the actual logic performed by different UI components.

use crate::delegate::{AppDelegate, AppDelegateQueue};
use crate::environment::Environment;
use crate::resource;
use crate::temp::DirectoryProvider;
use crate::ui_downloader::{UiAppDownloader, UiAppDownloaderParameters, UiProgressUpdater};

use mullvad_update::{
api::VersionInfoProvider,
app::{self, AppDownloader},
version::{Version, VersionArchitecture, VersionInfo, VersionParameters},
version::{Version, VersionInfo, VersionParameters},
};
use rand::seq::SliceRandom;

Expand All @@ -29,7 +30,7 @@ enum TaskMessage {
pub struct AppController {}

/// Public entry function for registering a [AppDelegate].
pub fn initialize_controller<T: AppDelegate + 'static>(delegate: &mut T) {
pub fn initialize_controller<T: AppDelegate + 'static>(delegate: &mut T, environment: Environment) {
use mullvad_update::{api::HttpVersionInfoProvider, app::HttpAppDownloader};

// App downloader to use
Expand All @@ -48,7 +49,11 @@ pub fn initialize_controller<T: AppDelegate + 'static>(delegate: &mut T) {
verifying_key,
};

AppController::initialize::<_, Downloader<T>, _, DirProvider>(delegate, version_provider)
AppController::initialize::<_, Downloader<T>, _, DirProvider>(
delegate,
version_provider,
environment,
)
}

/// JSON files should be stored at `<base url>/<platform>.json`.
Expand All @@ -68,8 +73,11 @@ impl AppController {
///
/// Providing the downloader and version info fetcher as type arguments, they're decoupled from
/// the logic of [AppController], allowing them to be mocked.
pub fn initialize<D, A, V, DirProvider>(delegate: &mut D, version_provider: V)
where
pub fn initialize<D, A, V, DirProvider>(
delegate: &mut D,
version_provider: V,
environment: Environment,
) where
D: AppDelegate + 'static,
V: VersionInfoProvider + Send + 'static,
A: From<UiAppDownloaderParameters<D>> + AppDownloader + 'static,
Expand All @@ -93,6 +101,7 @@ impl AppController {
delegate.queue(),
task_tx.clone(),
version_provider,
environment,
));
Self::register_user_action_callbacks(delegate, task_tx);
}
Expand Down Expand Up @@ -125,14 +134,14 @@ async fn fetch_app_version_info<Delegate, VersionProvider>(
queue: Delegate::Queue,
download_tx: mpsc::Sender<TaskMessage>,
version_provider: VersionProvider,
Environment { architecture }: Environment,
) where
Delegate: AppDelegate + 'static,
VersionProvider: VersionInfoProvider + Send,
{
loop {
let version_params = VersionParameters {
// TODO: detect current architecture
architecture: VersionArchitecture::X86,
architecture,
// For the downloader, the rollout version is always preferred
rollout: 1.,
// The downloader allows any version
Expand Down
36 changes: 36 additions & 0 deletions installer-downloader/src/environment.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
use mullvad_update::version::VersionArchitecture;

/// The environment consists of globals and/or constants which need to be computed at runtime.
pub struct Environment {
pub architecture: mullvad_update::format::Architecture,
}

pub enum Error {
/// Failed to get the host's CPU architecture.
Arch,
}

impl Environment {
/// Try to load the environment.
pub fn load() -> Result<Self, Error> {
let architecture = Self::get_arch()?;

Ok(Environment { architecture })
}

/// Try to map the host's CPU architecture to one of the CPU architectures the Mullvad VPN app
/// supports.
fn get_arch() -> Result<VersionArchitecture, Error> {
let arch = talpid_platform_metadata::get_native_arch()
.inspect_err(|err| log::debug!("{err}"))
.map_err(|_| Error::Arch)?
.ok_or(Error::Arch)?;

let arch = match arch {
talpid_platform_metadata::Architecture::X86 => VersionArchitecture::X86,
talpid_platform_metadata::Architecture::Arm64 => VersionArchitecture::Arm64,
};

Ok(arch)
}
}
2 changes: 2 additions & 0 deletions installer-downloader/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ pub mod controller;
#[cfg(any(target_os = "windows", target_os = "macos"))]
pub mod delegate;
#[cfg(any(target_os = "windows", target_os = "macos"))]
pub mod environment;
#[cfg(any(target_os = "windows", target_os = "macos"))]
pub mod log;
#[cfg(any(target_os = "windows", target_os = "macos"))]
pub mod resource;
Expand Down
16 changes: 15 additions & 1 deletion installer-downloader/src/winapi_impl/mod.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use installer_downloader::environment::{Environment, Error as EnvError};
use native_windows_gui as nwg;

use crate::delegate::{AppDelegate, AppDelegateQueue};
Expand All @@ -9,14 +10,27 @@ pub fn main() {
nwg::init().expect("Failed to init Native Windows GUI");
nwg::Font::set_global_family("Segoe UI").expect("Failed to set default font");

// Load "global" values and resources
let environment = match Environment::load() {
Ok(env) => env,
Err(error) => fatal_environment_error(error),
};

let window = ui::AppWindow::default();
let window = window.layout().unwrap();

let queue = window.borrow().queue();

queue.queue_main(|window| {
crate::controller::initialize_controller(window);
crate::controller::initialize_controller(window, environment);
});

nwg::dispatch_thread_events();
}

fn fatal_environment_error(error: EnvError) -> ! {
let content = match error {
EnvError::Arch => "Failed to detect CPU architecture",
};
nwg::fatal_message(installer_downloader::resource::WINDOW_TITLE, content)
}
1 change: 1 addition & 0 deletions talpid-platform-metadata/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,5 @@ features = [
"Win32_System_LibraryLoader",
"Win32_System_SystemInformation",
"Win32_System_SystemServices",
"Win32_System_Threading",
]
58 changes: 58 additions & 0 deletions talpid-platform-metadata/src/arch.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
//! Detect the running platform's CPU architecture.

/// CPU architectures supported by the talpid family of crates.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Architecture {
/// x86-64 architecture
X86,
/// ARM64 architecture
Arm64,
}

/// Return native architecture (ignoring WOW64). If the native architecture can not be detected,
/// [`None`] is returned. This should never be the case on working X86_64 or Arm64 systems.
#[cfg(target_os = "windows")]
pub fn get_native_arch() -> Result<Option<Architecture>, std::io::Error> {
use core::ffi::c_ushort;
use windows_sys::Win32::System::SystemInformation::{
IMAGE_FILE_MACHINE_AMD64, IMAGE_FILE_MACHINE_ARM64,
};
use windows_sys::Win32::System::Threading::{GetCurrentProcess, IsWow64Process2};

let native_arch = {
let mut running_arch: c_ushort = 0;
let mut native_arch: c_ushort = 0;

// SAFETY: Trivially safe. The current process handle is a glorified constant.
let current_process = unsafe { GetCurrentProcess() };

// IsWow64Process2:
// Determines whether the specified process is running under WOW64; also returns additional machine process and architecture information.
//
// SAFETY: Trivially safe, since we provide the required arguments.
if 0 == unsafe { IsWow64Process2(current_process, &mut running_arch, &mut native_arch) } {
return Err(std::io::Error::last_os_error());
}

native_arch
};

match native_arch {
IMAGE_FILE_MACHINE_AMD64 => Ok(Some(Architecture::X86)),
IMAGE_FILE_MACHINE_ARM64 => Ok(Some(Architecture::Arm64)),
_other => Ok(None),
}
}

/// Return native architecture.
#[cfg(not(target_os = "windows"))]
pub fn get_native_arch() -> Result<Option<Architecture>, std::io::Error> {
const TARGET_ARCH: Option<Architecture> = if cfg!(any(target_arch = "x86_64",)) {
Some(Architecture::X86)
} else if cfg!(target_arch = "aarch64") {
Some(Architecture::Arm64)
} else {
None
};
Ok(TARGET_ARCH)
}
4 changes: 4 additions & 0 deletions talpid-platform-metadata/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
mod arch;
#[cfg(target_os = "linux")]
#[path = "linux.rs"]
mod imp;
Expand All @@ -19,3 +20,6 @@ pub use self::imp::MacosVersion;
#[cfg(windows)]
pub use self::imp::WindowsVersion;
pub use self::imp::{extra_metadata, short_version, version};

pub use arch::get_native_arch;
pub use arch::Architecture;
5 changes: 3 additions & 2 deletions windows-installer/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,10 @@ rust-version.workspace = true
workspace = true

[target.'cfg(all(target_os = "windows", target_arch = "x86_64"))'.dependencies]
windows-sys = { version = "0.52.0", features = ["Win32_System", "Win32_System_LibraryLoader", "Win32_System_SystemInformation", "Win32_System_Threading"] }
tempfile = "3.10"
anyhow.workspace = true
talpid-platform-metadata = { path = "../talpid-platform-metadata" }
tempfile = "3.10"
windows-sys = { version = "0.52.0", features = ["Win32_System", "Win32_System_LibraryLoader", "Win32_System_SystemInformation", "Win32_System_Threading"] }

[build-dependencies]
winres = "0.1"
Expand Down
32 changes: 10 additions & 22 deletions windows-installer/src/windows.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
//! * `WIN_ARM64_INSTALLER` - a path to the ARM64 Windows installer
use anyhow::{bail, Context};
use std::{
ffi::{c_ushort, OsStr},
ffi::OsStr,
io::{self, Write},
num::NonZero,
process::{Command, ExitStatus},
Expand All @@ -16,11 +16,7 @@ use std::{
use tempfile::TempPath;
use windows_sys::{
w,
Win32::System::{
LibraryLoader::{FindResourceW, LoadResource, LockResource, SizeofResource},
SystemInformation::{IMAGE_FILE_MACHINE_AMD64, IMAGE_FILE_MACHINE_ARM64},
Threading::IsWow64Process2,
},
Win32::System::LibraryLoader::{FindResourceW, LoadResource, LockResource, SizeofResource},
};

/// Import resource constants from `resource.rs`. This is automatically generated by the build
Expand Down Expand Up @@ -124,22 +120,14 @@ enum Architecture {

/// Return native architecture (ignoring WOW64)
fn get_native_arch() -> anyhow::Result<Architecture> {
let mut running_arch: c_ushort = 0;
let mut native_arch: c_ushort = 0;

// SAFETY: Trivially safe, since we provide the required arguments. `hprocess == 0` is
// undocumented but refers to the current process.
let result = unsafe { IsWow64Process2(0, &mut running_arch, &mut native_arch) };
if result == 0 {
bail!(
"Failed to get native architecture: {}",
io::Error::last_os_error()
);
}
let Some(arch) =
talpid_platform_metadata::get_native_arch().context("Failed to get native architecture")?
else {
bail!("Unable to detect native architecture (most likely unsupported)");
};

match native_arch {
IMAGE_FILE_MACHINE_AMD64 => Ok(Architecture::X64),
IMAGE_FILE_MACHINE_ARM64 => Ok(Architecture::Arm64),
other => bail!("unsupported architecture: {other}"),
match arch {
talpid_platform_metadata::Architecture::X86 => Ok(Architecture::X64),
talpid_platform_metadata::Architecture::Arm64 => Ok(Architecture::Arm64),
}
}
Loading