2010-05-21

Writing a custom trigger for CruiseControl.Net

I’ve had some problems regarding the triggering of our nightly builds and has not been able to solve this with the standard triggers that comes with ccnet (using v1.4.4). So finally I took some time (night time, kids are sleeping and there is peace and quiet in the house :) and wrote a trigger that fixed the issue at hand. I will come to the trigger in just a moment but I’ll take some time to explain our ccnet setup.

Interval builds

All our projects are built using CruiseControl.Net and for each project we setup two builds. One interval build that triggers when the source has been changed. This build uses the intervalTrigger that checks the source repository at regular intervals. The interval build compiles the project and performs unit testing and provides feedback to the programmer. This is quite standard setup I guess so there’s no rocket science here…

Nightly builds

The other build is a nightly build that (in addition to the tasks for the interval build) performs analysis, generates documentation and installation packages ready to deploy to a target system. It also creates deliverable folder in a specific ”drop zone” for the project. This folder is named using the label of the nightly build.

Labels

All projects uses the project name and build no as label (like MyProject – 234). We reuse the build number of the interval build for the nightly build so if the last interval build was ”MyProject – 234”, the next time the nightly build is triggered the label for the nightly build is ”MyProject-Nightly – 234”. This creates a side effect that if the nightly build is triggered twice and the interval build hasn’t been triggered, the label is the same. If you remember the drop zone folder it uses the label of the nightly build as a name so if it is triggered twice using the same label, it tries to overwrite the drop zone folder.

This is a big no-no and to solve this I added a nant task that fires in the beginning of the nightly build that fails if the drop zone folder exists.

Triggering the nightly build

So to avoid failing the nightly build, it should only be triggered if an interval build has successfully been completed and the nightly build for that interval build hasn’t been performed.

Normally nightly triggers are defined to run at a specific time every night using a ScheduleTrigger that fires at that time if any changes to the source has been made during the day. In combination with a projectTrigger that only fires if the interval build has been successful we can accomplish the trigger to fire only if the interval is successful.

Problem with projectTrigger

There is a limitation to the projectTrigger that affects our setup severely. It doesn’t remember the project status after the ccnet server has restarted. This causes the nightly projects to be rebuilt after a server restart. Since we have lots of projects on the server, we need to restart it atleast once a week and this causes the nightly builds to fail.

New trigger?

To solve this we need another way to trigger the build. The first solution that comes to mind is if there was a trigger that could check if a specific folder (the drop zone folder in this case) was missing, we could replace the projectTrigger with a ”Missing folder trigger”.

Perhaps could look like this.

<missingFolderTrigger path="Z:\PublishedBuilds\MyProject-Nightly\1.0.0.234"/>
Problem here is that we don’t know the last folder name. The ”1.0.0.234”comes from the file version for our compiled assemblies.
The first part of the version number can be any numbers and the 234 refers to the build number of the project. This changes for each build so we need a way to look this up.

Final design

I speak highly of the KISS principle, but I’m also great at creating generic things and sometimes this ”generic gene” gets some overhand (and sometimes also get way out of hand, but I think I found a good level this time :)
So the final design was a trigger syntax like below:
<fileExistsTrigger
triggerOnMissing="true"
seconds="30"
path="Z:\PublishedBuilds\MyProject-Nightly\"
match="\d+\.\d+\.\d+\.[Regex.Match([Projects(MyProject).LastSuccessfulBuildLabel],\d+$)]"/>
  • This trigger is now a more ”generic” one allowing us to trigger on both files and directories.
  • The ”triggerOnMissing” allows us to trigger on both exists and not exists.
  • It’s based on an interval trigger and the ”seconds” parameter allows us to set on how often a file/directory should be checked.
  • The ”path” allows us to set the path that should be checked. If we only need to check a file/directory that we know the name of, this is enough. End with a \ to check a directory.

The match attribute

The ”match” allows us to perform a regular expression search that searches for a match for a directory/file in the supplied path.
  • The \d+ will match one or more digits
  • The \. will match a dot.
  • Repeating this match three times \d+\.\d+\.\d+\. will match the ”1.0.0.” part of the file (or any other valid version major.minor.build. combo)
Now the fun part begins :). I added some syntax to perform string operations and replacements. Any valid command is surrounded by [] and are replaced before the file/directory regex matching begins.
In this example we have two commands, one property and one method call. Properties are evaluated first so [Projects(MyProject).LastSuccessfulBuildLabel] allows us to instruct the trigger to lookup the project named ”MyProject” and return the LastSuccessfulBuildLabel. In this case this would be ”MyProject – 234”.
The method call would look like this after the property replacement. [Regex.Match(MyProject - 234,\d+$)]. This will call the Regex.Match method and return the matched pattern. In this case \d+$ will match the last digits in the ”MyProject – 234” and would return ”234”.
So the final match attribute will be \d+\.\d+\.\d+\.234 and would in this case find a folder named 1.0.0.234. If the folder is missing, the build will be triggered, but if it is there, no trigger is fired. If the interval build fires and is successful, the LastSuccessfulBuildLabel increases and is 235. When the nightly build is triggered again, the folder is missing and the trigger will fire.

Writing the trigger

The first part to think about when writing extensions/plugins to ccnet is that the assembly must be named ccnet.*.plugins.dll otherwise ccnet wont find it. Secondly, the trigger class needs to be decorated with the ReflectionTypeAttribute to tell the class what the xml element name for the class looks like (like [ReflectorType("fileExistsTrigger")]). Every field/property that should be read into the trigger should also be decorated with the ReflectorPropertyAttribute (like [ReflectorProperty("path", Required = true)]). I mainly looked at the ProjectTrigger that comes with ccnet and added stuff along the way.
Beware that the code don't fix all special cases, like if your project name contains , ) or other characters that messes up the expressions, but this is what I needed (remember TDD).
You can use the code below as it is or modify it to your needs. If you want the whole solution (including unit tests), just drop me a mail.
Good luck :)
using System;
using System.Collections.Generic;
using System.Linq;
using ThoughtWorks.CruiseControl.Remote;
using ThoughtWorks.CruiseControl.Core.Triggers;
using ThoughtWorks.CruiseControl.Core.Util;
using Exortech.NetReflector;
using System.Text.RegularExpressions;
using System.IO;

namespace Meridium.CruiseControl.Net.Triggers {
[ReflectorType("fileExistsTrigger")]
public class FileExistsTrigger : IntervalTrigger {
/// <summary>
/// If the trigger should be active if the file is missing, default is false
/// </summary>
[ReflectorProperty("triggerOnMissing", Required = false)]
public bool TriggerOnMissing;
/// <summary>
/// The url to the ccnet server, defaults to a local ccnet installation tcp://localhost:21234/CruiseManager.rem
/// </summary>
[ReflectorProperty("serverUri", Required = false)]
public string ServerUri;
/// <summary>
/// The file/directory path to see if it exists or not
/// </summary>
[ReflectorProperty("path", Required = true)]
public string Path;
/// <summary>
/// Optional parameter to use if a regular expression should be used to match a file or directory in the path. Default is an empty string.
/// </summary>
[ReflectorProperty("match", Required = false)]
public string Match;

private readonly ICruiseManagerFactory _managerFactory;

public IDictionary<string, ProjectStatus> ProjectStatus {
get {
lock (ProjectStatusLock) {
//if cache has timed out, clear cache.
if (_projectStatus != null && DateTime.Now > _cacheValidUntil) {
_projectStatus = null;
Log.Debug("Cache was deleted, was valid until " + _cacheValidUntil.ToString("yyyy-MM-dd HH:mm:ss,fff"));
}
if (_projectStatus == null) {
Log.Debug("Updating ProjectStatus cache from server: " + ServerUri);
_projectStatus = new Dictionary<string, ProjectStatus>();
foreach (ProjectStatus status in _managerFactory.GetCruiseManager(ServerUri).GetProjectStatus()) {
_projectStatus.Add(status.Name, status);
}
_cacheValidUntil = DateTime.Now + CacheTime;
Log.Debug("Cache valid until " + _cacheValidUntil.ToString("yyyy-MM-dd HH:mm:ss,fff"));
}
return _projectStatus;
}
}
}
private static DateTime _cacheValidUntil = DateTime.MinValue;
private static readonly TimeSpan CacheTime = new TimeSpan(0, 10, 0);
private static Dictionary<string, ProjectStatus> _projectStatus;
private static readonly object ProjectStatusLock = new object();

public FileExistsTrigger()
: this(new DateTimeProvider(), new RemoteCruiseManagerFactory()) {
}

public FileExistsTrigger(DateTimeProvider dtp, ICruiseManagerFactory managerFactory)
: base(dtp) {
ServerUri = "tcp://localhost:21234/CruiseManager.rem";
_managerFactory = managerFactory;
}
public override IntegrationRequest Fire() {
//only check on intervals
if (base.Fire() != null) {
try {
Log.Debug(string.Format("More than {0} seconds since last integration, checking url.", IntervalSeconds));
if (FileExists() != TriggerOnMissing) {
Log.Debug("Trigger matched, fire IntegrationRequest");
return new IntegrationRequest(BuildCondition, Name);
}
} catch (Exception ex) {
Log.Error(ex);
} finally {
IncrementNextBuildTime();
}
}
return null;
}
private string HandleProjectPropertyMatches(Match m) {
string projectName = m.Groups["projectName"].Value;
string property = m.Groups["property"].Value;
var ps = GetCurrentProjectStatus(projectName);
switch (property.ToLower()) {
case "lastsuccessfulbuildlabel":
return ps.LastSuccessfulBuildLabel;
case "name":
return ps.Name;
case "buildstatus":
return ps.BuildStatus.ToString();
default:
throw new NotImplementedException(string.Format("Support for property {0} is not implemented yet!", property));
}
}
private static string HandleRegexMatchMatches(Match m) {
string input = m.Groups["input"].Value;
string pattern = m.Groups["pattern"].Value;
Match match = Regex.Match(input, pattern);
return match.Success ? match.Value : string.Empty;
}
private bool FileExists() {
string fp = TranslateValue(Path);
string match = TranslateValue(Match);
if (!string.IsNullOrEmpty(match)) {
if (!fp.EndsWith(@"\"))//"
fp += @"\";//"
var dir = new DirectoryInfo(fp);
if (!dir.Exists) {
Log.Debug(string.Format("Matching path {0} failed. Directory does not exist.", fp));
return false;
}
foreach (var fs in dir.GetFileSystemInfos().Where(fs => Regex.IsMatch(fs.Name, match))) {
Log.Debug(string.Format("Match successful with fileSystemInfo {0}", fs.FullName));
return true;
}
Log.Debug(string.Format("No match for {0}", match));
return false;

}
bool isDirectory = fp.EndsWith(@"\");//"
bool exists = isDirectory ? Directory.Exists(fp) : File.Exists(fp);
Log.Debug(string.Format("Checking if {0} {1} exists: {2}", (isDirectory ? "directory" : "file"), fp, exists));
return exists;
}
/// <summary>
/// Translates the supplied value and expands all methods and variables
/// </summary>
/// <param name="val">The value to translate</param>
/// <returns>The translated value</returns>
private string TranslateValue(string val) {
//handle replacements...
//like [Projects(MyProject).LastSuccessfulBuildLabel]
val = Regex.Replace(val, @"\[Projects\((?<projectName>[^\)]+)\)\.(?<property>[^\]]+)\]", HandleProjectPropertyMatches, RegexOptions.IgnoreCase);
//handle Regexp
//like [Regex.Match(string,pattern)]
val = Regex.Replace(val, @"\[Regex\.Match\((?<input>[^,]+),(?<pattern>[^\)]+)\)\]", HandleRegexMatchMatches, RegexOptions.IgnoreCase);

return val;
}
private ProjectStatus GetCurrentProjectStatus(string project) {
if (!ProjectStatus.ContainsKey(project)) {
throw new NoSuchProjectException(project);
}
return ProjectStatus[project];
}
}
}

2010-04-13

Adding buttons to the TinyMce editor in EPiServer 6 programmatically

While developing ImageVault we needed to add buttons to insert images in the TinyMce editor that ships with EPiServer CMS 6. The buttons are decorated with a TinyMCEPluginButtonAttribute and they can be added manually by entering admin mode and modifying the settings for the XHTML property (see Oskars post "Add functionality to the rich text editor in EPiServer cms 6").
To do this by code this was rather simple since EPiServer has a great Api (even that it could contain more examples)...

The settings are stored in the EPiServer database and is accessed using the PropertySettingsRepository class.
To get the current settings for the TinyMce editor, just create a repository and use the GetDefault method

PropertySettingsRepository p = new PropertySettingsRepository();
PropertySettingsWrapper defaultSettings = p.GetDefault(typeof(TinyMCESettings));

The PropertySettingsWrapper contains metadata for the settings, like DisplayName, Id etc.
It also contains the specific setting for the TinyMce editor in the property PropertySettings.

To create a new settings for the TinyMce editor with some buttons added we can use the code below.

PropertySettingsRepository p = new PropertySettingsRepository();
PropertySettingsWrapper defaultSettings = p.GetDefault(typeof(TinyMCESettings));

//create a copy of the default settings
PropertySettingsWrapper copy = defaultSettings.Copy();
copy.DisplayName+="(My copy)";
TinyMCESettings settings = copy.PropertySettings as TinyMCESettings;

//create a new toolbar row for my buttons
ToolbarRow row = new ToolbarRow();
settings.Toolbars.Add(row);

//the buttons are added using the name defined in the
//ButtonName property of the TinyMCEPluginButtonAttribute.
row.Buttons.Add("mybutton1");
row.Buttons.Add("separator");
row.Buttons.Add("mybutton2");

//Save the copy as a global setting
p.SaveGlobal(copy);

//Set it as default
p.SetDefault(copy.Id);

Quite easy. The separator button is included in EPiServer 6 the others need to be added using classes decorated with the TinyMCEPluginButtonAttribute.

Note!
To actually use this code in EPiServer you need to insert it somewhere :) The easiest way is to create a InitializationModule class (implement IInitializaionModule and mark it with the InitializationModuleAttribute). Then in the Initialize method, add your code to the InitializationEngine.InitComplete event.

public void Initialize(InitializationEngine context) {
context.InitComplete+=(sender,args)=> MyInstallButtonsMethod();
}

More about EPiServer Initialization on episerver world.

2009-10-16

Converting word doc to pdf in CruiseControl.Net

Today I needed to automate the process where we create PDF files out of our client documentation in Microsoft Word format. Since up to now we have done it manually by opening the document and printing to a installed PDF printer. Quite tedious work.
After some searching I start to realize that there are many PDF creators around; and most of them uses Ms word and prints using a pdf printer (just as we use to do, but by automation).
The problem is that I don't want to install printers or the Ms Office package on our build server (if I can avoid it). Another wish is that the solution should be free and open source.
So finally I stumbled upon this article by Graham J. Williams that uses OpenOffice.org to do the conversion and a custom macro to allow the conversion to be called from the commandline.
REM  *****  BASIC  *****

Sub ConvertWordToPDF(cFile)
cURL = ConvertToURL(cFile)

' Open the document.
' Just blindly assume that the document is of a type that OOo will
' correctly recognize and open -- without specifying an import filter.
oDoc = StarDesktop.loadComponentFromURL(cURL, "_blank", 0, Array(MakePropertyValue("Hidden", True), ))

cFile = Left(cFile, LastIndexOf(cFile,".")) + "pdf"
cURL = ConvertToURL(cFile)

' Save the document using a filter.
oDoc.storeToURL(cURL, Array(MakePropertyValue("FilterName", "writer_pdf_Export"), ))

oDoc.close(True)

End Sub

Function LastIndexOf ( cText as String, cMatch as String) As Integer
lastIndex = 0
pos=0
Do
pos = InStr(pos+1,cText,cMatch)'
If pos >0 Then
lastIndex = pos
End If
Loop While pos >0
LastIndexOf()=lastIndex
End Function

Function MakePropertyValue( Optional cName As String, Optional uValue ) As com.sun.star.beans.PropertyValue
Dim oPropertyValue As New com.sun.star.beans.PropertyValue
If Not IsMissing( cName ) Then
oPropertyValue.Name = cName
EndIf
If Not IsMissing( uValue ) Then
oPropertyValue.Value = uValue
EndIf
MakePropertyValue() = oPropertyValue
End Function
I modified it a bit to allow correct handing of extensions.
To call it from a windows command prompt just enter
c:\program files\OpenOffice.Org 3\Program\swriter.exe -invisible "macro:///Standard.Module1.ConvertWordToPDF(c:\temp\My word document.doc)"
I incorporated it in our cruisecontrol.net nant scripts so all solutions that needs pdf conversions in the build chain can have it.
Great!

2010-05-11 Update: Fixed a bug with filenames that contained more than one period (.), added the LastIndexOf function

2009-08-06

Bugfix for Documentator Macros (VB)

A small bug fix for the auto documentation feature for VB.Net.

Thanks for the report Sam; what has it been, nearly 2 years since you reported the bug finally I've come about... ( You said it wasn't urgent... you'll probably reconsider next time ;)


When autodocumenting a Sub or function that is connected to an event, an error occurs.


The fix is included in version 2.5.3.0 that can be downloaded from http://dan.meridium.se/DocumentatorMacros/v2.5.3.0/Meridium.rar.


(For more information about the Documentator Macros, see http://www.codeproject.com/KB/cs/documentatormacros.aspx )

2009-07-27

Mvc for Winforms - Mapping the View event to the Controller action Part III

Long time since part I and II were posted, but I've been quite busy lately so I haven't found time (or prioritized) to write the last part, but here we are :)

To recap, in part I I explained about the background of the issue, that I was writing a small winforms MVC framework. The goal of the framework was to be able to instruct the controller to listen to a component in the view and when the component called an event, a specific method would be called.
For example, the call below would register the click event from the saveButton
Controller.RegisterAction(saveButton,"Save");
(in later editions I have also added support for lamda expressions instead of method names, and thus almost abandoned the previous)
Controller.RegisterAction(rotateRightButton,() => Controller.Rotate(90));
Controller.RegisterAction(rotateLeftButton,() => Controller.Rotate(-90));
In part II I wrote about how find and listen to the event of the component.
In this section I will explain how I call the method.

What we ended with in the last post was that we had registered the controllers event to call the ExecuteAction method of the Controller class.
public void ExecuteAction(object source, object eventArgs, ActionData actionData)
or in the latter case using lambda expressions (or action)
public void ExecuteAction(object source, object eventArgs, Action action)
In this case the Action is the easiest to implement. Just call the Invoke method of the Action instance and you are done.
public void ExecuteAction(object source, object eventArgs, Action action) {
action.Invoke();
}
The no Action ExecuteAction method (almost abandoned way )
The ActionData variant is somewhat more tedious but this is how ASP.NET MVC does it.
First we need to find the method to call. The ActionData contains the name of the method. We now needs to find the method of the controller that matches that name. MVC for ASP.NET uses the ActionSelectorClass. The ActionSelectorClass is responsible for retrieving the MethodInfo for an action by passing in the name of the action. It support action aliases and other things so look it up for a great example.
In short it uses reflection to get all methods, filters out irrelevant methods and returns the one that matches the name.
Example (really shortened example, lookup ActionSelectorClass in the ASP.NET MVC framework for a complete example.)
//get all methods
MethodInfo[] array = ControllerType.GetMethods(BindingFlags.InvokeMethod | BindingFlags.Public | BindingFlags.Instance);
//convert to a dictionary
ILookup<string,MethodInfo> methodLookup = array.ToLookup(method => method.Name, StringComparer.OrdinalIgnoreCase);
//return the matching MethodInfo
MethodInfo action = methodLookup[actionName];
Now we have the MethodInfo to invoke, only to calculate the parameters to pass to the method is left.

The action is invoked using the Invoke method of the MethodInfo class and we need to pass the instance to invoke it on (the controller instance) and the object array containing the parameters to pass to the method/action.
Example
action.Invoke(controller,parameterValues);
If the action don't have any parameters we just pass null, but if we do have parameters we need to assign values for them.
Remember: we wanted to be able to register the call
Controller.RegisterAction(createBoldTextButton, "CreateText", new {name="Bold", type=4}); 
To call the action method
public void CreateText(string name, int type) {...}  
using the "Bold" as value for the name parameter and 4 as value for the type parameter.

To succeed with this we need to do some parsing.
  1. Iterate all parameters of the method info.
  2. Handle special cases (like if the parameter is named source and is of type object, then the source value from the event handler method should be passed).
  3. Find a property with a matching name from the value object passed in the RegisterAction.
Iterate all parameters of the method info
Using reflection it's easy to find the parameters.

List<object> objects = new List<object>();
foreach (ParameterInfo info in method.GetParameters()) {
objects.Add(GetParameterValue(info));
}
return objects.ToArray();
Handle special cases
The GetParameterValue method will return the value to pass to the supplied ParameterInfo. In some cases the value to be passed as parameter is not any of the values passed to the values parameter (in the RegisterAction method), like for example, if one would need to get the EventArgs or source parameter passed from the event component, then we would need to handle them. (other customizations can be done here, this is an example)
Type parameterType = parameterInfo.ParameterType;
string parameterName = parameterInfo.Name;

//handle eventArgs and source parameters
if (string.Equals(parameterName, "e", StringComparison.OrdinalIgnoreCase) && _eventArgs != null && parameterType.IsAssignableFrom(_eventArgs.GetType()))
return _eventArgs;
if (string.Equals(parameterName, "source", StringComparison.OrdinalIgnoreCase)&& parameterType.Equals(typeof(object)))
return _source;

//parse the values object
return GetValuesValue(parameterType, parameterName);
Find a matching property of the value object
At last we would try to find a matching property of the value object that matches the parameter type and name. (First we check that the parameter don't matches the whole value object)
//if we don't have a value, return null
if (ValueType == null)
return null;
//if the value is matching, use it as value
if (type.Equals(ValueType))
return _actionData.Values;
PropertyInfo[] valueProperties= ValueType.GetProperties();
//if the value don't have any properties, return null
if (valueProperties.Length == 0)
return null;
//find matching property
PropertyInfo[] possibleMatches = valueProperties
.Where(p => string.Equals(name, p.Name, StringComparison.OrdinalIgnoreCase)
&& type.IsAssignableFrom(p.PropertyType))
.ToArray();
if (possibleMatches == null || possibleMatches.Length == 0)
return null;
if (possibleMatches.Length > 1)
throw new AmbiguousMatchException(string.Format("The value collection contains ambigous match values for the parameter {0} {1}", type, name));
return possibleMatches[0].GetValue(_actionData.Values, null);
Ok, so now we can invoke the method with our array of parameters. (as I said, a bit tedious).
If we compare the two variants,
Controller.RegisterAction(createBoldTextButton, "CreateText", new {name="Bold", type=4});
Controller.RegisterAction(createBoldTextButton, ()=> Controller.CreateText("Bold", 4);
you understand why I have abandoned this last variant and only uses Actions/lambda functions.
The tedious method can still be used if you want to lift the connection between view and controller to a configuration layer. Then the Action method will be hard to implement.

That sums up this last part of my WinForms MVC framework lessons and experiences. Hope that you learned something.

2009-06-24

The start address <https://newhostname.domain.com> cannot be crawled

Have a SharePoint site on one of our servers and have lately added a new hostname to that server to access a new site collection. I started getting a lot of entries in the eventlog using event id 2436 stating:

The start address <https://newhostname.domain.com> cannot be crawled.

Context: Application 'Search index file on the search server', Catalog 'Search'

Details:
Access is denied. Check that the Default Content Access Account has access to this content, or add a crawl rule to crawl this content. (0x80041205)

For more information, see Help and Support Center at http://go.microsoft.com/fwlink/events.asp.


Another symptom on this error is that if you open a web browser on the server itself and try to navigate to the url, you'll only get an authentication dialog (symptom on a 401 error).

After a bit of searching I found a solution that I recognized. The following kb article http://support.microsoft.com/kb/971382 provided the solution to add the new hostname to the registry key HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control\Lsa\MSV1_0\BackConnectionHostNames .

When I started regedit I found that the key already existed and a couple of hostnames already was present. It seems that I had implemented this fix before.
Better to write a post about it so I don't forget it in the future :)

Ps. Don't forget to restart the server after applying the registry change. Ds.

Export and Import Work item queries in TFS projects

I have lately been introducing new WorkItemTypes in our TFS project to handle tests and support cases. With those new types the need for modifying the queries in the current projects was necessary. To modify a query I could have opened them in visual studio and saved them to disk and then saved them individually to each project. Since we are getting a lot of projects (60+) this would be a quite tedious task to update 10+ queries in each project individually.
To solve this administrative plague I wrote a small commandline program that allows you to export and import queries from a project.

Example
To list all queries in a project
TFSQueryUtil.exe /t https://tfsserver.domain.com:8143 /p "My Tfs Project"

This will list every Work Item Query by Scope, Name and description

To export all queries to the current folder
TFSQueryUtil.exe /t https://tfsserver.domain.com:8143 /p "My Tfs Project" /o Export /q *

This will export each query using the name of the query (plus the .wiq extension) as name of the exported file. If you want to only export one query, use the example below.
TFSQueryUtil.exe /t https://tfsserver.domain.com:8143 /p "My Tfs Project" /o Export /q "My query"

To import all *.wiq files to a project as Team queries
TFSQueryUtil.exe /t https://tfsserver.domain.com:8143 /p "My Tfs Project" /o Import /f *.wiq /qs Public

This command will use the filename (without the .wiq extension) as name for the imported query.
You can also specify a Description of the query by using the /qd switch (best for use with one query imports)

The format of the wiq files follows the WorkItemQuery schema but since the schema only includes the query and no meta data (like name, scope or description) this has to be provided as parameter switches... (Perhaps something to fix in TFS 2010)

If you need more help add the /? parameter.

I haven't released the program as open source but feel free to use it if you have need for it.
Can be downloaded from http://dan.meridium.se/TfsQueryUtil.rar
To use it you need to have Team Foundation Explorer 2008 installed. (needs the tfs dlls in the GAC).

2009-09-28 Update; small bugfix
* Now writes xml files as utf-8 (was an in consequence between xml notation and file encoding)