Tome's Land of IT

IT Notes from the Powertoe – Tome Tanasovski

Category Archives: csharp

Interprocess Communication (IPC) Between Asynchronous Events Triggered in C# to PowerShell

I played with Apache Zookeeper and PowerShell all weekend.  While I won’t dig into what I’ve been doing deeply in this article, I will tell you that the .NET interface requires a lot of asynch callbacks (event triggering).  Fortunately, I worked out a way to not only receive an event from C# (easy Bruce Payette taught us that years ago), but I also found a way to communicate variables into the runsapce of the action that gets executed when an event is triggered.

We’ll start with a simple C# class with an event to subscribe to.  In this case, I’m going to subscribe to the Changed event of my TestEvent.Watcher class.  This class has a single method which I can use to invoke the event with a string message attached to it.  This code should be invoked prior to running anything else in this post:

$code = @"
namespace TestEvent
{
    using System;

    public class Watcher
    {
        public delegate void ChangedEvent(object sender, WatcherEventArgs e);
        public event ChangedEvent Changed;

        public virtual void InvokeEvent(string message)
        {
            if (Changed != null) {
                Changed(this, new WatcherEventArgs(message));
            }
        }
    }
    public class WatcherEventArgs : EventArgs
    {
        public string message;
        public WatcherEventArgs(string message) {
            this.message = message;
        }
    }
}
"@
add-type -typedefinition $code

Here is the code to create the subscription and invoke the event. Register-ObjectEvent creates a job to run the event actions:

$watcher = new-object testevent.watcher

$job = Register-ObjectEvent -InputObject $watcher -EventName Changed -Action {
    $eventargs.message
}

$watcher.InvokeEvent('Triggering this message')

sleep 1 # just to ensure that the trigger happens
$job |receive-job

When we invoke, it looks like this:

PS C:\> .\test.ps1
Triggering this message

In Zookeeper, I needed to pass the current and valid connection object I had to the watcher so that when the event was triggered it would be sure to use the live object. Unfortunately, I didn’t see a way to pass arguments to the scriptblock, but I realized that I had full access to the $sender variable. In the above example,$sender is the testevent.watcher object I created and stored in the $watcher variable. Therefore, I figured I could just use add-member on $watcher to attach whatever objects I need to the action scriptblock.  These could then be accessed via $sender,

$watcher = new-object testevent.watcher
$watcher |add-member -NotePropertyName 'object' -NotePropertyValue 'value at watcher creation'

$job = Register-ObjectEvent -InputObject $watcher -EventName Changed -Action {
    $eventargs.message
    $sender.object
}

$watcher.InvokeEvent('Triggering this message')

sleep 1 # just to ensure that the trigger happens
$job |receive-job

The results of the above show that $sender.object indeed has the value I set when I registered the event.

PS C:\> .\test.ps1
Triggering this message
value at watcher creation

With that accomplished, I had to test whether or not the parent script could modify the $watcher object prior to the trigger and still set the value. This would enable me to pass live objects if they ever changed or needed to be updated by the parent script.

$watcher = new-object testevent.watcher
$watcher |add-member -NotePropertyName 'object' -NotePropertyValue 'value at watcher creation'

$job = Register-ObjectEvent -InputObject $watcher -EventName Changed -Action {
    $eventargs.message
    $sender.object
}

$watcher.object = 'value at watcher invokation'
$watcher.InvokeEvent('Triggering this message')

sleep 1 # just to ensure that the trigger happens
$job |receive-job

As you can see, success! The object is updated and passed at the time of the trigger rather than at the time of registration.

C:\> .\test.ps1
Triggering this message
value at watcher invokation

In case your interested, communication from the triggered job to the parent (the other half of IPC) is very easy. Simply use the messages/objects from the output of the scriptblock itself. In other words, your parent script will need to call receive-job and deal with the messages/output accordingly.

UPDATE

The above technique is sound and still useful for controlling messages to an event trigger.  It’s a very clean technique to use the $sender object.  However, I realized further in my testing and playing that it’s not always necessary.  Actions have access to GLOBAL and SCRIPT scope variable.  Not only can they access those variables on demand, but they can set them too.  This winds up being an even easier medium for transferring state because it’s actually just updating the live state.

Invoking the GetDiskSpaceInformation method of the IOfflineFilesCache COM interface with C# and PowerShell

This article could also be entitled, “Using an Inproc COM server in C# and PowerShell”.

Part I of this series shows how to invoke the GetDiskSpaceInformation method of the IOfflineFilesCache COM interface via C++. This was accomplished after failing miserably at trying to get it to work with C# and PowerShell. However, after I solved the problem in C++ I understood exactly how it worked and was able to ask better questions in Google to find out how to do the same in C#.

The challenge

First, to explain why it’s hard. When I first looked at the docs, I thought this would be easy. In PowerShell, a COM object can be invoked by instantiating a com object with new-object.  For example:

$app = new-object -comobject excel.application

Even if that didn’t work, I knew that often times C++ code could be Marshaled to and from the world of C# and managed code with a bit of tinkering. This is usually where you see pinvoke.net and code that leverages add-type.  However, in this case, the libraries do not exist in pinvoke.  Basically, because this is really COM, you cannot do this.  Also, because there are no tlb files associated, you cannot easily just use the interfaces like they are COM.

Just to be clear as to why this is the case:  This is a new interface.  It was plugged in by the Windows developers into the latest versions of Windows.  It’s implemented in COM so that other languages can get access to it.  However, it’s not fully at the point where it needs to be flushed into general use.  I expect that in the years to come, we’ll see these interfaces exposed with a tlb and eventually there may even be a PowerShell module that is used to manage the offline files cache directly.  However, if you want access before that day comes, you need to get crafty.

Finding GUIDs OLE/COM Object Viewer

The key to invoking this code from C# is to know the GUIDs that were used for both the CLSID and the interface we are trying to get access to.  This can be accomplished by running the OLE/COM Object Viewer.  For me, this was installed with my Visual Studio 2013 and can be found here: C:\Program Files (x86)\Windows Kits\8.1\bin\x86\oleview.exe.

Once in the GUI, you can browse to Object Classes->All Objects->Offline Files Cache Control

olecomviewer1

We’re looking for the GUID: 48C6BE7C-3871-43CC-B46F-1449A1BB2FF3

Next, if you double-click on that you’ll see the interfaces.  In our case, we want the IOfflineFilesCache interface.

olecomviewer2The GUID is 855D6203-7914-48B9-8D40-4C56F5ACFFC5

It should be noted that these GUIDs are static.  You do not need to run the COM viewer on your desktop if you are invoking the exact same interface that I am demonstrating.  The GUIDs are the same on ever computer.  However, this is here to show the generic steps that are needed to invoke the methods on one of these no-TLB COM interfaces.

CreateInstance

The first step is to create an instance of the CLSID using the GUID we found above.

Guid ID = new Guid("48C6BE7C-3871-43cc-B46F-1449A1BB2FF3");
Type idtype = Type.GetTypeFromCLSID(ID);
IOfflineFilesCache obj = (IOfflineFilesCache) Activator.CreateInstance(idtype, true);

It should be noted that the code we are using requires:

using System.Runtime.InteropServices;

Unfortunately, in the above bit of code, we are referencing the IOfflineFilesCache type, but it does not yet exist anywhere.  Therefore, we have to help C# know what this is by creating an interface with the ComImport attribute

Interface Attributes

