Skip to content
Draft
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
5 changes: 5 additions & 0 deletions crates/devolutions-agent-shared/src/update_json.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ use std::fmt;
/// {
/// "Gateway": {
/// "TargetVersion": "1.2.3.4"
/// },
/// "HubService": {
/// "TargetVersion": "latest"
/// }
/// }
/// ```
Expand All @@ -16,6 +19,8 @@ use std::fmt;
pub struct UpdateJson {
#[serde(skip_serializing_if = "Option::is_none")]
pub gateway: Option<ProductUpdateInfo>,
#[serde(skip_serializing_if = "Option::is_none")]
pub hub_service: Option<ProductUpdateInfo>,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
Expand Down
4 changes: 4 additions & 0 deletions crates/devolutions-agent-shared/src/windows/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,7 @@ pub const GATEWAY_UPDATE_CODE: Uuid = uuid!("{db3903d6-c451-4393-bd80-eb9f45b902
///
/// See [`GATEWAY_UPDATE_CODE`] for more information on update codes.
pub const AGENT_UPDATE_CODE: Uuid = uuid!("{82318d3c-811f-4d5d-9a82-b7c31b076755}");
/// MSI upgrade code for the Devolutions Hub Service.
///
/// See [`GATEWAY_UPDATE_CODE`] for more information on update codes.
pub const HUB_SERVICE_UPDATE_CODE: Uuid = uuid!("{f437046e-8e13-430a-8c8f-29fcb9023b59}");
8 changes: 7 additions & 1 deletion devolutions-agent/src/updater/detect.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
use uuid::Uuid;

use devolutions_agent_shared::DateVersion;
use devolutions_agent_shared::windows::{GATEWAY_UPDATE_CODE, registry};
use devolutions_agent_shared::windows::{GATEWAY_UPDATE_CODE, HUB_SERVICE_UPDATE_CODE, registry};

use crate::updater::{Product, UpdaterError};

Expand All @@ -12,11 +12,17 @@ pub(crate) fn get_installed_product_version(product: Product) -> Result<Option<D
Product::Gateway => {
registry::get_installed_product_version(GATEWAY_UPDATE_CODE).map_err(UpdaterError::WindowsRegistry)
}
Product::HubService => {
registry::get_installed_product_version(HUB_SERVICE_UPDATE_CODE).map_err(UpdaterError::WindowsRegistry)
}
}
}

pub(crate) fn get_product_code(product: Product) -> Result<Option<Uuid>, UpdaterError> {
match product {
Product::Gateway => registry::get_product_code(GATEWAY_UPDATE_CODE).map_err(UpdaterError::WindowsRegistry),
Product::HubService => {
registry::get_product_code(HUB_SERVICE_UPDATE_CODE).map_err(UpdaterError::WindowsRegistry)
}
}
}
12 changes: 10 additions & 2 deletions devolutions-agent/src/updater/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ pub(crate) use self::product::Product;
const UPDATE_JSON_WATCH_INTERVAL: Duration = Duration::from_secs(3);

// List of updateable products could be extended in future
const PRODUCTS: &[Product] = &[Product::Gateway];
const PRODUCTS: &[Product] = &[Product::Gateway, Product::HubService];

/// Context for updater task
struct UpdaterCtx {
Expand Down Expand Up @@ -210,8 +210,16 @@ async fn read_update_json(update_file_path: &Utf8Path) -> anyhow::Result<UpdateJ
let update_json_data = fs::read(update_file_path)
.await
.context("failed to read update.json file")?;

// Strip UTF-8 BOM if present (some editors add it)
let data_without_bom = if update_json_data.starts_with(&[0xEF, 0xBB, 0xBF]) {
&update_json_data[3..]
} else {
&update_json_data
};

let update_json: UpdateJson =
serde_json::from_slice(&update_json_data).context("failed to parse update.json file")?;
serde_json::from_slice(data_without_bom).context("failed to parse update.json file")?;

Ok(update_json)
}
Expand Down
99 changes: 82 additions & 17 deletions devolutions-agent/src/updater/package.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ pub(crate) async fn install_package(
log_path: &Utf8Path,
) -> Result<(), UpdaterError> {
match ctx.product {
Product::Gateway => install_msi(ctx, path, log_path).await,
Product::Gateway | Product::HubService => install_msi(ctx, path, log_path).await,
}
}

Expand All @@ -32,7 +32,7 @@ pub(crate) async fn uninstall_package(
log_path: &Utf8Path,
) -> Result<(), UpdaterError> {
match ctx.product {
Product::Gateway => uninstall_msi(ctx, product_code, log_path).await,
Product::Gateway | Product::HubService => uninstall_msi(ctx, product_code, log_path).await,
}
}

Expand Down Expand Up @@ -67,14 +67,46 @@ async fn install_msi(ctx: &UpdaterCtx, path: &Utf8Path, log_path: &Utf8Path) ->
}
}

if msi_install_result.is_err() {
return Err(UpdaterError::MsiInstall {
product: ctx.product,
msi_path: path.to_owned(),
});
match msi_install_result {
Ok(status) => {
let exit_code = status.code().unwrap_or(-1);

// MSI exit codes:
// 0 = Success
// 3010 = Success but reboot required (unexpected - our installers shouldn't require reboot)
// 1641 = Success and reboot initiated
// Other codes = Error
match exit_code {
0 => {
info!("MSI installation completed successfully");
Ok(())
}
3010 | 1641 => {
// Our installers should not require a reboot, but if they do, log as warning
// and continue since the installation technically succeeded
warn!(
%exit_code,
"MSI installation completed but unexpectedly requires system reboot"
);
Ok(())
}
_ => {
error!(%exit_code, "MSI installation failed with exit code");
Err(UpdaterError::MsiInstall {
product: ctx.product,
msi_path: path.to_owned(),
})
}
}
}
Err(_) => {
error!("Failed to execute msiexec command");
Err(UpdaterError::MsiInstall {
product: ctx.product,
msi_path: path.to_owned(),
})
}
}

Ok(())
}

async fn uninstall_msi(ctx: &UpdaterCtx, product_code: Uuid, log_path: &Utf8Path) -> Result<(), UpdaterError> {
Expand All @@ -101,14 +133,47 @@ async fn uninstall_msi(ctx: &UpdaterCtx, product_code: Uuid, log_path: &Utf8Path
}
}

if msi_uninstall_result.is_err() {
return Err(UpdaterError::MsiUninstall {
product: ctx.product,
product_code,
});
match msi_uninstall_result {
Ok(status) => {
let exit_code = status.code().unwrap_or(-1);

// MSI exit codes:
// 0 = Success
// 3010 = Success but reboot required (unexpected - our installers shouldn't require reboot)
// 1641 = Success and reboot initiated
// Other codes = Error
match exit_code {
0 => {
info!(%product_code, "MSI uninstallation completed successfully");
Ok(())
}
3010 | 1641 => {
// Our installers should not require a reboot, but if they do, log as warning
// and continue since the uninstallation technically succeeded
warn!(
%exit_code,
%product_code,
"MSI uninstallation completed but unexpectedly requires system reboot"
);
Ok(())
}
_ => {
error!(%exit_code, %product_code, "MSI uninstallation failed with exit code");
Err(UpdaterError::MsiUninstall {
product: ctx.product,
product_code,
})
}
}
}
Err(_) => {
error!(%product_code, "Failed to execute msiexec command");
Err(UpdaterError::MsiUninstall {
product: ctx.product,
product_code,
})
}
}

Ok(())
}

fn ensure_enough_rights() -> Result<(), UpdaterError> {
Expand Down Expand Up @@ -159,7 +224,7 @@ fn ensure_enough_rights() -> Result<(), UpdaterError> {

pub(crate) fn validate_package(ctx: &UpdaterCtx, path: &Utf8Path) -> Result<(), UpdaterError> {
match ctx.product {
Product::Gateway => validate_msi(ctx, path),
Product::Gateway | Product::HubService => validate_msi(ctx, path),
}
}

Expand Down
8 changes: 7 additions & 1 deletion devolutions-agent/src/updater/product.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,20 @@ use std::str::FromStr;

use devolutions_agent_shared::{ProductUpdateInfo, UpdateJson};

use crate::updater::productinfo::GATEWAY_PRODUCT_ID;
use crate::updater::productinfo::{GATEWAY_PRODUCT_ID, HUB_SERVICE_PRODUCT_ID};

/// Product IDs to track updates for
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum Product {
Gateway,
HubService,
}

impl fmt::Display for Product {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Product::Gateway => write!(f, "Gateway"),
Product::HubService => write!(f, "HubService"),
}
}
}
Expand All @@ -25,6 +27,7 @@ impl FromStr for Product {
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"Gateway" => Ok(Product::Gateway),
"HubService" => Ok(Product::HubService),
_ => Err(()),
}
}
Expand All @@ -34,18 +37,21 @@ impl Product {
pub(crate) fn get_update_info(self, update_json: &UpdateJson) -> Option<ProductUpdateInfo> {
match self {
Product::Gateway => update_json.gateway.clone(),
Product::HubService => update_json.hub_service.clone(),
}
}

pub(crate) const fn get_productinfo_id(self) -> &'static str {
match self {
Product::Gateway => GATEWAY_PRODUCT_ID,
Product::HubService => HUB_SERVICE_PRODUCT_ID,
}
}

pub(crate) const fn get_package_extension(self) -> &'static str {
match self {
Product::Gateway => "msi",
Product::HubService => "msi",
}
}
}
Loading
Loading