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

Please provide an example for using WMI ExecMethod, full with passing arguments and extracting results #2751

Closed
ydirson opened this issue Dec 21, 2023 · 20 comments · Fixed by #2794
Labels
question Further information is requested

Comments

@ydirson
Copy link

ydirson commented Dec 21, 2023

Suggestion

#2041 adds sample WMI code, but it does not help with the more complex ExecMethod mechanism, which needs to be used eg. in a Xen VM to access read/write to XenStore (through xeniface driver).

Ideally the experience gained on xenstore-win experimentation could be used to implement an ExecMetho abstraction in wmi-rs

@kennykerr
Copy link
Collaborator

I'd suggest reaching out to https://stackoverflow.com/search?q=windows-rs

@ydirson
Copy link
Author

ydirson commented Dec 22, 2023

@kennykerr kennykerr added question Further information is requested and removed enhancement New feature or request labels Dec 22, 2023
@ydirson
Copy link
Author

ydirson commented Jan 3, 2024

Well, apparently crowds do not seem to have much to say on this point 🤷

@ydirson
Copy link
Author

ydirson commented Jan 8, 2024

@kennykerr any idea how we can make progress on this?

@kennykerr
Copy link
Collaborator

If you are just looking for help using an API, you can also try: https://docs.microsoft.com/en-us/answers/topics/windows-api.html

If you have a minimal repro in C++ and are just looking for help porting to Rust, you can share the minimal repro here and I can take another look.

@ydirson
Copy link
Author

ydirson commented Jan 8, 2024

I do have a minimal repro, but only in Python as shown on StackOverflow and PowerShell as follows:

# Locate the base object
$base = gwmi -n root\wmi -cl CitrixXenStoreBase
# Create a session
$sid = $base.AddSession("MyNewSession")
$session = gwmi -n root\wmi -q "select * from CitrixXenStoreSession where SessionId=$($sid.SessionId)"

# Write a value
$session.SetValue("data/TempValue","This is a string")

@kennykerr
Copy link
Collaborator

@ydirson
Copy link
Author

ydirson commented Jan 15, 2024

This comment is helpful:

https://stackoverflow.com/questions/77703233/using-wmi-iwbemservicesexecmethod-from-rust#comment137154095_77703233

The comment has its merits, and notably helps me to realize the URL to the driver was incorrect.
Nevertheless, the question indeed applies to any ExecMethod call with input and output parameters - the thing is, I'm no Windows dev or admin, and I had to choose a short example I could easily test myself. This WMI SDK example will likely be easier for Windows devs.

That example indeed shows API calls I had not encountered before, so I made a first attempt. I get stuck from things that possibly just come from my lack of familiarity with Windows APIs - the particular blocking point here is "how can I feed a Variant containing a BSTR to that Put method?" - the variant doc only mentions String, despite having a VT_BSTR

use std::error::Error;
use windows::core::{BSTR, w};
use windows::Win32::System::{
    Com::VARIANT,
    Wmi::{
        IWbemClassObject,
        //IWbemServices,
        WBEM_FLAG_RETURN_WBEM_COMPLETE,
    },
};
use wmi::{COMLibrary, WMIConnection, WMIError};

fn type_of<T>(_: &T) -> String{
    format!("{}", std::any::type_name::<T>())
}

