February 2008 - Posts

Back in manageability land. On TechEd EMEA Developers 2007 I delivered a talk on "Next-generation manageability: Windows PowerShell and MMC 3.0" covering the concept of layering graphical management tools (in this case MMC 3.0) on top of Windows PowerShell cmdlets (and providers). In this post, I'll cover this principle by means of a sample.

 

Introduction

It should be clear by now that Windows PowerShell is at the core of the next-generation manageability platform for Windows. First-class objects, ranging from .NET over COM to everything the Extended Type System can deal with (XML, ADSI, etc), together with scripting support allow people to automate complicated management tasks (combined with the v2.0 features on remoting and eventing this will only get better). Part of this vision is to layer management UIs on top of Windows PowerShell, which opens the door to broader discoverability: explore functionality in the UI, manage it over there and learn how the task would be done through the PowerShell CLI (command-line interface) directly, possibly wrapping it in a script for reuse in automation scenarios. On the development side this is also very appealing due to the fact the UI is a thin layer on top of the underlying cmdlet-based implementation which allows for better testing.

To lay the foundation for this post, please make sure to read the following tutorials:

We'll combine the two in a solution to create a sample layered management sample.

 

Step 0 - Solution plumbing

While thinking about this post I was wondering what to use as the running sample. Task managers layered on get-process are boring, similar for a Service Manager snap-in on top of get-service. Creating providers is too much to address in one post (my sample on TechEd created a provider to talk to a SQL database, allowing to cd to a table and dir it, exposing all of this to an MMC snap-in that hosted a Windows Forms DataGrid control). So I came up with the idea of writing a Tiny IIS Manager targeting IIS 7. This post assumes you've installed IIS 7 locally on your Windows Vista or Windows Server 2008 machine.

Before you start, make sure to run Visual Studio 2008 as administrator since we're going to launch Windows PowerShell loading a snap-in that requires administrative privileges.

Create a new solution called TinyIisManager:

image

Add two class library projects, one called TinyIisPS and another called TinyIisMMC. To configure the projects, follow my tutorials mentioned above:

Step 0.0 - Add the required references to the projects

This is how the end-result should look like:

image

Step 0.1 - Tweak the Debugger settings under the project properties

Again just the results (click to enlarge):

image image

Note: Make sure the paths to MMC and PS are set correctly on your machine. These settings won't work yet since we're missing the debug.* files (see below).

Step 0.2 - Add empty place-holders for the snap-ins (both PS and MMC)

Almost trivial to do if you've read the cookbook posts. Rename Class1.cs for the PS library into IisMgr.cs and add the following code:

image

Rename Class1.cs for the MMC library into IisMgr.cs and add the following code:

image

Step 0.3 - Build and register

Build both projects, open a Visual Studio 2008 Command Prompt running as administrator and cd into the bin\Debug folders for both projects to run installutil.exe against the created assemblies:

image

Step 0.4 - Creating debugging files

Open Windows PowerShell, add the registered snap-in and export the console file to debug.psc1 under the TinyIisPS project root folder:

image

Open MMC, add the registered snap-in (CTRL-M) and save the file as debug.msc under the TinyIisMMC project root folder:

image image

Don't worry about the empty node in the Selected snap-ins display - our constructor didn't set the node text (yet). Don't forget to close both MMC and Windows PowerShell.

Step 0.5 - Validate debugging

You should now be able to right-click any of the two projects and choose "Debug, Start new instance" to start a debugging session. Validate this is right: the MMC snap-in should load and the PS snap-in should be available:

image image

You're now all set to start the coding.

 

Step 1 - Building the Windows PowerShell layer

Let's start at the bottom of the design: the Windows PowerShell layer that will do all the real work. To keep things simple, we'll just provide a few cmdlets although bigger systems would benefit from providers too (so that you can navigate through a (optionally hierarchical) data store, e.g. to cd into virtual folder in an IIS website). We'll write just three cmdlets:

  • get-site - retrieves a list of sites on the local IIS 7 web server
  • start-site - starts a site
  • stop-site - stops a site

Feel free to envision other cmdlets of course :-). The API we'll use to talk to IIS is the new Microsoft.Web.Administration of IIS 7 which can be found under %windir%\system32\inetsrv, so let's import it (make sure you're under the right project: TinyIisPS):

image

Import the namespace Microsoft.Web.Administration to IisMgr.cs and add the following cmdlet classes (for simplicity I stick them in the same file - not recommended for manageability of your source tree :-)):

[Cmdlet(VerbsCommon.Get, "site")]
public class GetSiteCmdlet : Cmdlet
{
    protected override void ProcessRecord()
    {
        using (ServerManager mgr = new ServerManager())
        {
            WriteObject(mgr.Sites, true);
        }           
    }
}

public abstract class ManageSiteCmdlet : Cmdlet
{
    protected ServerManager _manager;

    [Parameter(Mandatory = true, Position = 1, ValueFromPipelineByPropertyName = true)]
    public string Name { get; set; }

    protected override void BeginProcessing()
    {
        _manager = new ServerManager();
    }

    protected override void EndProcessing()
    {
        if (_manager != null)
            _manager.Dispose();
    }

    protected override void StopProcessing()
    {
        if (_manager != null)
            _manager.Dispose();
    }
}

[Cmdlet(VerbsLifecycle.Start, "site", SupportsShouldProcess = true)]
public class StartSiteCmdlet : ManageSiteCmdlet
{
    protected override void ProcessRecord()
    {
        Site site = _manager.Sites[ Name ];

        if (site == null)
        {
            WriteError(new ErrorRecord(new InvalidOperationException("Site not found."), "404", ErrorCategory.ObjectNotFound, null));
        }
        else if (site.State == ObjectState.Started || site.State == ObjectState.Starting)
        {
            WriteWarning("Can't start site.");
        }
        else if (ShouldProcess(site.Name, "Start"))
        {
            site.Start();
        }
    }
}

[Cmdlet(VerbsLifecycle.Stop, "site", SupportsShouldProcess = true)]
public class StopSiteCmdlet : ManageSiteCmdlet
{
    protected override void ProcessRecord()
    {
        Site site = _manager.Sites[ Name ];

        if (site == null)
        {
            WriteError(new ErrorRecord(new InvalidOperationException("Site not found."), "404", ErrorCategory.ObjectNotFound, null));
        }
        else if (site.State == ObjectState.Stopped || site.State == ObjectState.Stopping)
        {
            WriteWarning("Can't stop site.");
        }
        else if (ShouldProcess(site.Name, "Stop"))
        {
            site.Stop();
        }
    }
}

Just 80 lines of true power. Time for a quick check of the functionality. Run the TinyIisPS project under the debugger and play around a little with the cmdlets:

image

If you see messages like the one below, check you're running Visual Studio 2008 as an administrator which will fork the child Windows PowerShell debuggee process as administrator too:

image 

 

Step 2 - Building the graphical MMC layer on top of the cmdlets

Time to bump up our TinyIisMMC project. The first thing to do is to add a reference to the System.Management.Automation.dll assembly (the one used in the PS project to write the cmdlets) since we need to access the Runspace functionality in order to host Windows PowerShell in the context of our MMC snap-in:

image

Also add references to System.Windows.Forms (needed for some display) and Microsoft.Web.Administration (see instructions above - similar as in the PowerShell layer). We'll need this in order to use the objects returned by the PowerShell get-site cmdlet. Time to start coding again. Basically an MMC snap-in consists of:

  • The SnapIn class which acts as the root of the hierarchy; it adds nodes to its tree;
  • A tree of ScopeNode instances which get displayed in the tree-view;
  • Actions associated with the nodes;
  • View descriptions to render a node in the central pane.

We'll keep things simple and provide only the tree with a few actions and an HTML-based view on the item (which just loads the website - after tab-based browsing we now have tree-based browsing :-)). Let's start by the SnapIn class:

[SnapInSettings("{36D66A51-A9A4-4981-B338-B68D15068F5C}", DisplayName = "Tiny IIS Manager")]
public class IisMgr : SnapIn
{
    private Runspace _runspace;

    public IisMgr()
    {
        InitializeRunspace();

        this.RootNode = new SitesNode();
    }

    internal Runspace Runspace { get { return _runspace; } }

    private void InitializeRunspace()
    {
        RunspaceConfiguration config = RunspaceConfiguration.Create();

        PSSnapInException warning;
        config.AddPSSnapIn("IisMgr", out warning);

        // NOTE: needs appropriate error handling

        _runspace = RunspaceFactory.CreateRunspace(config);
        _runspace.Open();
    }

    protected override void OnShutdown(AsyncStatus status)
    {
        if (_runspace != null)
            _runspace.Dispose();
    }
}

In here, the core bridging with PowerShell takes place: we create a runspace (the space in which we run commands etc) based on some configuration object that has loaded the IisMgr PowerShell snap-in created in the previous paragraph. We also expose the runspace through an internal property so that we can reference it from the other classes used by the snap-in, such as SitesNode:

class SitesNode : ScopeNode
{
    public SitesNode()
    {
        this.DisplayName = "Web sites";
        this.EnabledStandardVerbs = StandardVerbs.Refresh;

        LoadSites();
    }

    protected override void OnRefresh(AsyncStatus status)
    {
        LoadSites();
        status.Complete("Loaded websites", true);
    }

    private void LoadSites()
    {
        this.Children.Clear();
        this.Children.AddRange(
            (from site in ((IisMgr)this.SnapIn).Runspace.CreatePipeline("get-site").Invoke()
             select new SiteNode((Site)site.BaseObject)).ToArray()
        );
    }
}

The constructor is easy: we add a display name to the node (no blankness anymore) and enable the "standard verb" Refresh (which will appear in the action pane). To handle Refresh, we overload Refresh. Notice MMC 3.0 support asynchronous loading (not to block the management console when an action is taking place) but let's not go there for now. In LoadSites the real stuff happens: we grab the Runspace through the internal property defined on the SnapIn, create a pipeline that simply invokes get-site and finally invoke it by calling Invoke. This produces a collection of PSObject objects, which are wrappers (used for the Extended Type System) around the original object (in our case a Microsoft.Web.Administration.Site object). Using a simple LINQ query we grab the results and wrap them in SiteNode objects (see below) which are added as the node's children.

class SiteNode : ScopeNode
{
    private Site _site;
    private Microsoft.ManagementConsole.Action _startAction;
    private Microsoft.ManagementConsole.Action _stopAction;
    private HtmlViewDescription _view;

    public SiteNode(Site site)
    {
        _site = site;

        this.DisplayName = site.Name;
        this.EnabledStandardVerbs = StandardVerbs.Properties | StandardVerbs.Refresh;

        _startAction = new Microsoft.ManagementConsole.Action() { Tag = "start", DisplayName = "Start" };
        this.ActionsPaneItems.Add(_startAction);
        _stopAction = new Microsoft.ManagementConsole.Action() { Tag = "stop", DisplayName = "Stop" };
        this.ActionsPaneItems.Add(_stopAction);

        Refresh();

        Microsoft.Web.Administration.Binding binding = _site.Bindings[0];
        _view = new HtmlViewDescription(new Uri(String.Format("{0}://{1}:{2}", binding.Protocol, binding.Host == "" ? "localhost" : binding.Host, binding.EndPoint.Port))) { DisplayName = "View site", Tag = "html" };

        this.ViewDescriptions.Add(_view);
    }

    protected override void OnAction(Microsoft.ManagementConsole.Action action, AsyncStatus status)
    {
        switch (action.Tag.ToString())
        {
            case "start":
                ((IisMgr)this.SnapIn).Runspace.CreatePipeline("start-site -name \"" + _site.Name + "\"").Invoke();
                break;
            case "stop":
                ((IisMgr)this.SnapIn).Runspace.CreatePipeline("stop-site -name \"" + _site.Name + "\"").Invoke();
                break;
        }

        Refresh();
    }

    protected override void OnAddPropertyPages(PropertyPageCollection propertyPageCollection)
    {
        propertyPageCollection.Add(new PropertyPage() {
            Title = "Website",
            Control = new PropertyGrid() {
                SelectedObject = _site,
                Dock = DockStyle.Fill
            }
        });
    }

    protected override void OnRefresh(AsyncStatus status)
    {
        Refresh();
    }

    private void Refresh()
    {
        _startAction.Enabled = _site.State == ObjectState.Stopped;
        _stopAction.Enabled = _site.State == ObjectState.Started;
    }
}

That's basically it. In the constructor we define a couple of custom actions for the "Stop" and "Start" actions. We enable the verbs for Properties and Refresh and provide some basic implementation for those (for properties we rely on the PropertyGrid control although in reality you'd want a much more customized view on the data that hides the real underlying object model). We also add an HTML view description that points at the URL of the website itself (normally you'd use different types of view descriptions in order to show items under that particular node, e.g. virtual folders for the website, or a bunch of 'control panel style' configuration options, as in the real inetmgr.exe). Again, the logic to invoke cmdlets is very similar, we just add some parameterization:

        switch (action.Tag.ToString())
        {
            case "start":
                ((IisMgr)this.SnapIn).Runspace.CreatePipeline("start-site -name \"" + _site.Name + "\"").Invoke();
                break;
            case "stop":
                ((IisMgr)this.SnapIn).Runspace.CreatePipeline("stop-site -name \"" + _site.Name + "\"").Invoke();
                break;
        }

and this time no data is returned (strictly speaking that's not true since errors will flow back through the runspace - feel free to play around with this).

 

Step 3 - The result

Time to admire the result. Launch the MMC snap-in project under the debugger:

image  image

The full code is available over here. Usual disclaimers apply - this is nothing more than sample code...

Enjoy!

Del.icio.us | Digg It | Technorati | Blinklist | Furl | reddit | DotNetKicks

My recent series of "cookbook" posts has been very well-received and coincidentally I got mail today about MMC 3.0 snap-ins. I wrote on the subject a while (read: Vista RC1 timeframe) ago, so in this post I'll revisit the topic in cookbook style in order to provide the foundation for my next post on MMC 3.0 and PowerShell layering, a topic I talked about on TechEd EMEA 2007 but that never made it to a blog post.

For the record, my previous cookbook posts include the following:

For regular readers of my blog, step 1 and 2 will sound repetitive but in true PowerShell-style I'd say "verbosity is your friend" (J. Snover), especially in cookbooks.

 

Step 1 - Create a Class Library project

Create a new (C#) class library project, e.g. called MyMmcSnapIn:

image

 

Step 2 - Import references

In Solution Explorer, right-click the project node and select Add Reference... On the Browse tab, navigate to the %programfiles%\Reference Assemblies folder and locate the Microsoft\mmc\v3.0 subfolder:

image

Note: Reference Assemblies are installed by the Windows SDK - a must-have for each platform developer.

In there, select the Microsoft.ManagementConsole.dll file:

image

Click OK. Next, go back to the Add Reference dialog and select System.Configuration.Install from the .NET tab:

image

 

Step 3 - Create your snap-in

Snap-ins derive from the SnapIn class. Go ahead and rename Class1.cs to MySnapIn.cs. Next, inherit the class from SnapIn which will require to import the Microsoft.ManagementConsole namespace:

image

For sake of this post, let's keep things simple and just implement the bare minimum, i.e. setting the RootNode property to some node:

image

I'm using C# 3.0 syntax to do this, as shown below:

image

resulting in this piece of code:

image

A real implementation (see next post) would likely create a node class that derives from ScopeNode to create a custom node that consumes data (e.g. from PowerShell, see next post).

 

Step 4 - Adding metadata

Our snap-in needs to carry some metadata using the SnapInSettings custom attribute. This one needs a GUID so go to Tools, Create GUID to create one:

image

Click Copy and Exit which will put a GUID on the clipboard:

image

Now add the SnapInSettings custom attribute to your class:

image

and specify DisplayName and Description (other properties are not really required in this case):

image

 

Step 5 - The installer

In order to register the snap-in on the machine we need to add an installer class. This is as easy as creating a class deriving from SnapInInstaller:

image

Here's the resulting complete code:

image

 

Step 6 - Compile and register

Time to compile the project. Next, open up a Visual Studio 2008 Command Prompt running as Administrator and cd into the bin\Debug folder of your project. Now run installutil on the created assembly:

image

To verify the installation was successful, you can take a look in the registry under HKLM\Software\Microsoft\MMC\SnapIns and look for a key called FX:myguid where myguid is the one specified on the SnapInSettingsAttribute. It should point at your newly created assembly:

image

 

Step 7 - Create an MMC console for debugging

Time to test our snap-in. Go to Start, Run and specify mmc.exe. In the MMC console go to File, Add/Remove Snap-In (CTRL-M). You should see the registered MMC snap-in in there:

image

Notice the name and description. Select it and click Add. Finally click OK. The result should look like:

image

Quite minimalistic, I agree, but we're alive and kicking! Finally choose File, Save and save the console configuration to a file called Debug.msc under your project's folder:

image

Finally, close the MMC console (otherwise the loaded snap-in dll would remain locked, blocking subsequent builds).

 

Step 8 - Setting up the debugger

Back in Visual Studio, go to the project properties. On the tab Debug enter the path to mmc.exe (in the system32 folder) and specify the relative path (starting from bin\Debug) to Debug.msc created in the previous step for the command-line arguments:

image

Set a breakpoint in the code:

image

and press F5. You'll see the breakpoint getting hit:

image

 

Congratulations - your MMC debugging dinner is ready to be served!

Del.icio.us | Digg It | Technorati | Blinklist | Furl | reddit | DotNetKicks

A small post this time. While playing around with LINQ queries lately I noticed one minor missing piece that's merely a convenience thing but anyhow I thought to share it with the world: a ForEach operator. Such a "sequence operator" (to use an old-fashioned word, remember LINQ to Objects used to be called the Standard Query Operators, explaining the abbreviation used in my LINQSQO project at http://www.codeplex.com/LINQSQO) would allow us to write a query an iterate over it directly; look at it as a postfix variant of the foreach keyword if you want.

Here's how it looks like:

static class MoreEnumerable
{
   public static void ForEach<T>(this IEnumerable<T> source, Action<T> action)
   {
      if (source == null)
         throw new ArgumentNullException("source");

       if (action == null)
         throw new ArgumentNullException("action"); 

      foreach (T item in source)
         action(item);
   }
}

I won't elaborate on possible combinations with the Parallel FX extensions library and will leave that to the reader. Anyway, here's how your brand new home-brew operator would be used:

(from p in products where p.UnitPrice > 123 select new { Name = p.ProductName, Price = p.UnitPrice }).ForEach(p => {
   Console.WriteLine(p);
});

which is similar to List<T>'s ForEach<T> method. Notice you'll have full IntelliSense inside the lambda body - the type of p is inferred through the generic parameter T which is the anonymous (projection) type in the sample above.

Have fun!

Update: Apparently people read my posts as late as I'm posting them :-) which is of course well appreciated. I've posted a few personal insights on the pros and cons of this pattern in this post's comments section. Actually the original goal of the post was just to show some "more extensions" one could envision (have a set of other "functional style operators" coming up) but I like the idea of turning it into discussion mode :-). All feedback is welcome!

Del.icio.us | Digg It | Technorati | Blinklist | Furl | reddit | DotNetKicks

Introduction

Today I received the following mail from one of my blog readers:

Hi Bart,

The task scheduler in Vista and Windows Server 2008 has improved dramatically.

Unfortunately, there are no classes in the .NET Framework that allow us VB.NET/C# developers to leverage its power.

Many .NET applications require some form of scheduling or alerting, and instead of trying to roll our own with timers and stuff, over and over again, it seems to me that it would be much nicer to use the stable and powerful foundation that the OS offers for this.

Therefore, I thought it would be a great idea for a future article on your blog, about how to access and use the Task Scheduler 2.0 from .NET, with possibly an easy-to-use .NET wrapper class?

It seems that a lot of developers don't realize or think about what's sitting there right under the hood, 'cause I haven't seen any blog posts about this new Task Scheduler and its many features from a .NET developer's perspective yet. So here's my thought... :)

Best regards,

************

Obviously I completely agree with the statement on leveraging the power of the OS foundations whenever possible rather than reinventing the wheel once over again. Task Scheduler 2.0 is a great sample of such rich functionality offered by the OS and especially now we're shipping Windows Server 2008 this becomes even more important for server applications. Nevertheless, for desktop uses the Task Scheduler provides a tremendous amount of functionality as well and Windows Vista is using its own dogfood as you can see when you execute schtasks from the command-line (indicated a few well-known tasks in red):

image

In this post I'll cover how to use this API in a fairly easy way from managed code though COM interop, and I'll explain some of the richness the platform can give you.

 

Importing the library

I assume you've already created a Console Application in C# (though all of this would work in e.g. VB.NET as well). The Task Scheduler 2.0 API lives in a file called Taskschd.dll under the System32 folder on your system. In order to reference it from your .NET project, simply go to Solution Explorer, right-click the project node and choose Add Reference:

image

This will create the COM interop assembly as shown in Solution Explorer:

image

 

A simple demo task

Time to write our first task, or better to register it. Essentially tasks in Task Scheduler 2.0 are represented as XML fragments as you can see from schtasks:

image

I'd encourage readers to take a closer look at schtasks and the information one can obtain through it about the wide variety of tasks registered on the system. The API we'll be talking to allows us to manage these tasks (create new ones for example) though code, which provides an object model to create the metadata that represents a task as displayed above under the format of XML.

 

Step 1 - Establish a connection with the service

In order to talk to the Task Scheduler service we need to create a proxy object to it and connect to the service, either on the local machine or remote machine. We'll stick with the local machine for the scope of this post. Start by writing the following piece of code:

image

This will require to import the namespace TaskScheduler as revealed by the 'SmartTag' in Visual Studio. Notice I'm using the C# 3.0 local variable type inference keyword "var" here, but one could well write:

TaskSchedulerClass scheduler = new TaskSchedulerClass();

but not (using the TaskScheduler interface provided by the interop assembly)

TaskScheduler scheduler = new TaskSchedulerClass();

(little quiz - why?). Anyhow, we still need to connect to it using the Connect method:

image

We can simply supply four null arguments indicating we want to use the token of the user logged on currently (tip: run using administrative privileges to manage the service effectively). Needless to say, you can use other values for those parameters to connect to a particular machine (first parameter) and to specify a particular user (parameters 2-4 specify user name, password and domain) but we won't go there in this post.

 

Step 2 - Create a new task

The scheduler class provides a factory approach to create new tasks using the NewTask method. It takes one parameter that's reserved for future use (a typical COM API phenomenon) and should be set to 0 for the time being. Once the task has been created, we'll set some properties on it; the most typical ones living under RegistrationInfo and Settings (others will be covered further on):

image

Notice the amount of settings available to tweak the task, e.g. to control behavior with respect to the current power state of the machine, idle time, etc. For our purposes, the RegistrationInfo settings are enough as shown above.

 

Step 3 - Triggers

When to run the task? Enter triggers. There are a bunch of different trigger types available as revealed when calling Triggers.Create:

image

Most of these are self-explanatory (in case of doubt more information can be found on MSDN). The more interesting part is how to create a trigger in managed code. The crux lies in the fact you need to cast the result of the Create call to the right interface, such as ITimeTrigger for the TASK_TRIGGER_TIME type. Let's show a sample:

image

Other similar interfaces for other types of triggers can be found in the TaskScheduler namespace. Time triggers are pretty simple to understand so let's stick with those. In the sample above, we add an identifier to the trigger (tasks can have more than one trigger by the way) as well as some specific settings for this particular trigger. Besides of this we set the start and end time for the trigger; the settings in the sample specify a point in the past and the future so our current time falls nicely in between, triggering the demo task right away once we run it. If you want more powerful triggering, you can take a look at the Repetition property or use tasks such as 'daily' or 'monthly day-of-week (DOW)' or ...

Notice the strange (ISO 8601) date/time format specified on MSDN as: YYYY-MM-DDTHH:MM:SS(+-)HH:MM. In here, the first part is self-explanatory; the part after the +- is used to specify a time zone since tasks are stored based on UTC time. Tip: a string formatter will prove useful to generate this format.

 

Step 4 - Actions

After when comes what. Again there's some choice amongst the different types of actions to be taken:

image 

The EXEC task is one of the most common ones though. The common pattern is the same again: Create, cast, configure. Here's an example of a mail-task but this will require some server configuration in order to work:

image

Here's another one in the category EXEC:

image

Feel free to choose either of those, I'll go for yet another one that displays a message (by now the pattern should be captured I guess):

image

 

Step 5 - Task registration

We've created all the data needed to hook up the task. Last step is to hook it really up by calling RegisterTaskDefinition in some folder. Tasks are logically grouped in folders which you can managed through the ITaskFolder interface. One can obtain a specific folder using the GetFolder call on the scheduler service object. For demo purposes (and because of lack of inspiration tonight :-)) we'll drop the task in the root folder:

image

Again there's a bunch of flexibility available here but simplicity rules for blogging, so the stuff above is pretty much the easiest one can get. Basically we create (or update if it already exists) a named task "Demo" with the credentials of the logged-on used that can only be run when an interactive user is logged on to the machine and with no custom ACL to protect the task (which could be set using an SDDL descriptor).

 

Step 6 - Running it

To run the task we could add a line of code (though you could use schtasks /Run too or obivously rely on your (complex) triggers you've put in place). Since the API is not only about creating tasks, this shows nicely how to control tasks. Here's the whole program with the run line at the bottom:

image

Run it and you should see the following dialog coming out of the blue:

image

Victory at last :-).

 

Step 7 - Geeks only

Geeks can check where the message comes from e.g. using task manager (or a more advanced tool like Process Explorer):

image

Also, you can take a look at the task metadata using schtasks.exe:

image

And finally, if you want to delete the experiment, use schtasks.exe with the /Delete flag:

schtasks /delete /tn Demo

(or use the API to do so obviously :-)).

 

Happy multi-tasking!

Del.icio.us | Digg It | Technorati | Blinklist | Furl | reddit | DotNetKicks

It's great to see Scott pulled the trigger to tell the world about our .NET 3.5 Client Product Roadmap. Lately our setup team - which I'm officially part of - has been working in overdrive to put the pieces for the "Improved .NET Framework Setup for Client Applications" together and we're all looking forward to our first beta release. Once it's available I'll post more technical details outlining how it will help to ease your deployment, how it works and how you can customize and brand your client application's deployment experience.

Stay tuned!

Del.icio.us | Digg It | Technorati | Blinklist | Furl | reddit | DotNetKicks

I'm a firm believer of the "innovation through integration" theme. As a fan of MSBuild and PowerShell I wondered what it would take to bring the two worlds closer together. This post outlines the result of a short but 'powerful' experiment. In order to read this post, I strongly recommend to check out my recent post named The custom MSBuild task cookbook to learn about writing and debugging custom MSBuild tasks.

 

Hosting PowerShell

In order to run PowerShell in a customized environment (as opposed to the default shell that comes with the technology) one needs to work with runspaces. Essentially a runspace allows to host the PowerShell engine and interact with it through pipelines. We'll only cover very basic communication in this post. If one wants to feed back data from PowerShell to the MSBuild output for instance, a PSHost implementation would be required but that goes far beyond the scope of this post.

I've posted about runspaces earlier in my A first introduction to Windows PowerShell runspaces post about one year ago. You might want to check out that post for more information on hosting PowerShell.

 

Introducing PSBuild

Far from original, I admit, but let's call our baby PSBuild. In order to implement it, create a new class library project (C#) and add references to the following assemblies:

image

Including the MSBuild assemblies has been covered in the The custom MSBuild task cookbook post; for more information on the System.Management.Automation assembly, see my Easy Windows PowerShell cmdlet development and debugging post (step 2).

 

Implementing the task

First on our to-do list is implementing the custom MSBuild task in the C# code file. It's barely 45 lines:

using System;
using System.Management.Automation;
using System.Management.Automation.Runspaces;
using System.Text;
using Microsoft.Build.Framework;
using Microsoft.Build.Utilities;

namespace PSBuild
{
    public class InvokeScript : Task
    {
        [Required]
        public ITaskItem Script { get; set; }

        [Required]
        public string Function { get; set; }
        public ITaskItem[] Parameters { get; set; }

        public override bool Execute()
        {
            RunspaceConfiguration runspaceConfig = RunspaceConfiguration.Create();

            using (Runspace runspace = RunspaceFactory.CreateRunspace(runspaceConfig))
            {
                runspace.Open();

                StringBuilder commandLine = new StringBuilder();
                commandLine.Append(Function + " ");

                foreach (ITaskItem parameter in Parameters)
                {
                    commandLine.AppendFormat("\"{0}\" ", parameter.ItemSpec);
                }

                using (RunspaceInvoke scriptInvoker = new RunspaceInvoke(runspace))
                {
                    scriptInvoker.Invoke(Script.ItemSpec);
                    scriptInvoker.Invoke(commandLine.ToString());
                }
            }

            return true;
        }
    }
}

Usual disclaimers apply - this code is far from ideal and is just meant to illustrate the concept. Talking about a concept... Let's discuss:

  • We require two parameters: Script containing a PowerShell script block and Function pointing to the function to be invoked. The Parameters for invocation are optional since a function may have no parameters obviously.
  • Notice the type of the Script and Parameters properties. By using ITaskItem we can integrate nicely with MSBuild as we'll see further.
  • The Execute method does all the work. Essentially we create a Runspace and invoke two commands: first is the Script definition itself, second is the invocation of the script based on the Function and Parameters values.
  • Error handling was omitted from the code above - a production quality implementation needs to catch errors and return false in case of an error. Also, some logging (Log property on Task) would be welcome, e.g. to print the command-line that's being invoked (tip: Log.LogCommandLine).

 

Testing the task

Check out my The custom MSBuild task cookbook post for instructions on testing custom MSBuild tasks. I'll just show a sample MSBuild file below that invokes a script:

image

The UsingTask imports our task library built in the previous step. To define the script, we simply define a MyScript tag under a PropertyGroup element. In here we define a function called "ProcessList" that takes in two arguments. I've spread it across two lines to show that local variables (and in extension to that - try it yourself :-) - more advanced scripting techniques) simply work. Finally, we invoke our InvokeScript task somewhere, in this case in the Debug target (again, see The custom MSBuild task cookbook for more info) but you could imagine it to be part of your core build definition. The InvokeScript task references the MyScript through the property reference syntax $(...) of MSBuild; the Function is a simple string and in Parameters we put a semi-colon separated list of parameters which will be assigned $args[0] ... $args[n] in the invoked PowerShell script.

One could imagine the parameterization of the InvokeScript task to be much more complete and flexible (e.g. one could drop the Function attribute and simply execute some script) but that's just a matter of implementation. Also a way to feed back results isn't too difficult (RunspaceInvoke::Invoke returns a collection of PSObjects).

Here's what it does:

image

Notice that one can use any MSBuild variable in the parameterization which gives us a tremendous amount of power. For example, one could write a script that pre-processes all files in @(Compile), and leverage all of the PowerShell and .NET Framework power to do so. I leave it to the reader to experiment with the possibilities.

 

Happy PSBuilding!

Del.icio.us | Digg It | Technorati |