Hey Man Nice Shot (part 2)

(Continued...)

Here is what has been accomplished so far in this grand No Impersonate plan:

  1. Grab all entries in the Custom Action table
  2. Decide which of these entries are deferred
  3. Update the deferred entries to include the no impersonate bit
  4. Put the entries back into the table

Let's get crackig on the third item

Update the deferred entries to include the no impersonate bit

There are already ways to perform an assignment to metadata, but as far as I know there is no way to do the sort of modification that needs to be accomplished here: the OR assignment operator. Lets create a new task that will accomplish this. The task is stubbed out below:

 using System;
using System.Collections.Generic;
using Microsoft.Build.Framework;
using Microsoft.Build.Utilities;

namespace SetupProjects.Tasks
{
    public class AssignValue : Task
    {
        [Required]
        public ITaskItem[] Items
        {
            get { return null; }
            set { }
        }

        [Required]
        public string Metadata
        {
            get { return null; }
            set { }
        }

        [Required]
        public string Value
        {
            get { return null; }
            set { }
        }

        public string Operator
        {
            get { return null; }
            set { }
        }

        [Output]
        public ITaskItem[] ModifiedItems
        {
            get { return null; }
            set { }
        }

        public override bool Execute()
        {
            throw new Exception("The method or operation is not implemented.");
        }
    }
}

There are a total of 5 parameters to this task, 3 of which are required and 1 which is output. They are:

  • Items: The items to modify
  • Metadata: The name of the metadata to modify
  • Value: The value to use in the assignment
  • Operator: The name of the assignment operator to use. This is optional: if no name is specified, straight assignment will be performed. Otherwise, this will be the name of the class for the assignment operator
  • ModifiedItems: Output of the modified items

I'm going to take a slightly different approach this time for the unit tests. The tests felt a little forced with the FilterItems task, which used private methods that may not have used had tests been written. This time around, let's plan on writing tests that are closer to actual usage.

  • Assigning to an empty list of items results in no output items
  • Assigning a value to existing metadata produces output items with the correct metadata values
  • Assigning a value to non-existent metadata adds the value to the output items
  • Using an operator with existing metadata produces output items with the correct metadata values
  • Using an operator with non-existent metadata produces an exception

The first test should be pretty easy:

 [TestMethod()]
public void AssignValue_EmptyTest()
{
    AssignValue target = new AssignValue();
    target.Items = new TaskItem[0];
    target.Metadata = "Number";
    target.Value = "1";
    
    bool actual = target.Execute();

    Assert.IsTrue(actual, "Execute method did not succeed");
    Assert.IsNotNull(target.ModifiedItems, "ModifiedItems is null");
    Assert.AreEqual(0, target.ModifiedItems.Length, "ModifiedItems is not empty");
}

This test (which has been named AssignValue_EmptyTest to differentiate it from FilterItems' EmptyTest) creates a new instance of the AssignValue task and sets the required parameters. Most of the parameters are actually meaningless because the one that really matters for this test is Items, which is an empty array of ITaskItems. The test then Executes the task, ensures that the task passes, and ensures that no items are returned out of the task. Initially, the test fails because Execute is currently throwing an Exception:

 Failed AssignValue_EmptyTest   Test method SetupProjects.Tasks.UnitTests.AssignValueTest.AssignValue_EmptyTest threw exception:  System.Exception: The method or operation is not implemented.

No problem:

 public override bool Execute()
{
    return true; 
}

That still doesn't get the test passing, but it is a step closer:

 Failed  AssignValue_EmptyTest   Assert.IsNotNull failed. ModifiedItems is null   

ModifiedItems needs to not return null. so, let's get that output parameter filled out:

 private List modifiedItems = new List(); 
[Output]
public ITaskItem[] ModifiedItems
{
    get { return modifiedItems.ToArray();  }
    set { }
}

which gets the test passing.

  • Assigning to an empty list of items results in no output items
  • Assigning a value to existing metadata produces output items with the correct metadata values
  • Assigning a value to non-existent metadata adds the value to the output items
  • Using an operator with existing metadata produces output items with the correct metadata values
  • Using an operator with non-existent metadata produces an exception

The next test will witness the return of our old friend, the testItems list. I won't include the code again, but suffice it to say it was copied from the FilterItemsTest class. Here is the test for the next item:

 [TestMethod()]
public void AssignValue_ExistingMetadataTest()
{
    AssignValue target = new AssignValue();
    target.Items = testItems.ToArray();
    target.Metadata = "Number";
    target.Value = "1";

    bool actual = target.Execute();

    Assert.IsTrue(actual, "Execute method did not succeed");
    Assert.IsNotNull(target.ModifiedItems, "ModifiedItems is null");
    Assert.AreEqual(testItems.Count, target.ModifiedItems.Length, string.Format("ModifiedItems should contain {0} elements", testItems.Count));
    for (int i=0; i<target.ModifiedItems.Length; i++)
    {
        ITaskItem actualItem = target.ModifiedItems[i];
        Assert.AreEqual(testItems[i].ItemSpec, actualItem.ItemSpec);
        Assert.IsTrue(actualItem.GetMetadata("Number").Equals("1"), string.Format("Metadata \"Number\" was {0}. Expected 1", actualItem.GetMetadata("Number")));
    }
}

This test initially fails because the ModifiedItems list comes back empty:

 Failed    AssignValue_ExistingMetadataTest    Assert.AreEqual failed. Expected:<5>, Actual:<0>. ModifiedItems should contain 5 elements    

Its time to get ModifiedItems involved with the task. Modify the Execute function to do something useful:

 public override bool Execute()
{
    foreach(ITaskItem item in Items)<br>    {<br>        TaskItem modifiedItem  = new TaskItem(item);<br>        modifiedItem.SetMetadata(Metadata, Value);<br>        modifiedItems.Add(modifiedItem);<br>    } 
    return true;
}

However, this still won't work until Items, Metadata, and Value are more than just stubs:

 private ITaskItem[] items; 
[Required]
public ITaskItem[] Items
{
    get { return items; }
    set { items = value;  }
}

private string metadata; 
[Required]
public string Metadata
{
    get { return metadata;  }
    set { metadata = value;  }
}

private string value; 
[Required]
public string Value
{
    get { return value; }
    set { this.value = value;  }
}

After this, both AssignValue tests now pass.

  • Assigning to an empty list of items results in no output items
  • Assigning a value to existing metadata produces output items with the correct metadata values
  • Assigning a value to non-existent metadata adds the value to the output items
  • Using an operator with existing metadata produces output items with the correct metadata values
  • Using an operator with non-existent metadata produces an exception

My guess is that the next test will pass out of the gate:

 [TestMethod()]
public void AssignValue_NotExistingMetadataTest()
{
    AssignValue target = new AssignValue();
    target.Items = testItems.ToArray();
    target.Metadata = "NewValue";
    target.Value = "word";

    bool actual = target.Execute();

    Assert.IsTrue(actual, "Execute method did not succeed");
    Assert.IsNotNull(target.ModifiedItems, "ModifiedItems is null");
    Assert.AreEqual(testItems.Count, target.ModifiedItems.Length, string.Format("ModifiedItems should contain {0} elements", testItems.Count));
    for (int i = 0; i < target.ModifiedItems.Length; i++)
    {
        ITaskItem actualItem = target.ModifiedItems[i];
        Assert.AreEqual(testItems[i].ItemSpec, actualItem.ItemSpec);
        Assert.AreEqual(testItems[i].GetMetadata("Number"), actualItem.GetMetadata("Number"), "Comparing Number metadata");
        Assert.AreEqual("word", actualItem.GetMetadata("NewValue"), "Comparing NewValue metadata");
    }
}

And it did pass. Still, it didn't take long to confirm and I'm happy to have this coverage.

  • Assigning to an empty list of items results in no output items
  • Assigning a value to existing metadata produces output items with the correct metadata values
  • Assigning a value to non-existent metadata adds the value to the output items
  • Using an operator with existing metadata produces output items with the correct metadata values
  • Using an operator with non-existent metadata produces an exception

A new "Or" operator will be written for the next test. This will require a new class, interface, and changes to AssignValue to use the new operator. In anticipation of the new class and future class, the tests are going to be added a separate class, but will still be using the AssignValue task. This is a slightly different approach from the FilterItems task, in which the test class defined a unique filter and passed that in.

Here is the test class for this next test:

     [TestClass()]
    public class OrOperatorTest
    {
        private List testItems;

        [TestInitialize()]
        public void TestInitialize()
        {
             /* Ommitted... */
        }

        [TestMethod()]
        public void OrOperator_ExistingMetadataTest()
        {
            AssignValue target = new AssignValue();
            target.Items = testItems.ToArray();
            target.Metadata = "Number";
            target.Value = "2";
            target.Operator = "Or";

            bool succeeded = target.Execute();

            Assert.IsTrue(succeeded, "Execute method did not succeed");
            Assert.IsNotNull(target.ModifiedItems, "ModifiedItems is null");
            Assert.AreEqual(testItems.Count, target.ModifiedItems.Length, string.Format("ModifiedItems should contain {0} elements", testItems.Count));

            List expectedItems = new List();
            for (int i = 0; i < testItems.Count; i++)
            {
                TaskItem item = new TaskItem(testItems[i]);
                int expectedResult = (i + 1) | 2;
                item.SetMetadata("Number", expectedResult.ToString());
                expectedItems.Add(item);
            }
            for (int i = 0; i < target.ModifiedItems.Length; i++)
            {
                ITaskItem actualItem = target.ModifiedItems[i];
                Assert.AreEqual(expectedItems[i].ItemSpec, actualItem.ItemSpec);
                Assert.AreEqual(expectedItems[i].GetMetadata("Number"), actualItem.GetMetadata("Number"), "Number metadata");
            }
        }
    }

The test uses the old familiar dataset from previous tests (note to self: should refactor this into a base class). The test then verifies that the | was done correctly by setting up the expectations prior to testing the actual results. And this test is currently failing:

 Failed    OrOperator_ExistingMetadataTest Assert.AreEqual failed. Expected:<3>, Actual:<2>. Number metadata

Let's get this passing. Step one is to set up something to perform the operation by defining an interface:

 public interface IOperator
{
    string Operate(string arg1, string arg2);
}

It may seem a little strange to be passing in strings here, but both the Value parameter and Metadata value off the Item are both strings, so this may make sense. The expectation (although not enorced) for most arithmetic operators is that the first argument is the left-hand side of the expression, and second argument is the right-hand side.

Next create the OrOperator class which implements this interface:

 class OrOperator : IOperator
{
    public string Operate(string left, string right)
    {
        int result = int.Parse(left) | int.Parse(right);
        return result.ToString();
    }
}

And finally finish off the AssignValue task:

 private string op;<br>public string Operator<br>{<br>    get { return op; }<br>    set { op = value; }<br>} 

public override bool Execute()
{
    IOperator op = null;<br>    if (!string.IsNullOrEmpty(Operator))<br>    {<br>        op = CreateOperator();<br>    } 
    foreach(ITaskItem item in Items)
    {
        TaskItem modifiedItem  = new TaskItem(item);
        if (op != null)<br>        {<br>            modifiedItem.SetMetadata(Metadata, op.Operate(item.GetMetadata(Metadata), Value));<br>        }<br>        else<br>        { 
            modifiedItem.SetMetadata(Metadata, Value);
         } 
        modifiedItems.Add(modifiedItem);
    }
    return true;
}

private IOperator CreateOperator()<br>{<br>    if (string.Equals(Operator, "or", StringComparison.OrdinalIgnoreCase))<br>        return new Operators.OrOperator();<br>    return null;<br>} 

The task finally makes use of the Operator parameter. Based on the existence of that value, the task creates and uses an operator, setting the modified item when the operation is complete. (Note: after finishing this task, I decided it might be more generally useful to have the task take 2 or maybe even 3 metadata names: in an expression such as a = b _ c, it sort of makes sense to allow the metadata names to be specified for all 3 values. But, this is currently beyond the needs of the task to set up no impersonate custom actions, so this is going to stay as is).

The test now passes, but I think the code requires a new test:

  • Assigning to an empty list of items results in no output items
  • Assigning a value to existing metadata produces output items with the correct metadata values
  • Assigning a value to non-existent metadata adds the value to the output items
  • Using an operator with existing metadata produces output items with the correct metadata values
  • Passing in an invalid operator name produces an exception
  • Using an operator with non-existent metadata produces an exception

Returning null from CreateOperator doesn't quite feel right; I think an exception would be preferred:

 [DeploymentItem("SetupProjects.Tasks.dll")]
[TestMethod()]
[ExpectedException(typeof(ArgumentException))]
public void AssignValue_BogusOperatorTest()
{
    AssignValue target = new AssignValue();
    target.Items = testItems.ToArray();
    target.Metadata = "Number";
    target.Value = "2";
    target.Operator = "FooOperator";
    target.Execute();
}

Running this test fails:

 Failed   AssignValue_BogusOperatorTest   AssignValueTest Test method SetupProjects.Tasks.UnitTests.AssignValueTest.AssignValue_BogusOperatorTest did not throw expected exception.

This is easy enough to correct:

 private IOperator CreateOperator()
{
    if (string.Equals(Operator, "or", StringComparison.OrdinalIgnoreCase))
        return new Operators.OrOperator();

    throw new ArgumentException(string.Format("Could not create operator named {0}", Operator), Operator);
}
  • Assigning to an empty list of items results in no output items
  • Assigning a value to existing metadata produces output items with the correct metadata values
  • Assigning a value to non-existent metadata adds the value to the output items
  • Using an operator with existing metadata produces output items with the correct metadata values
  • Passing in an invalid operator name produces an exception
  • Using an operator with non-existent metadata produces an exception

Technically, this last test is probably working: I would guess that if metadata is missing, Parse would throw a FormatException. Here is the test to make sure:

 [DeploymentItem("SetupProjects.Tasks.dll")]
[TestMethod()]
[ExpectedException(typeof(FormatException))]
public void OrOperator_NonExistingMetadataTest()
{
    AssignValue target = new AssignValue();
    target.Items = testItems.ToArray();
    target.Metadata = "FooMetadata";
    target.Value = "2";
    target.Operator = "Or";
    target.Execute();
}

As I expected, this did pass.

  • Assigning to an empty list of items results in no output items
  • Assigning a value to existing metadata produces output items with the correct metadata values
  • Assigning a value to non-existent metadata adds the value to the output items
  • Using an operator with existing metadata produces output items with the correct metadata values
  • Passing in an invalid operator name produces an exception
  • Using an operator with non-existent metadata produces an exception

Now that all of the tests are passing, it is time to update the postbuild step to make use of the Or operator. The task should update the filtered items to or 2048 (the bit for NoImpersonate) onto the Type data:

 <Target Name="PostBuild">
    <Select MsiFileName="$(BuiltOutputPath)"
            TableName="CustomAction">
        <Output TaskParameter="Records" ItemName="CustomActionRecords" />
    </Select>
    <Message Text="All Custom Actions:" />
    <Message Text="%(CustomActionRecords.Action) %(CustomActionRecords.Type)" />

    <FilterItems Name="BitwiseAnd" FilterInfo="@(DeferredCustomActionFilterInfo)"  Items="@(CustomActionRecords)">
        <Output TaskParameter="MatchedItems" ItemName="DeferredCustomActionRecords" />
    </FilterItems>
    <Message Text="Deferred Custom Actions:" />
    <Message Text="%(DeferredCustomActionRecords.Action) %(DeferredCustomActionRecords.Type)" />

    <AssignValue Items="@(DeferredCustomActionRecords)" Metadata="Type" Operator="Or" Value="2048">
        <Output TaskParameter="ModifiedItems" ItemName="ModifiedDeferredCustomActionRecords" />
    </AssignValue>
    <Message Text="Added NoImpersonate:" />
    <Message Text="%(ModifiedDeferredCustomActionRecords.Action) %(ModifiedDeferredCustomActionRecords.Type)" />
</Target>

Building the project shows that this seems to be working:

 Project "E:\MWadeBlog\src\Examples\SetupNoImpersonate\PostBuild.proj" (default targets):

Target PostBuild:
    All Custom Actions:
    _CA05F9CD_3117_47F8_AD98_5A53210BCD93.uninstall 1025
    _CA05F9CD_3117_47F8_AD98_5A53210BCD93.uninstall.SetProperty 51
    _F8702B9C_568F_49D7_A77F_6FF50945BBB7.install 1025
    _F8702B9C_568F_49D7_A77F_6FF50945BBB7.install.SetProperty 51
    _51C6890F_5CB0_4085_8587_0CBACD4B4431.rollback 1281
    _51C6890F_5CB0_4085_8587_0CBACD4B4431.rollback.SetProperty 51
    _51D9F96A_3196_4E83_811C_0FFD4127D3BE.commit 1537
    _51D9F96A_3196_4E83_811C_0FFD4127D3BE.commit.SetProperty 51
    DIRCA_TARGETDIR 307
    DIRCA_CheckFX 1
    VSDCA_VsdLaunchConditions 1
    ERRCA_CANCELNEWERVERSION 19
    ERRCA_UIANDADVERTISED 19
    VSDCA_FolderForm_AllUsers 51
    Deferred Custom Actions:
    _CA05F9CD_3117_47F8_AD98_5A53210BCD93.uninstall 1025
    _F8702B9C_568F_49D7_A77F_6FF50945BBB7.install 1025
    _51C6890F_5CB0_4085_8587_0CBACD4B4431.rollback 1281
    _51D9F96A_3196_4E83_811C_0FFD4127D3BE.commit 1537
    Added NoImpersonate:
    _CA05F9CD_3117_47F8_AD98_5A53210BCD93.uninstall 3073
    _F8702B9C_568F_49D7_A77F_6FF50945BBB7.install 3073
    _51C6890F_5CB0_4085_8587_0CBACD4B4431.rollback 3329
    _51D9F96A_3196_4E83_811C_0FFD4127D3BE.commit 3585

Build succeeded.

One task left.

  1. Grab all entries in the Custom Action table
  2. Decide which of these entries are deferred
  3. Update the deferred entries to include the no impersonate bit
  4. Put the entries back into the table

Put the entries back into the table

There are a couple of ways this could be done. Aaron uses view.Modify. Its also possible to use the WiRunSQL.vbs script with commands like:

 cscript WiRunSQL.vbs SetupNoImpersonate.msi "UPDATE `CustomAction` SET `Type`=3585 WHERE `Action`='_51D9F96A_3196_4E83_811C_0FFD4127D3BE.commit'

Alternatively, there are tasks defined in SetupProjects.Tasks that will perform the necessary work. ExecuteSql and ModifyTableData would both probably work. Let's use the latter.

This is pretty easy, most of the difficult parts have been done as part of the Message tasks in the project. To wit:

 <Target Name="PostBuild">
    <Select MsiFileName="$(BuiltOutputPath)"
            TableName="CustomAction">
        <Output TaskParameter="Records" ItemName="CustomActionRecords" />
    </Select>
    <Message Text="All Custom Actions:" />
    <Message Text="%(CustomActionRecords.Action) %(CustomActionRecords.Type)" />

    <FilterItems Name="BitwiseAnd" FilterInfo="@(DeferredCustomActionFilterInfo)"  Items="@(CustomActionRecords)">
        <Output TaskParameter="MatchedItems" ItemName="DeferredCustomActionRecords" />
    </FilterItems>
    <Message Text="Deferred Custom Actions:" />
    <Message Text="%(DeferredCustomActionRecords.Action) %(DeferredCustomActionRecords.Type)" />

    <AssignValue Items="@(DeferredCustomActionRecords)" Metadata="Type" Operator="Or" Value="2048">
        <Output TaskParameter="ModifiedItems" ItemName="ModifiedDeferredCustomActionRecords" />
    </AssignValue>
    <Message Text="Added NoImpersonate:" />
    <Message Text="%(ModifiedDeferredCustomActionRecords.Action) %(ModifiedDeferredCustomActionRecords.Type)" />

     <ModifyTableData MsiFileName="$(BuiltOutputPath)" <br>                     TableName="CustomAction" <br>                     ColumnName="Type" <br>                     Value="%(ModifiedDeferredCustomActionRecords.Type)" <br>                     Where="`Action`='%(ModifiedDeferredCustomActionRecords.Action)'" /> 
</Target>

Viewing the msi in Orca shows that the deferred custom actions do in fact have the NoImpersonate bit set.

Putting it all together

Now that all of the individual tasks have been defined to make deferred custom actions no impersonate, let's group all of the tasks together so that it is easier to remember. By moving all of the tasks into a target in the SetupProjects.targets file, anyone who wants to make their custom actions no impersonate need only call the target. So, most of the project contents get moved to the SetupProjects.targets file:

 <ItemGroup>
    <_DeferredCustomActionFilterInfo Include="Type">
        <Value>1024</Value>
    </_DeferredCustomActionFilterInfo>
</ItemGroup>

<Target Name="NoImpersonateCustomActions">
    <Select MsiFileName="$(BuiltOutputPath)"
            TableName="CustomAction">
        <Output TaskParameter="Records" ItemName="_CustomActionRecords" />
    </Select>

    <FilterItems Name="BitwiseAnd" 
                 FilterInfo="@(_DeferredCustomActionFilterInfo)"  
                 Items="@(_CustomActionRecords)">
        <Output TaskParameter="MatchedItems" ItemName="_DeferredCustomActionRecords" />
    </FilterItems>

    <AssignValue Items="@(_DeferredCustomActionRecords)" 
                 Metadata="Type" 
                 Operator="Or" 
                 Value="2048">
        <Output TaskParameter="ModifiedItems" ItemName="_ModifiedDeferredCustomActionRecords" />
    </AssignValue>

    <ModifyTableData MsiFileName="$(BuiltOutputPath)" 
                     TableName="CustomAction" 
                     ColumnName="Type" 
                     Value="%(_ModifiedDeferredCustomActionRecords.Type)" 
                     Where="`Action`='%(_ModifiedDeferredCustomActionRecords.Action)'" />
</Target>

This is mostly the same as before, but I made all of the Items private by prefacing them with "_".

After this change, the postbuild.proj becomes a lot easier. Here it is in its full glory:

 <Project xmlns="https://schemas.microsoft.com/developer/msbuild/2003" DefaultTargets="PostBuild">
    <Import Project="$(MSBuildExtensionsPath)\SetupProjects\SetupProjects.Targets" />
    
    <Target Name="PostBuild">
        <CallTarget Targets="NoImpersonateCustomActions" />
    </Target>
</Project>

SetupProjects.Tasks-1.0.20629.0-src.zip