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

fix(agent): add logic to start Gateway service after update #1172

Merged
merged 5 commits into from
Jan 9, 2025

Conversation

pacmancoder
Copy link
Contributor

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 "Running" state, to avoid unintuitive behavior

Copy link

Let maintainers know that an action is required on their side

  • Add the label release-required Please cut a new release (Devolutions Gateway, Devolutions Agent, Jetsocat, PowerShell module) when you request a maintainer to cut a new release (Devolutions Gateway, Devolutions Agent, Jetsocat, PowerShell module)

  • Add the label release-blocker Follow-up is required before cutting a new release if a follow-up is required before cutting a new release

  • Add the label publish-required Please publish libraries (`Devolutions.Gateway.Utils`, OpenAPI clients, etc) when you request a maintainer to publish libraries (Devolutions.Gateway.Utils, OpenAPI clients, etc.)

  • Add the label publish-blocker Follow-up is required before publishing libraries if a follow-up is required before publishing libraries

@pacmancoder pacmancoder requested a review from CBenoit December 24, 2024 15:39
Copy link
Member

@CBenoit CBenoit left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good work!

General request: Review all the safety comments according to my suggestions.

crates/win-api-wrappers/src/service.rs Outdated Show resolved Hide resolved
crates/win-api-wrappers/src/service.rs Show resolved Hide resolved
use crate::Error;

pub struct ServiceManager {
handle: Owned<SC_HANDLE>,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

praise: Nice, I didn’t notice about these wrappers until now. We should definitely use Ref and Owned more!

crates/win-api-wrappers/src/service.rs Outdated Show resolved Hide resolved
crates/win-api-wrappers/src/service.rs Outdated Show resolved Hide resolved
crates/win-api-wrappers/src/service.rs Outdated Show resolved Hide resolved
crates/win-api-wrappers/src/service.rs Outdated Show resolved Hide resolved
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

praise: Good architecture.

crates/win-api-wrappers/src/service.rs Outdated Show resolved Hide resolved
Ok(Self { handle })
}

fn open_service_with_access(&self, service_name: &str, access: u32) -> anyhow::Result<Service> {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thought: Error type in this library should be refactored and used consistently across all modules, falling back to anyhow for now (I need to throw AllocError from RawBuffer somehow, mapping it to some win32 error code feels wrong).

Copy link
Member

@CBenoit CBenoit Dec 30, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How are you thinking we should go about this? I don’t think it’s necessarily better to have a single Error type which can be anything, but there is no easy way to create lazy enums like in TypeScript to combine multiple errors easily either :/

We may want to provide a top-level Error type for complex errors:

enum Error {
    Alloc(AllocError),
    System(windows::core::Error),
    Other(anyhow::Error),}

Well, quite similar to what we already have in the end 😂
Is it something like that you were thinking about?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The problem is that crate uses a mix of our own Error type (e.g in acl.rs) but in many places anyhow is used (e.g. token.rs). I think we should use a single Error type throughout all crate modules instead of this mix. IMHO anyhow should only be used in top-level crates like devolutions-gateway/devolutions-agent (exception - if third party library throws anyhow, wrap it in its enum variant.

What is really needed in this crate is to eliminate the mixing of error types in library API, but this is definitely out of the scope of PR.

However, I don't know why I decided to switch to anyhow instead of adding new Error variant before going on vacation, thank you for bringing that up, looks like my brain was very blurry during those last-minute changes, I'll look into it again, most likely switch to Error back 😄

Copy link
Member

@CBenoit CBenoit Jan 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, yeah we could stop using anyhow though I would like some way to attach additional context to the error. Sometimes we just get an obscure system error in the Error::System variant with no idea of which call caused it.

Given that we typically don’t inspect programmatically the structured errors, I think surprisingly anyhow is kind of fine even in libraries, though I tend to agree with you; it’s cleaner to have a little bit more of structure for libraries.

I don’t think we should necessarily have a strict rule of "one crate = one Error type" though. The standard library is not following this rule, because errors returned by a given module are often structurally completely unrelated to the kind of errors returned in another module.

We do have that in win-api-wrappers as well: we don’t need to use a wider error in the raw_buffer module, because AllocError is all we need. Everything is cleanly encapsulated inside the module.
Your service module could also just expose a ServiceError that is self-explanatory enough and does not require an additional context string to it:

enum ServiceError {
    #[error("failed to query service state for `{product}`")]
    QueryServiceState {
        product: Product,
        source: win_api_wrappers::Error,
    },
    #[error("failed to start service for `{product}`")]
    StartService {
        product: Product,
        source: win_api_wrappers::Error,
    },
}

Just an idea, maybe you need to add a few variants, and maybe it’s just not a good idea at all in this case.

FYI: I think we should split the win-api-wrappers into two crates: one for the cross-platform primitives (the code that we use to interface with the Windows API, but that is still pure Rust), such as RawBuffer, Win32Dst and other helpers, and one for the code actually wrapping Windows API such as token module. I’m thinking about doing this refactor a little bit later this week. The code in the helper crate would not have a single top-level error because everything is only loosely related, but a top-level error in the API wrapper crate would be very useful.

We could imagine something similar to what we have in IronRDP: https://github.com/Devolutions/IronRDP/blob/25bbb2682c95419fc18a61cab81e29a0ab9f23d4/crates/ironrdp-error/src/lib.rs

This should be adjusted to our needs. First no generic type, we can hardcode the enum. Also maybe we need an optional context string for only system errors:

enum Error {
    Alloc(AllocError),
    System { source: windows::core::Error, context: Option<String> },
    Other(anyhow::Error),}

If you want to go ahead and design that, let me know your ideas in Slack, as the conversation here is getting buried by GitHub UI 😂

Copy link
Member

@CBenoit CBenoit Jan 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, by the way, this didn’t occur to me either when I initially reviewed, but you don’t necessarily need to propagate the AllocError errors. In the dst module, we assume there is enough memory to allocate the structure:

unsafe { raw_buffer.realloc(new_size).expect("OOM") };

(pretty much like Vec is doing)

And we make some reasonable assumptions about the number of elements:

.add(current_count.try_into().ok().expect("count is too big"))

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, I thought about it for a bit and I think having one crate=one error type error is not optimal either...

I like the idea of per-module custom error types. That way the error types will have only minimal nesting (easier to match for specific errors if needed) while keeping the possibility to add more context on-demand and new error kinds without breaking the API (Significantly at least. I think we could make all custom errors #[non_exhaustive] to enforce error matching with catch-all but I don't know if it worth it?).

For the scope of this PR, I'll add a new ServiceError type with a single ServiceError::Win32 variant (I'll follow your suggestion and add .expect for OOM)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good! For the record, I think it was a good thing to have variants based on the kind of failure like you initially had when the PR was opened:

enum ServiceError {
    #[error("failed to query service state for `{product}`")]
    QueryServiceState {
        product: Product,
        source: win_api_wrappers::Error,
    },
    #[error("failed to start service for `{product}`")]
    StartService {
        product: Product,
        source: win_api_wrappers::Error,
    },
}

Instead of win_api_wrappers::Error, this could be windows::core::Error:

enum ServiceError {
    #[error("failed to query service state for `{product}`")]
    QueryServiceState {
        product: Product,
        source: windows::core::Error,
    },
    #[error("failed to start service for `{product}`")]
    StartService {
        product: Product,
        source: windows::core::Error,
    },
}

The source is windows::core::Error for each of them, but the context is different.

I’ll leave the details up to you. You may merge with the single variant too if you prefer.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To be honest, I think it makes sense only in the higher-level crates - e.g. I still use Product in devolutions-agent/src/updater/error.rs to distinguish errors for different products in updater module, but win-api-wrapper errors are usually wrapped in higher level errors and passed as source

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indeed, there are not many FFI calls interleaved. At this point you may as well return a windows::core::Error directly.

@pacmancoder pacmancoder requested a review from CBenoit December 24, 2024 18:17
@pacmancoder
Copy link
Contributor Author

@CBenoit I am ready for review round 2 😃

@pacmancoder
Copy link
Contributor Author

@CBenoit please review again

@pacmancoder pacmancoder requested a review from CBenoit January 7, 2025 15:46
@CBenoit
Copy link
Member

CBenoit commented Jan 7, 2025

Sure! Can you also answer to my questions in the previous comments such as here? #1172 (comment)
Thank you!

@CBenoit
Copy link
Member

CBenoit commented Jan 7, 2025

About this suggestion: #1172 (comment)
There are many other variables that could be shadowed, such as raw_service_handle and raw_sc_handle.

@pacmancoder
Copy link
Contributor Author

Sure! Can you also answer to my questions in the previous comments such as here? #1172 (comment)
Thank you!

Ah sorry, I completely missed them

Copy link
Member

@CBenoit CBenoit left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I’m done reviewing. You can address the last nitpicks, and feel free to merge!

@pacmancoder pacmancoder merged commit 651d8cf into master Jan 9, 2025
36 checks passed
@pacmancoder pacmancoder deleted the fix/updater-service-restart branch January 9, 2025 14:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Development

Successfully merging this pull request may close these issues.

2 participants