07 January 2014

Completely remove an application tier from TFS 2012

If you ever wondered if it is possible to completely remove an application tier from the TFS Administration Console, here is an answer for you. Yes, but it is tricky and may result in a broken TFS installation. It involves messing with the catalog in the TFS_Configuration database.

As a disclaimer, this method is absolutely not supported. Not by me, not by my company, not by Microsoft. Use at your own risks.

So if you decide to reproduce the steps I am going to describe, please BACK UP your TFS installation (especially the Tfs_Configuration database).

Now, a little bit of context before starting, TFS uses a set of tables known as the catalog to keep track of the different pieces of your installation. It contains a lot of things including the databases, the servers, the web applications, etc.

Whenever you had a new piece to the installation (app tier, reporting tier, etc.) it goes into the catalog. Our problem is that for the moment there is no way to tell when you completely remove an app tier. So even if you uninstall an app tier, it stays in the catalog and eventually it will go out of sight thanks to the "Filter out machines that have not connected in more than 3 days".

Our goal here is to remove the app tier from the catalog itself. Unfortunately there is quite a bit of steps, and even more things to be aware of.

The first warning is that the steps below will produce the desired output only if you completely remove the server from the installation.

For example, consider a server APP-SERV1 hosting an app tier and another APP-SERV2 hosting an second app tier and SQL Server Analysis Services.

If you want to simply uninstall the app tier from APP-SERV2 but keep the SSAS on it, there is a high chance that this procedure will break your installation.

Actually this procedure removes the server from the catalog including all the other components that could have been part of the TFS installation (databases, reporting, datawarehouse, etc.).

Assuming TFS-APP is the server we'll remove, here is a high level list of the steps we will perform:
  • find the catalog resource for TFS-APP,
  • find the catalog resources for TFS-APP services,
  • find the catalog node for TFS-APP,
  • find the catalog nodes for TFS-APP services,
  • find the properties of TFS-APP,
  • find the catalog node dependencies for TFS-APP and services,
  • remove the items we found.

1. First things first, open a connection to your Tfs_Configuration database.

2. Then we start by looking for our server in the tbl_CatalogResource and tbl_CatalogNode tables:

SELECT 'Resource >'
     , r.*
     , 'Type >'
     , t.*
     , 'Node >'
     , n.*
  FROM [Tfs_Configuration].[dbo].[tbl_CatalogNode] n
  JOIN [Tfs_Configuration].[dbo].[tbl_CatalogResource] r
    ON r.Identifier = n.ResourceIdentifier
  JOIN [Tfs_Configuration].[dbo].[tbl_CatalogResourceType] t
    ON t.Identifier = r.ResourceType
 WHERE r.DisplayName = '{SERVER}' -- Replace with your server name

3. Note the value of the PropertyId, ParentPath, ChildItem, ResourceIdentifier fields. They will be used to identify and then delete the relevant data.

4. Select the properties associated with the catalog resource from the tbl_PropertyValue table.

SELECT * 
  FROM [Tfs_Configuration].[dbo].[tbl_PropertyValue]
 WHERE ArtifactId = '{PropertyId}' -- Replace with the value from step 3

5. Select the relevant items from the tbl_CatalogNodeDependency, we are looking for a) the items TFS-APP depends on, b) the items that depend on TFS-APP and its children:

-- a)
SELECT *
  FROM [Tfs_Configuration].[dbo].[tbl_CatalogNodeDependency]
 WHERE ParentPath = '{ParentPath}' --Replace with value from step 3
    AND ChildItem = '{ChildItem}' -- Replace with value from step 3

-- b)
SELECT *
  FROM [Tfs_Configuration].[dbo].[tbl_CatalogNodeDependency]
 WHERE RequiredParentPath LIKE '{ParentPath}{ChildItem}%' -- Replace with values from step 3

6. The records returned by a) will be removed in the end.

7. From the results returned by b) note the values of the RequiredParentPath and RequiredChilditem

Warning: If the app tier was the only TFS component installed on the server, you should only have the "WebApplication" value  in the AssociationKey field, otherwise it means you had other components like databases or datawarehouse installed on the server. In this case, I recommend you make sure you understand the implications. For example, make sure the reports and warehouse were already migrated to another server (check in the TFS Administration Console).

8. Select the APP-SERVER children:


SELECT 'Resource >'
     , r.*
     , 'Type >'
     , t.*
     , 'Node >'
     , n.*
  FROM [Tfs_Configuration].[dbo].[tbl_CatalogNode] n
  JOIN [Tfs_Configuration].[dbo].[tbl_CatalogResource] r
    ON r.Identifier = n.ResourceIdentifier
  JOIN [Tfs_Configuration].[dbo].[tbl_CatalogResourceType] t
    ON t.Identifier = r.ResourceType
 WHERE r.ParentPath = '{RequiredParentPath}' -- Replace with value from step 7
   AND r.ChildItem = '{RequiredChildItem}' -- Replace with value from step 7

9. Note the values of PropertyId, ParentPath, ChildItem, ResourceIdentifier.

10. We have everything we need to start the actual removal of the server.

-- Delete property values
DELETE FROM [Tfs_Configuration].[dbo].[tbl_PropertyValue] WHERE ArtifactId IN ({PropertyId}) -- Replace with values from steps 3 and 9

-- Delete dependencies for TFS-APP
DELETE FROM [Tfs_Configuration].[dbo].[tbl_CatalogNodeDependency]
 WHERE ParentPath = '{ParentPath}'
   AND ChildItem = '{ChildItem}' -- Replace with values from step 3

-- Delete dependencies to TFS-APP and services (Web application)
DELETE FROM [Tfs_Configuration].[dbo].[tbl_CatalogNodeDependency]

 WHERE RequiredParentPath LIKE '{ParentPath}{ChildItem}%' -- Replace with values from step 3

-- Delete nodes
DELETE FROM [Tfs_Configuration].[dbo].[tbl_CatalogNode]
 WHERE ParentPath = '{ParentPath}'
   AND ChildItem = '{ChildItem}' -- Replace with values from step 3 and 9

-- Delete resources
DELETE FROM [Tfs_Configuration].[dbo].[tbl_CatalogResource]
 WHERE Identifier IN ({ResourceIdentifier}) -- Replace with values from 3 and 9

11. Open the TFS Administration Console, you should now see only the active app tiers in the Application Tiers section even if you uncheck the "Filter out..." checkbox.
If the list is empty, it means the catalog is broken and TFS cannot rebuild the dependencies. This can happen if you forgot to delete a dependency on a deleted node.
To find the source of the problem, open the Event Viewer, you should see some TFS Service errors with the following message:
TF246024: An error occured while attempting to build catalog nodes. A catalog node is missing the following path to its parent: [parent path].

One possible solution would be to find the tbl_CatalogNodeDependency record with the RequiredParentPath set to the value in the error message, and then delete the record.

Error when running the TFS Best Practices Analyzer with an account containing a dollar ($) sign

I recently tried to run the TFS BPA using an account containing a dollar ($) sign. It was something like "user$name".
I ended up with a rather strange error message looking like the following:

Cannot validate the URL provided The scan was generated using the corrected URL "%TFSServerURLValidated%".

A quick look at the "Other reports" section of the scan results, I found that the tool was trying to find files in the following directory:

c:\Users\user\AppData\Roaming\Microsoft\TfsBpa\[...]

The important part is highlighted.
It seems that some of the PowerShell scripts are analyzing the account name as the literal user followed by a variable named $name, instead of interpreting the whole account name as a literal.

As I didn't want to go through the various PS scripts in the BPA installation directory, I decided to take the lazy approach.
I created a symbolic link C:\Users\user pointing to C:\Users\user$name\ using the following command:

mklink /d \Users\user \Users\user$name

After that the BPA was able to perform the checks as expected.
Of course it is not a long term solution and it works only for my account, but still easier than scanning all the ps1 files to find the culprit.

26 September 2013

Error TF400324 when starting the Visual Studio Test Controller

I recently had a problem starting the Test Controller Configuration Tool. There was no error, but it simply hung on the loading state. A quick look at the event log showed three errors. The most important one was:
The Controller service could not be started. TF400324: Team Foundation services are not available from server: [redacted URL]

The URL was referring to an old team project collection. I actually wanted to use the Configuration Tool to change this URL.

Since I could not attach the controller to another collection by using the Configuration Tool, I started to look around to find where the configuration itself was stored. The answer is an XML file named QTControllerConfig.xml located in C:\Program Files (x86)\Microsoft Visual Studio 11.0\Common7\IDE.
I opened the file, replaced the content with this:
<?xml version="1.0" encoding="UTF-8"?>
<ControllerConfiguration xmlns="http://microsoft.com/schemas/VisualStudio/TeamTest/2010/QTControllerConfig.xsd">
  <TestEnvironments>
    <TestEnvironment name="Default" id="8869bc36-5a69-4901-b46a-a38dfbd2ed06">
      <Description>Default Test Environment</Description>
      <MachineRoles>
        <MachineRole name="Default" id="dc134fe8-3c53-42e4-bcc1-6f2392e2fb8c" />
      </MachineRoles>
    </TestEnvironment>
  </TestEnvironments>
  <Properties>
    <Property name="MaxAgentVersionOnController" value="11.0.60610" />
  </Properties>
</ControllerConfiguration>
After that, I tried to open the Configuration Tool again and it worked.

For the record, updating the URL to the intended one did not work because the test controller itself was not yet registered.

28 June 2013

Visual Studio TFS 2012 and Web Deployment Packages

If you have been using Visual Studio 2010 and TFS to build Web Deployment packages (usable with MS Deploy), there is in the project properties a parameter for the name of the Web application in IIS.
The parameter is available in the Package/Publish Web section under IIS Web site/application name to use on the destination server.
Unfortunately this parameter is not available in Visual Studio 2012.
You can specify it when using the Publish feature (by right-clicking on the project) but if you want to set up builds in TFS, you will end up with an application in IIS with a _deploy suffix.

One possible solution is to add an additional parameter to the MSBuild command to update the IIS Web Application name.
In the process properties of the build definition, update the DeployIisAppPath in the MSBuild arguments:
MSBuild additional arguments

Entity Framework and TFS Build

I recently had an issue when trying to configure automatic deployments in TFS.
I was building a MS Deploy package with one build definition and deploying it with another.
The application was based on ASP.NET MVC with Entity Framework as a ORM
When I tried to open the application on the target server, I got an error "Unable to load the specified metadata resource".

The entity data model was configured to store the model files as resources into the assembly. The error was caused by the fact that the build process was no longer executing the packaging step. Usually Visual Studio does it automatically for you but MSBuild is not so kind. You have to manually tell it to execute the EntityDeploy step.

I added an instruction in the project file (containing the data model) to execute the packaging step.

<Target Name="AfterBuild">
  <EntityDeploy Sources="Models\DataModel.edmx" outputPath=".">
  </EntityDeploy>
</Target>

After that I made sure the build definition specified the MSBuild switch to rebuild the binaries (and not just build).

25 April 2012

.NET 4.0 version of InformationBox

At long last, a .NET 4.0 version of InformationBox was published on CodePlex!
As you might expect, it now supports the optional and named parameters making it a little easier to read the code.

You can grab the latest source code release from here: http://infobox.codeplex.com/SourceControl/list/changesets

08 August 2011

DateTimePicker and DateTime.MaxValue

For some unknown reason, the Winforms DateTimePicker control does not allow for dates higher than 9998-12-31. Trying to set the DateTimePicker MaxDate property of DateTime.MaxValue will result in an exception. This is a limitation imposed by the component itself.

Good news is there is a small trick to overcome this limitation. It requires to update the static MaxDateTime property to an arbitrary value (DateTime.MaxValue in our case). Lets put up some reflection code for that purpose:

var dtpType = typeof(DateTimePicker);
var field = dtpType.GetField("MaxDateTime"BindingFlags.Public | BindingFlags.Static);
if (field != null)
{
    field.SetValue(new DateTimePicker(), DateTime.MaxValue);
}

Just copy and paste this code into the startup method. Basically it needs to run before any DateTimePicker control is created.

Please note that running this code can have side effects because it is not known why this limitation is present in the first place. Use it at your own risk.

13 September 2010

Create UserControl with inline content

Let's say you want to create a user control that contains some literal text:
<uc1:InlineContent>
    This is some standard text message
</uc1:InlineContent>
Here is the simplest UserControl that matches this description:
using System.Web.UI;

[ParseChildren(ChildrenAsProperties = true,
               DefaultProperty = "Content")]
public partial class InlineContent : System.Web.UI.UserControl
{
    [PersistenceMode(
        PersistenceMode.EncodedInnerDefaultProperty)]
    public string Content { get; set; }
}

06 September 2010

Adding a delay before processing Textbox events

Typical scenario: You provide the user with a TextBox he can use to type a filter over a big chunk of data that takes a lot of time to process. Most of the time, you would handle the TextChanged event of the TextBox and issue a filter operation over the data source using the character(s) available in the TextBox. Typical problem: The user complains because he has to wait between the key strokes even if he already knows a filter that would allow the search to take place in a matter of a few seconds. Solution: Introduce a delay between the last key stroke and the processing using a timer. Walkthough: Create a new Winforms project with a Form. Add a TextBox (named textBox), a ListBox (named listBox). To mimic a very slow data source, we will create a dummy class:



public class VerySlowDataAccess
{
    private static string[] data = null;
    static VerySlowDataAccess()
    {
        data = new[]
        {
            "AAAAAA1", "AAAAAA2", "AAAAAA3", "AAAAAA4",
            "AAAAAA5", "AAAAAA6", "AAAAAA7", "BBBBBB1",
            "BBBBBB2", "BBBBBB3", "BBBBBB4", "BBBBBB5",
            "BBBBBB6", "BBBBBB7", "BBBBBB8"
        };
    }
    public static string[] GetItems(string filter)
    {
        Thread.Sleep(3000);
        return data.Where(d => d.StartsWith(filter)).ToArray();
    }
}



Now we can build our very simple form to highlight the concept. Go back the form, add a timer attribute using the System.Threading.Timer and not the System.Windows.Forms.Timer (very important).



private System.Threading.Timer timer;
Then create a thread safe method to populate the ListBox based on the textbox value. By thread-safe, I actually UI-thread safe.
public void LoadData() 
{
    if (this.InvokeRequired)
    {
        this.Invoke(new Action(LoadData));
    }
    else
    {
        this.listBox.Items.Clear();
        this.listBox.Items.AddRange(VerySlowDataAccess.GetItems(textBox.Text));
    } 
}
Next step is to initialize the timer in the Form constructor. We will hook it up with our LoadData method and set the timeout to Infinite to deactivate it (for now):
public MainForm()
{
    InitializeComponent();
    timer = new System.Threading.Timer((c) => LoadData(), null, Timeout.Infinite, Timeout.Infinite);
}
Last but not least, as you may expect, we will add the handler for the TextChanged event of the TextBox.

private void textBox_TextChanged(object sender, EventArgs e)
{
    // 250ms is the maximum time between key strokes before the delegate is called (LoadData in that case)
    timer.Change(250, Timeout.Infinite);
}
So, what is happening here? Basically what we do is simply reset the timer every time the text is changed. Then after the 250 ms delay and if no key was pressed, the delegate will be called, thus refreshing the ListBox. This works because we always set the timer's repeat time to Infinite, it will fire only once after a key stroke sequence. The delay is arbitrarily set to 250 ms. Based on my own experience, it is sufficient for a regular user (with Internet-grade typing skills) and it is not so long as to be noticeable. Possible improvements:
  • Prevent the load to be triggered again until it is finished. A thread-safe boolean in the LoadData method can do the job here.
  • Encapsulate all this behavior into a black-box control.

02 February 2010

Visual Studio 2010 Training Kit

For those who are looking for a good place to start with .NET 4.0 and Visual Studio 2010, you can download from Microsoft a complete training kit containing a whole lot of first class information about the next major .NET release and its sidekick. :)

Here is the link to the download page : http://www.microsoft.com/downloads/details.aspx?FamilyID=752CB725-969B-4732-A383-ED5740F02E93&displaylang=en