fn main() -> Result<(), Box<dyn Error>> {
    let cnx = WMIConnection::with_namespace_path(r#"ROOT\CIMV2"#, COMLibrary::new()?)?;
    eprintln!("WMI opened: {:p}", &cnx);

    // get Win32_Process object

    let mut wmi_object = None;
    let object_path = BSTR::from("Win32_Process");
    let ret = unsafe { cnx.svc.GetObject(&object_path,
                                         WBEM_FLAG_RETURN_WBEM_COMPLETE.0 as _,
                                         None,
                                         Some(&mut wmi_object),
                                         None) };
    ret.expect("GetObject failure");
    let wmi_object: IWbemClassObject = wmi_object.ok_or(WMIError::NullPointerResult)?;

    // GetMethod("Create")

    let mut in_params_class: Option<IWbemClassObject> = None;
    let mut out_params_class: Option<IWbemClassObject> = None;

    
    let ret = unsafe { wmi_object.GetMethod(w!("Create"), 0,
                                            &mut in_params_class, &mut out_params_class) };
    ret.expect("GetMethod failure");

    // in params
    
    let in_params_class = in_params_class.unwrap();
    let mut in_params: Option<IWbemClassObject> = None;
    let ret = unsafe { in_params_class.SpawnInstance(0) };
    let in_params = ret.expect("SpawnInstance should return an instance");

    let var_command: VARIANT = VARIANT::from(BSTR::from("notepad.exe"));
    let ret = in_params.Put(w!("CommandLine"), 0, &var_command, 0);

    // ...

    Ok(())
}

@kennykerr
Copy link
Collaborator

I recently added a sample illustrating how to use VARIANT types.

https://github.com/microsoft/windows-rs/blob/master/crates/samples/windows/shell/src/main.rs

This has not yet been published, but you add a git dependency to kick the tires:

https://doc.rust-lang.org/cargo/reference/specifying-dependencies.html#specifying-dependencies-from-git-repositories

@ydirson
Copy link
Author

ydirson commented Jan 15, 2024

I recently added a sample illustrating how to use VARIANT types.

https://github.com/microsoft/windows-rs/blob/master/crates/samples/windows/shell/src/main.rs

That does not seem too far from my attempt at using VARIANT::from(), except that 0.48 does not implement it for BSTR.

This has not yet been published, but you add a git dependency to kick the tires:

Do you mean by this, that unreleased crate functionality is necessary to create a BSTR variant? That would be unfortunate, as wmi-rs still forces me to stick with 0.48

@kennykerr
Copy link
Collaborator

It should be possible with older versions, just more complicated.

@ydirson
Copy link
Author

ydirson commented Jan 15, 2024

I'm starting to consider dropping the wmi-rs crate altogether, since it does not really appear to be maintained, but that will require translating into Rust those boilerplate-looking steps 1-5 from the WMI example, possibly taking code from wmi-rs itself.

Given the number of calls involved, it would likely be useful to have a Rust version of that example.

@kennykerr
Copy link
Collaborator

Thanks for the example, I'll see if I can write a quick sample.

@ydirson
Copy link
Author

ydirson commented Jan 15, 2024

My quick incomplete one, inspired by wmi-rs for initialization, so 0.48-targeted ... and already problematic for 0.52 when it comes to passing WBEM_GENERIC_FLAG_TYPE as u32

use std::error::Error;
use windows::core::{BSTR, w};
use windows::Win32::System::{ Com, Rpc, Wmi };

fn type_of<T>(_: &T) -> String{
    format!("{}", std::any::type_name::<T>())
}

fn main() -> Result<(), Box<dyn Error>> {
    unsafe {
        Com::CoInitializeEx(None, Com::COINIT_MULTITHREADED)
    }.expect("should initialize COM");
    unsafe {
        Com::CoInitializeSecurity(
            None,
            -1, // let COM choose.
            None,
            None,
            Com::RPC_C_AUTHN_LEVEL_DEFAULT,
            Com::RPC_C_IMP_LEVEL_IMPERSONATE,
            None,
            Com::EOAC_NONE,
            None,
        )
    }.expect("should initialize COM security");
    // COM locator
    let loc: Wmi::IWbemLocator = unsafe {
        Com::CoCreateInstance(&Wmi::WbemLocator, None, Com::CLSCTX_INPROC_SERVER)
    }.expect("should get a COM locator");
    // connection
    let object_path_bstr = BSTR::from(r#"ROOT\CIMV2"#);
    let svc = unsafe {
        loc.ConnectServer(
            &object_path_bstr,
            &BSTR::new(),
            &BSTR::new(),
            &BSTR::new(),
            Wmi::WBEM_FLAG_CONNECT_USE_MAX_WAIT.0,
            &BSTR::new(),
            None,
        )
    }.expect("should connect to WMI service");

    // "set proxy"
    unsafe {
        Com::CoSetProxyBlanket(
            &svc,
            Rpc::RPC_C_AUTHN_WINNT, // RPC_C_AUTHN_xxx
            Rpc::RPC_C_AUTHZ_NONE,  // RPC_C_AUTHZ_xxx
            None,
            Com::RPC_C_AUTHN_LEVEL_CALL,      // RPC_C_AUTHN_LEVEL_xxx
            Com::RPC_C_IMP_LEVEL_IMPERSONATE, // RPC_C_IMP_LEVEL_xxx
            None,                        // client identity
            Com::EOAC_NONE,              // proxy capabilities
        )
    }.expect("should set proxy blanket");

    // get Win32_Process object

    let mut wmi_object = None;
    let object_path = BSTR::from("Win32_Process");
    let ret = unsafe { svc.GetObject(&object_path,
                                     Wmi::WBEM_FLAG_RETURN_WBEM_COMPLETE.0 as _,
                                     None,
                                     Some(&mut wmi_object),
                                     None) };
    ret.expect("GetObject failure");
    let wmi_object: Wmi::IWbemClassObject = wmi_object
        .expect("WMI should find a Win32_Process object");

    // GetMethod("Create")

    let mut in_params_class: Option<Wmi::IWbemClassObject> = None;
    let mut out_params_class: Option<Wmi::IWbemClassObject> = None;

    
    let ret = unsafe { wmi_object.GetMethod(w!("Create"), 0,
                                            &mut in_params_class, &mut out_params_class) };
    ret.expect("GetMethod failure");

    // in params
    
    let in_params_class = in_params_class.unwrap();
    let mut in_params: Option<Wmi::IWbemClassObject> = None;
    let ret = unsafe { in_params_class.SpawnInstance(0) };
    let in_params = ret.expect("SpawnInstance should return an instance");

    //let var_command = VARIANT::from(BSTR::from("notepad.exe"));
    //let ret = in_params.Put(w!("CommandLine"), 0, &var_command, 0);

    // ExecMethod

    // FIXME out params

    let out_params_class = out_params_class.unwrap();


    Ok(())
}

@kennykerr
Copy link
Collaborator

I've expanded the WMI sample to cover ExecMethod here: #2794

@ydirson
Copy link
Author

ydirson commented Jan 15, 2024

I've expanded the WMI sample to cover ExecMethod here: #2794

Great, thanks, I've been able to replicate the ExecMethod("Create") call from there and to get its return value.

However, things are not so easy when in comes to my original API calls:

  • my GetMethod, SpawnInstance, even Putof my input parameter do succeed (I even checked thatPutwith a wrongly-named parameter raisesWBEM_E_NOT_FOUND`, though apparently putting a wrongly-typed variant does not raise any error?)
  • my ExecMethod still fails with WBEM_E_INVALID_METHOD_PARAMETERS

With AddSession as documented here and AFAICT declared here, I thought the following ought to work. Any clue where to look?

    let mut in_params_class: Option<Wmi::IWbemClassObject> = None;
    unsafe { xs_base.GetMethod(w!("AddSession"), 0,
                               &mut in_params_class, std::ptr::null_mut()) }?;
    let in_params_class = in_params_class.unwrap();
    // fill input params
    let in_params = unsafe { in_params_class.SpawnInstance(0) }?;
    let var_session_name = VARIANT::from("MySession");
    unsafe { in_params.Put(w!("Id"), 0, &var_session_name, 0) }?;

    // method call
    let mut out_params = None;
    eprintln!("calling ExecMethod");
    unsafe { svc.ExecMethod(
        &BSTR::from("CitrixXenStoreBase"),
        &BSTR::from("AddSession"),
        Default::default(),
        None,
        &in_params,
        Some(&mut out_params),
        None,
    ) }?;
    eprintln!("called ExecMethod");
    let out_params = out_params.unwrap();

@kennykerr
Copy link
Collaborator

Sorry, I don't know anything about "xen" - best of luck!

@ydirson
Copy link
Author

ydirson commented Jan 16, 2024

Well, my problem in itself is not really Xen-specific, it is centered around the use of the Windows APIs (and tooling). Here ExecMethod apparently tells me "Parameters provided for the method are not valid", and I suspect the call fails because I do not feed it the type of data it expects.
The whole WMI concept seems to be about introspection, so I guess there would be an API to look into this in_params_class returned by GetMethod and list the accepted parameters and their types, but I can't see how I'm supposed to look at it.
The MOF I pointed to shows the Id input parameter is defined as "string": [In, IDQualifier(0)]string Id - I guess my let var_session_name = VARIANT::from("MySession"); statement could really be the issue. I see the resulting variant has a type of 8 (VT_BSTR if I trust the doc, where it does seem to be the only string type), and would as a first step want to double-check that this is indeed what the WMI object expects. This issue that passing a VT_I4 value raises the exact same error forces me to wonder.

(Sorry that part may well be wandering off-topic, likely largely due to me being unfamiliar with the Windows habits) Is there somewhere a description of how WMI works, notably here which component does parameter type checking? My guess would be that it is "something in the Windows-provided stack" rather than the driver implementing the method, and surely there would be some way of getting that something to output some additional traces? I activated logging of WMI events as described here -- as much as I can, as:

  • when checking the "enable logging" setting as instructed I get notified that doing so "may lose events" (uh wtf did I have anything beforehand that may be lost now?)
  • I had to check that box and validate the dialog twice before I see the checkbox checked when opening the properties dialog again
  • I cannot make any sense of the "WMI events appear in the event window for WMI-Activity" statement in the instructions
  • even after I see a "Debug" trace under "WMI-Activity/Debug" (with empty contents, and which I guess must refer to the activation of this extra logging?) all I see are events under "WMI-Activity/Operational", where I do see only the same error returned by the API, and also see those that occurred before I activated logging

@ydirson
Copy link
Author

ydirson commented Jan 16, 2024

With some more poking around I understand that:

  • I was only manipulating the class object (the one named CitrixXenStoreBase, which I fetched using GetObject)
  • I have to use ExecQuery() instead (like wmi-rs does, in fact) to get the singleton object of that class
  • for some reason calling Get() on a class object hoping to get a documented property returns a VT_NULL variant, as opposed to WBEM_E_NOT_FOUND for properties its instance would not have, which led me astray for some time
  • while ExecMethod naturally needs the instance object, GetMethod indeed still must be called on the class object

It's indeed that my use case was a bit different from the one in the example. I was finally able to call that method and extract the output parameters.

@kennykerr
Copy link
Collaborator

Glad you got it working!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
question Further information is requested
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants