A closer look at Windows PowerShell OneGet - Part 2

In part one, we introduced the OneGet architecture and touched on providers, the components responsible for doing all the heavy lifting. Before we can dive into providers, however, we need to talk about OneGet plugins.

The OneGet Core API supports a simple plugin model that prevents coupling by way of Duck typing. That is, plugins don't reference any OneGet specific types or bind to any assemblies on disk. Instead, they implement a lightweight container encapsulating a few methods and properties that will be called and discovered at runtime, ultimately exposing any providers hosted within.

To get a better handle on what that means, let's look at a simple plugin hosting a single package provider:

public class Plugin
{
private PackageProvider _packageProvider;

public void InitPlugin(Func<string,
IEnumerable<object>, object> callback)
{
this._packageProvider = new PackageProvider();
}

public string PluginName
{
get
{
return "Sample";
}
}

public IEnumerable<string> PackageProviderNames
{
get
{
yield return "Sample";
}
}

public object CreatePackageProvider(string name)
{
return this._packageProvider;
}

public void DisposePlugin()
{
}
}
  • InitPlugin offers an opportunity to handle any internal initialization required. In this case, we create an instance of our single package provider class. We'll cover that class and that odd-looking callback parameter in a bit.

  • PluginName is used for simple book-keeping and logging.

  • PackageProviderNames lets OneGet know of any providers hosted by this plugin. In this case, we return a single provider named "Sample".

  • CreatePackageProvider is called once per provider listed by PackageProviderNames. These calls provide an opportunity to create package providers on-demand if needed.

  • DisposePlugin offers an opportunity to close or release any resources before plugin tear down.

Package providers bear the brunt of OneGet work and the required API surface reflects that. With the preview bits, providers are expected to implement about 14 methods.

Roll up your sleeves, here we go:

using Callback = Func<string, IEnumerable<object>, object>;

public class PackageProvider
{
public void AddPackageSource(string name, string location,
bool trusted, Callback c)
{
}

public bool FindPackage(string name, string requiredVersion,
string minimumVersion, string maximumVersion, Callback c)
{
return false;
}

internal bool FindPackageByFile(string filePath, Callback c)
{
return false;
}

public void GetInstallationOptionDefinitions(Callback c)
{
}

public bool GetInstalledPackages(string name, Callback c)
{
return false;
}

public void GetMetadataDefinitions(Callback c)
{
}

public bool GetPackageSources(Callback c)
{
return false;
}

public string GetProviderName(Callback c)
{
return "";
}

public bool InstallPackageByFastpath(string fastPath, Callback c)
{
return false;
}

public bool InstallPackageByFile(string filePath, Callback c)
{
return false;
}

public bool IsTrustedPackageSource(string packageSource, Callback c)
{
return false;
}

public bool IsValidPackageSource(string packageSource, Callback c)
{
return false;
}

public void RemovePackageSource(string name, Callback c)
{
}

public bool UninstallPackage(string fastPath, Callback c)
{
return false;
}
}
  • GetProviderName is simply a unique name for the provider.
  • FindPackage and FindPackageByFile handle the discovery of packages using the provider's own sources. A special fast path package identifier is returned for each package found for use during installation (see InstallPackageByFastpath).
  • InstallPackageByFastpath and InstallPackageByFile handle package installation using either a special fast path package identifier or by a manually specified file path. UninstallPackage simply removes packages by fast path.
  • AddPackageSource, GetPackageSources, GetPackageSources, and RemovePackageSource allow for package source management. Generally speaking, a package source is a location where packages can be found, identified by a Uri (e.g. http://my.host.com/packages). These locations can be flagged as trusted, skipping installation time confirmations.
  • IsTrustedPackageSource handles checking if a specified source is trusted or not. It's up to the provider to define trust.
  • IsValidPackageSource handles checking if a specified source is valid. It's up to the provider to define validity. This is used in the case where the host is given a specific package source for package installation. IsValidPackageSource will potentially identify the source as invalid, saving an expensive call to FindPackage.
  • GetInstallationOptionDefinitions and GetMetadataDefinitions allow for the specification of additional installation switches that bubble up to the host.

So let's talk about this Callback object that's passed into every method.

To allow plugins to call back into OneGet core API without a tight coupling between components (i.e. duck typing), plugins are offered a callback object. You can think of this object as a telephone handset on a very long cable, passed to each provider method, allowing the provider to call back into OneGet and talk to its core APIs.

Under the covers, the Callback object is simply a fancy delegate of type Func[string FunctionName, IEnumerable[object] Parameters, object Return]. (I had to use square brackets to avoid a bug in Squarespace at this time.) It's mainly used to retrieve delegates pointing to OneGet core APIs.

For example:

  • Callback(FunctionName, null) returns a Delegate to FunctionName if the function exists.
  • Callback(FunctionName, Parameters) actually invokes FunctionName. This pattern could be used to assert that FunctionName should always exist.

But rather than have a bunch of API resolution gunk in each provider method, I recommend you move that responsibility to a dispatcher of sorts.

Here's an example of a simple implementation:

public class Dispatcher
{
private Func<string, IEnumerable<object>, object> _callback;

public Dispatcher(
Func<string, IEnumerable<object>, object> callback)
{
if (callback == null)
throw new ArgumentNullException("callback");

_callback = callback;
}

private T GetRemoteFunction<T>() where T : class
{
var type = typeof(T);

var genericDelegate =
(Delegate)_callback(
type.Name.Remove(type.Name.Length - 2), null);

return Delegate.CreateDelegate(type,
genericDelegate.Target, genericDelegate.Method, true) as T;
}

// [...]

private delegate void AskPermissionFn(String permission);
public void AskPermission(String permission)
{
var fn = this.GetRemoteFunction<AskPermissionFn>();
fn(permission);
}

// [...]
}

This Dispatcher implementation could then be leveraged in a provider implementation as such:

// [...]

public void RemovePackageSource(string name, Callback c)
{
var dispatcher = new Dispatcher(c);
dispatcher.AskPermission("Are you sure?");

// [...]
}

In Part 3, we'll complete the provider implementation and cover some debugging tips/gotchas.

Feel free to stop by and say hello at one of the OneGet Community Meetings that happen every Friday at 10:00AM PDT over Lync. Everyone is welcome.