-
Notifications
You must be signed in to change notification settings - Fork 13
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
fix(agent): add logic to start Gateway service after update (#1172)
This PR adds the following logic to the updater: * Gateway service startup mode when correctly performing MSI install * Start service if it was in "Manual" startup mode before installation, but in the "Running" state, to avoid unintuitive behavior
- Loading branch information
1 parent
3b8667d
commit 651d8cf
Showing
10 changed files
with
284 additions
and
10 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,160 @@ | ||
use std::alloc::Layout; | ||
|
||
use thiserror::Error; | ||
use windows::core::Owned; | ||
use windows::Win32::Foundation::{ERROR_INSUFFICIENT_BUFFER, GENERIC_READ}; | ||
use windows::Win32::System::Services::{ | ||
OpenSCManagerW, OpenServiceW, QueryServiceConfigW, QueryServiceStatus, StartServiceW, QUERY_SERVICE_CONFIGW, | ||
SC_HANDLE, SC_MANAGER_ALL_ACCESS, SERVICE_ALL_ACCESS, SERVICE_AUTO_START, SERVICE_BOOT_START, SERVICE_DEMAND_START, | ||
SERVICE_DISABLED, SERVICE_QUERY_CONFIG, SERVICE_QUERY_STATUS, SERVICE_RUNNING, SERVICE_STATUS, | ||
SERVICE_SYSTEM_START, | ||
}; | ||
|
||
use crate::raw_buffer::RawBuffer; | ||
use crate::utils::WideString; | ||
|
||
#[derive(Debug, Error)] | ||
pub enum ServiceError { | ||
#[error(transparent)] | ||
WinAPI(#[from] windows::core::Error), | ||
} | ||
|
||
pub type ServiceResult<T> = Result<T, ServiceError>; | ||
pub struct ServiceManager { | ||
handle: Owned<SC_HANDLE>, | ||
} | ||
|
||
impl ServiceManager { | ||
pub fn open_read() -> ServiceResult<Self> { | ||
Self::open_with_access(GENERIC_READ.0) | ||
} | ||
|
||
pub fn open_all_access() -> ServiceResult<Self> { | ||
Self::open_with_access(SC_MANAGER_ALL_ACCESS) | ||
} | ||
|
||
fn open_with_access(access: u32) -> ServiceResult<Self> { | ||
// SAFETY: FFI call with no outstanding preconditions. | ||
let handle = unsafe { OpenSCManagerW(None, None, access)? }; | ||
|
||
// SAFETY: On success, the handle returned by `OpenSCManagerW` is valid and owned by the | ||
// caller. | ||
let handle = unsafe { Owned::new(handle) }; | ||
|
||
Ok(Self { handle }) | ||
} | ||
|
||
fn open_service_with_access(&self, service_name: &str, access: u32) -> ServiceResult<Service> { | ||
let service_name = WideString::from(service_name); | ||
|
||
// SAFETY: | ||
// - Value passed as hSCManager is valid as long as `ServiceManager` instance is alive. | ||
// - service_name is a valid, null-terminated UTF-16 string allocated on the heap. | ||
let handle = unsafe { OpenServiceW(*self.handle, service_name.as_pcwstr(), access)? }; | ||
|
||
// SAFETY: Handle returned by `OpenServiceW` is valid and needs to be closed after use, | ||
// thus it is safe to take ownership of it via `Owned`. | ||
let handle = unsafe { Owned::new(handle) }; | ||
|
||
Ok(Service { handle }) | ||
} | ||
|
||
pub fn open_service_read(&self, service_name: &str) -> ServiceResult<Service> { | ||
self.open_service_with_access(service_name, SERVICE_QUERY_CONFIG | SERVICE_QUERY_STATUS) | ||
} | ||
|
||
pub fn open_service_all_access(&self, service_name: &str) -> ServiceResult<Service> { | ||
self.open_service_with_access(service_name, SERVICE_ALL_ACCESS) | ||
} | ||
} | ||
|
||
#[derive(Debug, Clone, Copy, PartialEq, Eq)] | ||
pub enum ServiceStartupMode { | ||
Boot, | ||
System, | ||
Automatic, | ||
Manual, | ||
Disabled, | ||
} | ||
|
||
pub struct Service { | ||
handle: Owned<SC_HANDLE>, | ||
} | ||
|
||
impl Service { | ||
pub fn startup_mode(&self) -> ServiceResult<ServiceStartupMode> { | ||
let mut cbbufsize = 0u32; | ||
let mut pcbbytesneeded = 0u32; | ||
|
||
// SAFETY: FFI call with no outstanding preconditions. | ||
let result = unsafe { QueryServiceConfigW(*self.handle, None, 0, &mut pcbbytesneeded) }; | ||
|
||
match result { | ||
Err(err) if err.code() == ERROR_INSUFFICIENT_BUFFER.to_hresult() => { | ||
// Expected error, continue. | ||
} | ||
Err(err) => return Err(err.into()), | ||
Ok(_) => panic!("QueryServiceConfigW should fail with ERROR_INSUFFICIENT_BUFFER"), | ||
} | ||
|
||
// The most typical buffer we work with in Rust are homogeneous arrays of integers such | ||
// as [u8] or Vec<u8>, but in Microsoft’s Win32 documentation, a `buffer` generally refers | ||
// to a caller-allocated memory region that an API uses to either input or output data, and | ||
// it is ultimately coerced into some other type with various alignment requirements. | ||
// | ||
// lpServiceConfig should point to aligned buffer that could hold a QUERY_SERVICE_CONFIGW | ||
// structure. | ||
let layout = Layout::from_size_align( | ||
usize::try_from(pcbbytesneeded).expect("pcbbytesneeded < 8K as per MSDN"), | ||
align_of::<QUERY_SERVICE_CONFIGW>(), | ||
) | ||
.expect("layout always satisfies from_size_align invariants"); | ||
|
||
// SAFETY: The layout initialization is checked using the Layout::from_size_align method. | ||
let mut buffer = unsafe { RawBuffer::alloc_zeroed(layout).expect("OOM") }; | ||
|
||
// SAFETY: Buffer passed to `lpServiceConfig` have enough size to hold a | ||
// QUERY_SERVICE_CONFIGW structure, as required size was queried and allocated above. | ||
// Passed buffer have correct alignment to hold QUERY_SERVICE_CONFIGW structure. | ||
unsafe { | ||
// Pointer cast is valid, as `buffer` is allocated with correct alignment above. | ||
#[expect(clippy::cast_ptr_alignment)] | ||
QueryServiceConfigW( | ||
*self.handle, | ||
Some(buffer.as_mut_ptr().cast::<QUERY_SERVICE_CONFIGW>()), | ||
pcbbytesneeded, | ||
&mut cbbufsize, | ||
)? | ||
}; | ||
|
||
// SAFETY: `QueryServiceConfigW` succeeded, thus `lpserviceconfig` is valid and contains | ||
// a QUERY_SERVICE_CONFIGW structure. | ||
let config = unsafe { buffer.as_ref_cast::<QUERY_SERVICE_CONFIGW>() }; | ||
|
||
match config.dwStartType { | ||
SERVICE_BOOT_START => Ok(ServiceStartupMode::Boot), | ||
SERVICE_SYSTEM_START => Ok(ServiceStartupMode::System), | ||
SERVICE_AUTO_START => Ok(ServiceStartupMode::Automatic), | ||
SERVICE_DEMAND_START => Ok(ServiceStartupMode::Manual), | ||
SERVICE_DISABLED => Ok(ServiceStartupMode::Disabled), | ||
_ => panic!("WinAPI returned invalid service startup mode"), | ||
} | ||
} | ||
|
||
pub fn is_running(&self) -> ServiceResult<bool> { | ||
let mut service_status = SERVICE_STATUS::default(); | ||
|
||
// SAFETY: hService is a valid handle. | ||
// lpServiceStatus is a valid pointer to a stack-allocated SERVICE_STATUS structure. | ||
unsafe { QueryServiceStatus(*self.handle, &mut service_status as *mut _)? }; | ||
|
||
Ok(service_status.dwCurrentState == SERVICE_RUNNING) | ||
} | ||
|
||
pub fn start(&self) -> ServiceResult<()> { | ||
// SAFETY: FFI call with no outstanding preconditions. | ||
unsafe { StartServiceW(*self.handle, None)? }; | ||
|
||
Ok(()) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,88 @@ | ||
use win_api_wrappers::service::{ServiceManager, ServiceStartupMode}; | ||
|
||
use crate::updater::{Product, UpdaterError}; | ||
|
||
const SERVICE_NAME: &str = "DevolutionsGateway"; | ||
|
||
/// Additional actions that need to be performed during product update process | ||
pub(crate) trait ProductUpdateActions { | ||
fn pre_update(&mut self) -> Result<(), UpdaterError>; | ||
fn get_msiexec_install_params(&self) -> Vec<String>; | ||
fn post_update(&mut self) -> Result<(), UpdaterError>; | ||
} | ||
|
||
/// Gateway specific update actions | ||
#[derive(Default)] | ||
struct GatewayUpdateActions { | ||
service_was_running: bool, | ||
service_startup_was_automatic: bool, | ||
} | ||
|
||
impl GatewayUpdateActions { | ||
fn pre_update_impl(&mut self) -> anyhow::Result<()> { | ||
info!("Querying service state for Gateway"); | ||
let service_manager = ServiceManager::open_read()?; | ||
let service = service_manager.open_service_read(SERVICE_NAME)?; | ||
|
||
self.service_startup_was_automatic = service.startup_mode()? == ServiceStartupMode::Automatic; | ||
self.service_was_running = service.is_running()?; | ||
|
||
info!( | ||
"Service state for Gateway before update: running: {}, automatic_startup: {}", | ||
self.service_was_running, self.service_startup_was_automatic | ||
); | ||
|
||
Ok(()) | ||
} | ||
|
||
fn post_update_impl(&self) -> anyhow::Result<()> { | ||
// Start service if it was running prior to the update, but service startup | ||
// was set to manual. | ||
if !self.service_startup_was_automatic && self.service_was_running { | ||
info!("Starting Gateway service after update"); | ||
|
||
let service_manager = ServiceManager::open_all_access()?; | ||
let service = service_manager.open_service_all_access(SERVICE_NAME)?; | ||
service.start()?; | ||
|
||
info!("Gateway service started"); | ||
} | ||
|
||
Ok(()) | ||
} | ||
} | ||
|
||
impl ProductUpdateActions for GatewayUpdateActions { | ||
fn pre_update(&mut self) -> Result<(), UpdaterError> { | ||
self.pre_update_impl() | ||
.map_err(|source| UpdaterError::QueryServiceState { | ||
product: Product::Gateway, | ||
source, | ||
}) | ||
} | ||
|
||
fn get_msiexec_install_params(&self) -> Vec<String> { | ||
// When performing update, we want to make sure the service startup mode is restored to the | ||
// previous state. (Installer sets Manual by default). | ||
if self.service_startup_was_automatic { | ||
info!("Adjusting MSIEXEC parameters for Gateway service startup mode"); | ||
|
||
return vec!["P.SERVICESTART=Automatic".to_string()]; | ||
} | ||
|
||
Vec::new() | ||
} | ||
|
||
fn post_update(&mut self) -> Result<(), UpdaterError> { | ||
self.post_update_impl().map_err(|source| UpdaterError::StartService { | ||
product: Product::Gateway, | ||
source, | ||
}) | ||
} | ||
} | ||
|
||
pub(crate) fn build_product_actions(product: Product) -> Box<dyn ProductUpdateActions + Sync + Send + 'static> { | ||
match product { | ||
Product::Gateway => Box::new(GatewayUpdateActions::default()), | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters