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

Add Wbemcli classes needed to query WMI #994

Closed
wants to merge 14 commits into from

Conversation

dbwiddis
Copy link
Contributor

@dbwiddis dbwiddis commented Aug 5, 2018

No description provided.

@dbwiddis dbwiddis force-pushed the wmi branch 2 times, most recently from 355b21e to 3afe6ac Compare August 5, 2018 18:44
Copy link
Member

@matthiasblaesing matthiasblaesing left a comment

Choose a reason for hiding this comment

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

I did a first pass - could you please have a look at the comments?

HRESULT CoSetProxyBlanket(Pointer pProxy, //
int dwAuthnSvc, //
int dwAuthzSvc, //
BSTR pServerPrincName, // OLECHAR
Copy link
Member

Choose a reason for hiding this comment

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

Beware! OLECHAR is not a BSTR. The parameter pServerPrincName is a OLECHAR array, so I'd bind this as a LPOLESTR.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

MS is evil! I saw typedef OLECHAR * BSTR and thought I was good. I only ever call that function with null so this never reared its ugly head for me... will fix.

*
* E_OUT_OF_MEMORY Out of memory.
*/
HRESULT CoInitializeSecurity(Pointer pSecDesc, NativeLong cAuthSvc, Pointer asAuthSvc, Pointer pReserved1,
Copy link
Member

Choose a reason for hiding this comment

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

pSecDec is a PSECURITY_DESCRIPTOR, so could WinNT.SECURITY_DESCRIPTOR be used here?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yep. Another case where I always used null and was lazy.

*
* E_OUT_OF_MEMORY Out of memory.
*/
HRESULT CoInitializeSecurity(Pointer pSecDesc, NativeLong cAuthSvc, Pointer asAuthSvc, Pointer pReserved1,
Copy link
Member

Choose a reason for hiding this comment

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

cAuthSvc is a LONG -- for the windows data types, microsoft says

LONG     A 32-bit signed integer. The range is  2147483648 through 2147483647 decimal.

So int should be a save binding.

*
* E_INVALIDARG One or more arguments is invalid.
*/
HRESULT CoSetProxyBlanket(Pointer pProxy, //
Copy link
Member

Choose a reason for hiding this comment

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

Should pProxy be Pointer or Unknown or IUnknown?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It's the pointed-to value of the last argument of IwbemLocator::ConnectServer which is a pointer to an IWbemServices object. In the code I use this Pointer in the IWbemServices initializer. Not sure what other mapping it should be.

}

public HRESULT Get(BSTR wszName, NativeLong lFlags, VARIANT.ByReference pVal, Pointer pvtType,
LongByReference plFlavor) {
Copy link
Member

Choose a reason for hiding this comment

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

According to the documents from microsoft this is the definition:

HRESULT Get(
  LPCWSTR wszName,
  long    lFlags,
  VARIANT *pVal,
  CIMTYPE *pType,
  long    *plFlavor
);

So I would use String fro wszName and wrap it into a WString for the call. Microsoft defines the sizes of the base types in:

https://msdn.microsoft.com/de-de/library/cc953fe1.aspx

So lFlags can be bound as int and plFlavor is a IntByReference. The LongByReference is to big. pType (it is not pvtType) is defined as:

typedef long CIMTYPE;

So it is not a pointer!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Wouldn't CIMTYPE *pType mean that pType is a pointer to CIMTYPE so a Pointer is valid here (IntByReference would be better as you note for long *plFlavor). I have read the value from this call using Pointer.getInt() and it has been the CIMTYPE.

super(pvInstance);
}

public HRESULT Next(NativeLong lTimeOut, NativeLong uCount, PointerByReference ppObjects,
Copy link
Member

Choose a reason for hiding this comment

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

Same comment as in IWbemClassObject applies - is it expected, that this method is called on a non windows platform? if not, NativeLong can be substituted with int.

// Connect to WMI through the IWbemLocator::ConnectServer method
// Connect to the namespace with the current user and obtain pointer
// pSvc to make IWbemServices calls.
HRESULT hres = loc.ConnectServer(new BSTR(namespace), null, null, null, null, null, null, pSvc);
Copy link
Member

Choose a reason for hiding this comment

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

Don't use the BSTR constructor. BSTR need to be allocated via OleAuto.INSTANCE.SysAllocString and they need to be freed with OleAuto.INSTANCE.SysFreeString.

}

public HRESULT ConnectServer(BSTR strNetworkResource, BSTR strUser, BSTR strPassword, BSTR strLocale,
NativeLong lSecurityFlags, BSTR strAuthority, Pointer pCtx, PointerByReference ppNamespace) {
Copy link
Member

Choose a reason for hiding this comment

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

You could use String instead of BSTR and allocate + release the BSTRs inside the function (see below for allocation of BSTR).

For pCtx should this really be a plain Pointer?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

For pCtx it would probably be an IWbemContext object. But in all the sample code I have seen on MS sites for WMI connections (both local and remote) it's been null. Reading the docs, it looks like IUnknown (or is there a ByReference version of it) may be correct?

/**
* Initializes COM library and sets security to impersonate the local user
*/
public static void initCOM() {
Copy link
Member

Choose a reason for hiding this comment

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

This looks dangerous if you combine it with other COM calling code.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I agree it looks dangerous but I'm not sure how else to handle it. I can ignore "already initialized" errors but then if I uninit later, it will kill the external program's COM code. If I don't uninit, it's a resource leak.

Copy link
Member

Choose a reason for hiding this comment

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

Make the user responsible for initializing COM. Else you need to guess how the user will use it. The COM Utilities have offer both options. COMThread handles the creation of a thread for dispatching COM Threads with correctly initialized COM. Factory uses that, while ObjectFactory allows creation of call proxies without a different thread.

I found it better to handle COM initialization myself, so I prefer to use the ObjectFactory.

There is a problem with your code - when calls to the utility methods are done from different threads, COM will only be initialized once. This violates the requirement, that CoInitializeEx must be called on each thread making COM calls.

Copy link
Contributor Author

@dbwiddis dbwiddis Aug 19, 2018

Choose a reason for hiding this comment

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

I've moved the COM initialization outside the Util class.

@dbwiddis
Copy link
Contributor Author

I think I've addressed all the comments except the type mapping for the last argument of ConnectServer. It works as I have it with a Pointer. I tried various combinations of Unknown and ByReference but ended up unable to access the multiple layers of pointers that PointerByReference allows.

* Exception encountered in this class
*/
@SuppressWarnings("serial")
class WbemcliException extends RuntimeException {
Copy link
Member

Choose a reason for hiding this comment

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

This is only used in the WbemcliUtil class, so I would move it there.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Actually it's used if CoCreateInstance() fails.

Copy link
Member

Choose a reason for hiding this comment

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

I see - thank you for the pointer, I really overlooked that case.

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 fair, there's an argument for just returning HRESULT and throwing the exception from the caller (in the Util method). Let me know if you'd prefer that.

Copy link
Member

Choose a reason for hiding this comment

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

COMBindingBaseObject uses COMUtils#checkRC which raises a COMException on error - that would align Wbemcli with other COM bindings.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good timing, I was actually just about to update this. Based on the above, I'll

  • return null from this method if it fails
  • handle null in the util class with a COMException along with other uses of this exception.

HRESULT hres = Ole32.INSTANCE.CoCreateInstance(CLSID_WbemLocator, null, WTypes.CLSCTX_INPROC_SERVER,
IID_IWbemLocator, pbr);
if (COMUtils.FAILED(hres)) {
Ole32.INSTANCE.CoUninitialize();
Copy link
Member

Choose a reason for hiding this comment

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

This looks like an artifact from the "handle COM initialization in the calls" approach. There is no reason to uninitialize COM here.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah, I had already removed that, somehow I committed it to a different branch. Same with the removed variables.

public Integer getInteger(T property, int index) {
Object o = this.propertyMap.get(property).get(index);
if (o == null) {
return 0;
Copy link
Member

Choose a reason for hiding this comment

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

I'm a bit irritated here - why return a primitive 0 instead of the java null? I would understand if you return int, but with a nullable type?

Copy link
Contributor Author

@dbwiddis dbwiddis Aug 19, 2018

Choose a reason for hiding this comment

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

That's a very good question, and I went back and forth on this implementation several times. My reasoning:

  • Requiring the user to null-check every return takes away some of the value of a utility class that provides sensible defaults (zero or empty string). The user still has the ability to test null (and even determine types if not-null) directly using getValue(). All the other getters are convenience methods, and I wanted them to be convenient.
  • The return type should be tied directly to the CIM type. getInteger() should only be used for uint16, sint32, and uint32 CIM types, all of which suggest a numeric value. The only time we run into an issue is if WMI has no value for that column, and we don't check the type.
  • I've also considered having different return types based on the CIM Type rather than the value type encoded in the Variant. For example, both String and uint64 return as a Variant BSTR, but the latter requires further parsing (to either long or possibly a BigInteger or WinNT.LARGE_INTEGER. All of which returns to the question of how to treat parse exceptions / nulls. I eventually settled on what you see as a happy medium.
  • If I had my druthers I'd probably use Java 8's Optional<Integer> here. But that's not an option (pun intended).
  • All these numeric values began their life as primitives, from VARIANT's various .*value() methods. They only got boxed when being added to the Lists. When I wrote this, I unboxed and returned primitives, and that's why I initially wrote the defaults.
  • In many cases in my application I would immediately re-box them to put into another list or map. So since the value was already boxed in the list, I felt returning it as it was (with a simple typecast) was the most direct option and changed it. So the code you see is a relic of the original return type but I still thought the defaults were more useful than null (since the user still has access to test null).

From the perspective of a user, if they want the default 0 it's just
result.getInteger(WmiProperty.WMIFIELD, 0)
If they want a different default:
result.getValue(WmiProperty.WMIFIELD, 0) == null ? -1 : result.getInteger(WmiProperty.WMIFIELD, 0)

If I were to return null instead of a guaranteed numeric it would require the user to always do:
result.getInteger(WmiProperty.WMIFIELD, 0) == null ? 0 : result.getInteger(WmiProperty.WMIFIELD, 0)

Runtime.getRuntime().addShutdownHook(new Thread() {
@Override
public void run() {
OleAuto.INSTANCE.SysFreeString(WQL);
Copy link
Member

Choose a reason for hiding this comment

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

I would not preallocated WQL and do the allocation in the query. At least I would measure if it really is a significant part of the call cost. This construction looks fragile.

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 allocation and release take an average of 33 microseconds for both calls. Assuming (generously) I could kill one of them that'd save 16 microseconds per invocation. So, yeah, not too significant. :)


// Track initialization of COM and Security
private static boolean comInitialized = false;
private static boolean securityInitialized = false;
Copy link
Member

Choose a reason for hiding this comment

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

These variables are not used anymore.

import com.sun.jna.ptr.PointerByReference;

/**
* This header is used by Remote Desktop Services.
Copy link
Member

Choose a reason for hiding this comment

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

?! Not sure what the header wants to tell me?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Well, I stole it from here. I guess I can make it more useful.

/**
* Increment the result count by one.
*/
public void incrementResultCount() {
Copy link
Member

Choose a reason for hiding this comment

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

add is private, this should also be private

OleAuto.INSTANCE.SysFreeString(queryStr);
OleAuto.INSTANCE.SysFreeString(wql);
if (COMUtils.FAILED(hres)) {
svc.Release();
Copy link
Member

Choose a reason for hiding this comment

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

why is svc released here? The value is passed in, so I would keep away from handling it.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It was released there in the sample code that I ported, although since I've moved out other resource releasing, may as well do that here, as well.

* A copy is also included in the downloadable source code package
* containing JNA, in file "AL2.0".
*/
package com.sun.jna.platform.win32;
Copy link
Member

Choose a reason for hiding this comment

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

These are COM bindings could you please move this and the Util class to the com.sun.jna.platform.win32.COM package?

@dbwiddis dbwiddis force-pushed the wmi branch 2 times, most recently from c5a6ff3 to 8b5c3a3 Compare August 26, 2018 04:59
@matthiasblaesing
Copy link
Member

I would be willing to merge - shall I?

@dbwiddis
Copy link
Contributor Author

Woohoo! Only one other change I was considering: The clsObj.Get() call is capable of returning the CIM type. This would allow for a more robust type checking by the user (e.g., better distinguish a String object with Variant type VT_BSTR between the various DATETIME, UINT64, and STRING, and other options: see left hand column of line 432 of the util class). While it's not really needed (the caller should already know the type from the WMI table documentation) I think it's useful to include.

Thanks as always for the educational code review and your patience.

@dbwiddis
Copy link
Contributor Author

OK, ready to merge! If you like my latest commit, include it. If not, back out the last commit and merge!

@matthiasblaesing
Copy link
Member

Hi Daniel, I had another look at this (sorry for the delay). Looking at the Wbem classes, they look a lot like JDBC and could be wrapped similarly, I did not pursue that further, as that might be overkill. However, I added a few changes, that you might want to consider:

https://github.com/matthiasblaesing/jna/commits/dbwiddis-wmi

Please have a look at the last three commits of the branch referenced above (If all went well, you should be able to pull directly from it). The first commit adds a bit binding sugar, the second moves WmiQuery more into a object oriented direction and the third fixes the unittests.

@dbwiddis
Copy link
Contributor Author

dbwiddis commented Sep 4, 2018

Looks great! I've added your commits to my PR. I submitted one final commit which removes an unused variable and unneeded cast, some unneeded javadoc params , and adds to the Wbemcli class javadocs to better match the MSDN documentation. (And my formatter spaces slightly different than yours.) Tests fine on my system, good to go here!

@matthiasblaesing
Copy link
Member

Thank you for your work - its appreciated. I added a slight update:

matthiasblaesing@68c8a96

I squashed the work into one feature commit and merged that to master.

@dbwiddis dbwiddis deleted the wmi branch February 18, 2019 02:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants