Hey Man Nice Shot (part 2)
Here is what has been accomplished so far in this grand No Impersonate plan:
Grab all entries in the Custom Action tableDecide which of these entries are deferred- Update the deferred entries to include the no impersonate bit
- 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 modifyMetadata
: The name of the metadata to modifyValue
: The value to use in the assignmentOperator
: 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 operatorModifiedItems
: 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 ITaskItem
s. The test then Execute
s 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 nul
l. 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 itemsAssigning 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 itemsAssigning a value to existing metadata produces output items with the correct metadata valuesAssigning 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 itemsAssigning a value to existing metadata produces output items with the correct metadata valuesAssigning a value to non-existent metadata adds the value to the output itemsUsing 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 itemsAssigning a value to existing metadata produces output items with the correct metadata valuesAssigning a value to non-existent metadata adds the value to the output itemsUsing an operator with existing metadata produces output items with the correct metadata valuesPassing 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 itemsAssigning a value to existing metadata produces output items with the correct metadata valuesAssigning a value to non-existent metadata adds the value to the output itemsUsing an operator with existing metadata produces output items with the correct metadata valuesPassing in an invalid operator name produces an exceptionUsing 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.
Grab all entries in the Custom Action tableDecide which of these entries are deferredUpdate the deferred entries to include the no impersonate bit- 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
Comments
Anonymous
June 29, 2007
Still unable to post multiple attachments to an entry. The installer was attached to part one of this post.Anonymous
September 18, 2007
The last step in the Vista-related improvements I outlined several months ago involves improving the