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

Code OK on V5.1 does not run on V7.0.0RC2 #11782

Closed
RG255 opened this issue Feb 5, 2020 · 10 comments · Fixed by #11795
Closed

Code OK on V5.1 does not run on V7.0.0RC2 #11782

RG255 opened this issue Feb 5, 2020 · 10 comments · Fixed by #11795
Assignees
Labels
Resolution-Fixed The issue is fixed. WG-Engine core PowerShell engine, interpreter, and runtime
Milestone

Comments

@RG255
Copy link

RG255 commented Feb 5, 2020

Name                           Value
----                           -----
PSVersion                      7.0.0-rc.2
PSEdition                      Core
GitCommitId                    7.0.0-rc.2
OS                             Microsoft Windows 10.0.18363
Platform                       Win32NT
PSCompatibleVersions           {1.0, 2.0, 3.0, 4.0…}
PSRemotingProtocolVersion      2.3
SerializationVersion           1.1.0.1
WSManStackVersion              3.0

The code in the attached script runs OK on V5 but generates the following error on V7:

MethodInvocationException: S:\PowerShellScripts\TestUserIngroup_1.ps1:5
Line |
   5 |  … t -Process {$_.GetType().InvokeMember('Name', 'GetProperty', $null, $ …
     |                ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
     | Exception calling "InvokeMember" with "5" argument(s): "Unknown name. (0x80020006 (DISP_E_UNKNOWNNAME))"

The script:

$user = $env:USERNAME
$group = 'Users'
$groupObj = [ADSI]"WinNT://./$group,group"
$membersObj = @($groupObj.psbase.Invoke('Members'))
$members = ($membersObj | ForEach-Object -Process {$_.GetType().InvokeMember('Name', 'GetProperty', $null, $_, $null)})

If ($members -contains $user) 
{Write-Host -Object "$user exists in the group $group"}
Else 
{Write-Host -Object "$user does not exists in the group $group"}

TestUserIngroup_1.zip

@RG255 RG255 added the Issue-Question ideally support can be provided via other mechanisms, but sometimes folks do open an issue to get a label Feb 5, 2020
@iSazonov
Copy link
Collaborator

iSazonov commented Feb 6, 2020

@SeeminglyScience Have you any thoughts?

@RG255
Copy link
Author

RG255 commented Feb 6, 2020

I am somewhat new to GitHub so I am not sure what you are expecting when you say "Have you any thoughts" My only thought at this time would be expecting it to work on PSV7* or there to be a better/alternative way to achieve the same result?

@iSazonov
Copy link
Collaborator

iSazonov commented Feb 6, 2020

@RG255 References like "@RG255" means contacting the owner of this name. So my question was for @SeeminglyScience who could share useful thoughts about the issue.
You could look other issues/discussion in the repo to get more experience about workflows.

@SeeminglyScience
Copy link
Collaborator

@iSazonov Looks like PowerShell isn't able to determine that IADsMembers (the collection returned by Members) is enumerable. So instead of $membersObj enumerating into member objects like in Windows PowerShell, the attempt is made to call a Name property on the IADsMembers interface which doesn't exist.

You can kinda see this by attempting to get the property Count instead, which will throw not implemented instead of unknown name.

@SeeminglyScience
Copy link
Collaborator

So playing with it a bit, my guess is that this happens because if you query IDispatch.GetIDsOfNames with the names _NewEnum and ppEnumerator then that exception is thrown. However, if you manually invoke GetIDsOfNames with a preserved sig (no automatic HResult handling) you'll see the HResult above returned, but it'll also show DISPID_NEWENUM for _NewEnum. If _NewEnum was really not found, it would be -1.

Not sure atm where this needs to be fixed though.

Tested with this code, click to expand
Add-Type -TypeDefinition '
    using System;
    using System.Runtime.InteropServices;
    using System.Runtime.InteropServices.ComTypes;

    namespace Testing
    {
        public class DispatchWrapper
        {
            private readonly IDispatch _real;

            internal DispatchWrapper(IDispatch real) => _real = real;

            public static DispatchWrapper Create(object obj) => new DispatchWrapper((IDispatch)obj);

            public int GetTypeInfoCount(out int info) => _real.GetTypeInfoCount(out info);

            public int GetTypeInfo(int iTInfo, int lcid, out ITypeInfo ppTInfo) => _real.GetTypeInfo(iTInfo, lcid, out ppTInfo);

            public int GetIDsOfNames(Guid iid, string[] rgszNames, int cNames, int lcid, int[] rgDispId)
                => _real.GetIDsOfNames(iid, rgszNames, cNames, lcid, rgDispId);

            public void Invoke(
                int dispIdMember,
                Guid iid,
                int lcid,
                INVOKEKIND wFlags,
                DISPPARAMS[] paramArray,
                out object pVarResult,
                out EXCEPINFO pExcepInfo,
                out uint puArgErr)
                => _real.Invoke(
                    dispIdMember,
                    iid,
                    lcid,
                    wFlags,
                    paramArray,
                    out pVarResult,
                    out pExcepInfo,
                    out puArgErr);
        }

        [Guid("00020400-0000-0000-c000-000000000046")]
        [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
        [ComImport]
        internal interface IDispatch
        {
            [PreserveSig]
            int GetTypeInfoCount(out int info);

            [PreserveSig]
            int GetTypeInfo(int iTInfo, int lcid, out ITypeInfo ppTInfo);

            [PreserveSig]
            int GetIDsOfNames(
                [MarshalAs(UnmanagedType.LPStruct)] Guid iid,
                [MarshalAs(UnmanagedType.LPArray, ArraySubType = UnmanagedType.LPWStr)] string[] rgszNames,
                int cNames,
                int lcid,
                [MarshalAs(UnmanagedType.LPArray, ArraySubType = UnmanagedType.I4)] [Out] int[] rgDispId);

            void Invoke(
                int dispIdMember,
                [MarshalAs(UnmanagedType.LPStruct)] Guid iid,
                int lcid,
                INVOKEKIND wFlags,
                [MarshalAs(UnmanagedType.LPArray)] [In] [Out] DISPPARAMS[] paramArray,
                out object pVarResult,
                out EXCEPINFO pExcepInfo,
                out uint puArgErr);
        }
    }'

$group = 'Users'
$groupObj = [ADSI]"WinNT://./$group,group"
$membersObj = @($groupObj.psbase.Invoke('Members'))
$disp = [Testing.DispatchWrapper]::Create($membersObj[0])
$nullGuid = [activator]::CreateInstance([guid])
$enUS = [cultureinfo]::CurrentCulture.LCID
$namesToQuery = '_NewEnum', 'ppEnumerator'
$dispIds = [int[]]::new($namesToQuery.Length)
$hr = $disp.GetIDsOfNames($nullGuid, $namesToQuery, $namesToQuery.Length, $enUS, $dispIds)

'HResult: 0x{0:X}' -f $hr
for ($i = 0; $i -lt $namesToQuery.Length; $i++) {
    '{0} ID: {1}' -f $namesToQuery[$i], $dispIds[$i]
}

Should return:

HResult: 0x80020006
DispIDs: -4, -1

@SeeminglyScience
Copy link
Collaborator

SeeminglyScience commented Feb 6, 2020

Another update, here's the code that needs to change:

var comTypeInfo = ComTypeInfo.GetDispatchTypeInfo(comObject);
if (comTypeInfo != null && comTypeInfo.NewEnumInvokeKind.HasValue)
{
// The COM object is a collection and also a IDispatch interface, so we try to get a
// IEnumVARIANT interface out of it by invoking its '_NewEnum (DispId: -4)' function.
var result = ComInvoker.Invoke(target, ComTypeInfo.DISPID_NEWENUM,
args: Array.Empty<object>(), byRef: null,
invokeKind: comTypeInfo.NewEnumInvokeKind.Value);
enumVariant = result as COM.IEnumVARIANT;
if (enumVariant != null)
{
return new ComEnumerator(enumVariant);
}
}

It fails because IADsMembers implements IDispatch, but throws E_NOTIMPL when trying to get type info. The other members work fine though.

Instead of (or in addition to) querying type info, it needs to directly invoke GetIDsOfNames with two names. The first has to be _NewEnum and the second can apparently be anything. It needs two though, otherwise it returns unknown. If the first ID is DISPID_NEWENUM then _NewEnum can be invoked. The GetIDsOfNames implementation needs PreserveSig and it's return type changed to int. If DISPID_NEWENUM is found and the HResult is DISP_E_UNKNOWNNAME, then the HResult should be ignored.

Example invoke using the above code in the details pane:

$results = $null
$excep = [Activator]::CreateInstance([Runtime.InteropServices.ComTypes.EXCEPINFO])
$argErr = 0u;
$disp.Invoke(-4, $nullGuid, $enUS, 'INVOKE_PROPERTYGET', @(), [ref] $res, [ref] $excep, [ref] $argErr)

# yield
$results

Which returns:

System.__ComObject
System.__ComObject
System.__ComObject
System.__ComObject
System.__ComObject
System.__ComObject

@SeeminglyScience
Copy link
Collaborator

/cc @SteveL-MSFT I'm pretty sure I've seen quite a few folks on with similar issues on the PS slack. This might be the first issue for it here, but it's potentially very impactful for folks still using COM objects frequently (and who aren't super likely to be vocal here). If possible, it may be worth resolving before 7.0 GA.

@iSazonov
Copy link
Collaborator

iSazonov commented Feb 6, 2020

@SeeminglyScience Thanks for your investigations!
I think we need to have more tests for COM interactions.

@iSazonov iSazonov added WG-Engine core PowerShell engine, interpreter, and runtime Issue-Enhancement the issue is more of a feature request than a bug and removed Issue-Question ideally support can be provided via other mechanisms, but sometimes folks do open an issue to get a labels Feb 6, 2020
@SteveL-MSFT SteveL-MSFT added this to the 7.0-Consider milestone Feb 6, 2020
@daxian-dbw daxian-dbw self-assigned this Feb 6, 2020
@daxian-dbw
Copy link
Member

daxian-dbw commented Feb 7, 2020

Thanks @SeeminglyScience for analyzing the issue and pinpoint the cause ❤️

The ComEnumerator was introduced back in early .NET Core 2.0 preview period of time (#4553), because GetEnumerator() didn't work on COM object even if the object can be cast to IEnumerable and .NET Core team said it was by design at the time (dotnet/runtime#21690).
With .NET Core 3.1, GetEnumerator() works on the COM objects that can be cast to IEnumerable 🎉

However, for the COM object that can be cast to IEnumerator, exception is thrown when calling MoveNext() on it.
So we still need the ComEnumerator, but much simplified to just cover the case where the COM object implements COM.IEnumVARIANT interface.

PR was submitted: #11795

@daxian-dbw daxian-dbw removed the Issue-Enhancement the issue is more of a feature request than a bug label Feb 7, 2020
@iSazonov iSazonov added the Resolution-Fixed The issue is fixed. label Feb 11, 2020
@ghost
Copy link

ghost commented Feb 21, 2020

🎉This issue was addressed in #11795, which has now been successfully released as v7.0.0-rc.3.:tada:

Handy links:

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Resolution-Fixed The issue is fixed. WG-Engine core PowerShell engine, interpreter, and runtime
Projects
None yet
Development

Successfully merging a pull request may close this issue.

5 participants