I should note that everything I’m demonstrating is documented here.  However, it’s a bit cludgy to get through.  Also, there are a few key elements it leaves out.  Specifically, it neglects to inform you that when you create the interface, you must implement all of the methods that exist in the interface in exact order leading up to the method you care about using.  The best way to get the methods and the order they are implemented is to read the C++ header file.  This was one of the #include files I showed in part I of this article.  Specifically, you need to view cscobj.h.  A quick search of your hard drive should find it in an SDK folder.  Once you have read through this, you can create the interface in the proper order:

    [ComImport]
    [Guid("855D6203-7914-48B9-8D40-4C56F5ACFFC5"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
    interface IOfflineFilesCache
    {
        [PreserveSig()]
        int Synchronize();

        [PreserveSig()]
        int DeleteItems();

        [PreserveSig()]
        int DeleteItemsForUser();

        [PreserveSig()]
        int Pin();

        [PreserveSig()]
        int UnPin();

        [PreserveSig()]
        int GetEncryptionStatus();

        [PreserveSig()]
        int Encrypt();

        [PreserveSig()]
        int FindItem();

        [PreserveSig()]
        int FindItemEx();

        [PreserveSig()]
        int RenameItem();

        [PreserveSig()]
        int GetLocation();

        [PreserveSig()]
        int GetDiskSpaceInformation(ref ulong pcbVolumeTotal, ref ulong pcbLimit, ref ulong pcbUsed, ref ulong pcbUnpinnedLimit, ref ulong pcbUnpinnedUsed);

        // only need to go as far as the function you need, but rest here for completeness

        [PreserveSig()]
        int SetDiskSpaceLimits();

        [PreserveSig()]
        int ProcessAdminPinPolicy();

        [PreserveSig()]
        int GetSettingObject();

        [PreserveSig()]
        int EnumSettiingObjects();

        [PreserveSig()]
        int IsPathCacheable();
    }

You’ll notice that in the above, the GUID is the GUID we found when looking at the interface in the OLE/COM viewer.

Finished code, but let’s do it in PowerShell

So, now that we have the interface and an instance of it, the rest is easy.  The following final bit of code injects the C# into PowerShell via add-type.  I simply return the results as a collection and then convert it into an object in PowerShell, but you could just as easily modify the code to have the object returned directly from C#.

$code = @'
using System;
using System.Runtime.InteropServices;

public class offlinecache {
    public static ulong[] GetOfflineCache() {
        ulong pcbVolumeTotal=0, pcbLimit=0, pcbUsed=0, pcbUnpinnedLimit=0, pcbUnpinnedUsed=0;
        Guid ID = new Guid("48C6BE7C-3871-43cc-B46F-1449A1BB2FF3");
        Type idtype = Type.GetTypeFromCLSID(ID);
        IOfflineFilesCache obj = (IOfflineFilesCache) Activator.CreateInstance(idtype, true);
        int i = obj.GetDiskSpaceInformation(ref pcbVolumeTotal, ref pcbLimit, ref pcbUsed, ref pcbUnpinnedLimit, ref pcbUnpinnedUsed);
        ulong[] output = new ulong[5];
        output[0] = pcbVolumeTotal;
        output[1] = pcbLimit;
        output[2] = pcbUsed;
        output[3] = pcbUnpinnedLimit;
        output[4] = pcbUnpinnedUsed;
        return output;
    }

    [ComImport]
    [Guid("855D6203-7914-48B9-8D40-4C56F5ACFFC5"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
    interface IOfflineFilesCache
    {
        [PreserveSig()]
        int Synchronize();

        [PreserveSig()]
        int DeleteItems();

        [PreserveSig()]
        int DeleteItemsForUser();

        [PreserveSig()]
        int Pin();

        [PreserveSig()]
        int UnPin();

        [PreserveSig()]
        int GetEncryptionStatus();

        [PreserveSig()]
        int Encrypt();

        [PreserveSig()]
        int FindItem();

        [PreserveSig()]
        int FindItemEx();

        [PreserveSig()]
        int RenameItem();

        [PreserveSig()]
        int GetLocation();

        [PreserveSig()]
        int GetDiskSpaceInformation(ref ulong pcbVolumeTotal, ref ulong pcbLimit, ref ulong pcbUsed, ref ulong pcbUnpinnedLimit, ref ulong pcbUnpinnedUsed);

        // only need to go as far as the function you need, but rest here for completeness

        [PreserveSig()]
        int SetDiskSpaceLimits();

        [PreserveSig()]
        int ProcessAdminPinPolicy();

        [PreserveSig()]
        int GetSettingObject();

        [PreserveSig()]
        int EnumSettiingObjects();

        [PreserveSig()]
        int IsPathCacheable();
    }
}
'@

add-type -TypeDefinition $code

$output = ([offlinecache]::GetOfflineCache()) 
new-object psobject -Property ([ordered] @{
    VolumeTotal = $output[0]
    Limit = $output[1]
    Used = $output[2]
    UnpinnedLimit = $output[3]
    UnpinnedUsed = $output[4]
})

Here’s a trail of it running:

06:50:06 PS C:\Dropbox\scripts> .\GetOfflineCacheDiskInfo.ps1


VolumeTotal   : 469636214784
Limit         : 109076041728
Used          : 0
UnpinnedLimit : 109076041728
UnpinnedUsed  : 0
%d bloggers like this: