# Friday, May 23, 2008

Yesterday I blogged about detecting IIS version from MSBuild, or more accurately, OS version.  Detecting whether my MSBuild script was running on XP allowed me to skip the application pool creation that would otherwise error out the build script.

What I didn't expect was that the website creation task would actually succeed in creating a secondary website in IIS 5.1.  I vaguely remember from other blog posts how its possible to programmatically create secondary websites in IIS 5.1, but didn't realize it was so easy.  Just for the record the following snippet also works against IIS 6 and 7.  The following build script does exactly that.

image

That takes care of new site creation, only optionally creating the site if it doesn't already exist, but always modifying it to ensure consistent settings.  The final piece we need is to stop running "Default Web Site" and start the new site.  IIS 5.1 can only run one site at a time so this is why we stop the default site and explicitly start the new site.

image

Once the script finishes we can check in inetmgr to see the new IIS 5.1 site right along side the default web site.

image

Are you wondering why I would go to all this trouble of monkeying around with IIS for development?  Well you should be.  The reason is that the Visual Studio web server does not support ISAPI filters, and since I'm doing URL rewriting at the ISAPI level, running under IIS is the only option.

Friday, May 23, 2008 3:43:45 PM (GMT Standard Time, UTC+00:00)  #    Disclaimer  |  Comments [0]  | 
# Thursday, May 22, 2008

I've been using the Microsoft SDC tasks to deploy IIS websites, and for the most part this has worked well once I grabbed the latest source from CodePlex; I ran into a few bugs that have been fixed since the last release.

Anyways, I've run into an annoyance with regards to local development builds on Windows XP.  Most of our devs are still using XP for their development platform which as you may know uses an old version of IIS - IIS 5.1.

  • No app pools
  • Single site only
  • No SSL

So, what does this have to do with the SDC tasks?  Well, I want the MSBuild script to fail gracefully with an intelligent error message when run on Windows XP.  Something like:

This version of IIS is not supported, IIS 6.0+ is required.

I couldn't find a decent built-in supported way in MSBuild to get the IIS version, let alone the operating system version.  You would think this would be supported without resorting to C#, but I couldn't find a way.  So what I ended up doing was using the command line "ver" command and piping that to a text file and then reading it back in.  Here's what that looks like:

MSBuild_OSVer

Thursday, May 22, 2008 4:02:50 PM (GMT Standard Time, UTC+00:00)  #    Disclaimer  |  Comments [2]  | 
# Sunday, January 13, 2008

I created a new utility called ConfigPoke for creating application configuration files from a template and one or more property files.  The property files allow you to create a set of property files in which each subsequent file overrides any settings in the previous.

The input property files are just plain text files with key value pairs, for example:

dbConnectionString        = server=localhost;database=Northwind;integrated security=SSPI
sessionConnectionString = server=localhost;database=AspState;integrated security=SSPI
smtpServer                  = localhost

 

This allow me to define a base set of shared developer properties and then I can define my own property file for me which overrides just properties, like the local database connection string.  I also define property files for each of my environments I deploy the application too: dev, test, release, live.

This utility integrates tightly with MSBuild and Visual Studio 2005/2008 using it's own target file.  There is also a command line interface for the application that can be used from NAnt or in your deployment process (NSIS in my case).

Per developer configuration happens automatically and is always up to date with the input template and property files with Visual Studio integration.  The output configuration files are conditionally built only if the input templates file or input property(s) files are changed, just like any regular source file in Visual Studio. If a Rebuild action is initiated in Visual Studio, the output configuration files are always rebuilt, even if the input files have not changed.

 

I've been using this utility for the past week at work, and it has worked really well.  Edit a property and everything just works.  Machine specific settings have proven invaluable especially on my laptop which has a lot of non-standard settings because I usually work disconnected on it.

 

You can download the MSBuild target DLL, console EXE, and MSBuild targets file from Google Code.  The code and the binary is release under the Apache 2.0 license.

Visual Studio 2005/2008 Integration Outline

  1. Import Sneal.Build.ConfigPoke.targets file into your VS project file.
  2. Create a ConfigTemplateFiles ItemGroup.
  3. Create a ConfigPropertyFiles ItemGroup.

Detailed Visual Studio 2005/2008 Integration Instructions

Include the Sneal.Build.ConfigPoke.targets file in your Visual Studio project file (csproj) using the following element:

<Import Project="Sneal.Build.ConfigPoke.targets" />

With the ConfigPoke MSBuild targets file included, the configuration building will automatically be hooked into the Visual Studio clean, build, rebuild process.

The Sneal.Build.ConfigPoke.targets file requires two MSBuild item groups as input: ConfigTemplateFiles?, and ConfigPropertyFiles?. These must be specified in your MSBuild (csproj) project file. Each ConfigTemplateFiles? item should be the destination filename + some arbitrary extension, I use '.template'. You may also probably want to set the "InProject?" item meta data to false so these templates or property files don't show up in VS solution explorer.

Here's an example ConfigTemplateFiles? ItemGroup? that you would put into your csproj file:

<ItemGroup>
  <ConfigTemplateFiles Include="$(MSBuildProjectDirectory)\Configs\web.config.template"/>
  <ConfigTemplateFiles Include="$(MSBuildProjectDirectory)\Configs\windsor.config.template" />
</ItemGroup>

To create user and machine specific overrides, you can include a base properties file and then optionally include a per user and per machine config, if they exist. This would allow each developer to create their own override properties file which can then be optionally checked into source control. To automatically set this up, you can take advantage of the built in MSBuild properties: USERNAME and COMPUTERNAME.

Here's an example ConfigPropertyFiles? ItemGroup? that you would put into your csproj file:

<ItemGroup>
  <ConfigPropertyFiles Include="$(MSBuildProjectDirectory)\Properties\App.Properties.base"/>
  <ConfigPropertyFiles Include="$(MSBuildProjectDirectory)\Properties\App.Properties.$(USERNAME)" Condition="Exists('$(MSBuildProjectDirectory)\Properties\App.Properties.$(USERNAME)')"/>
  <ConfigPropertyFiles Include="$(MSBuildProjectDirectory)\Properties\App.Properties.$(COMPUTERNAME)" Condition="Exists('$(MSBuildProjectDirectory)\Properties\App.Properties.$(COMPUTERNAME)')"/> 
</ItemGroup>

Note: You cannot pass in non-existant property files to the ConfigPoke utility, hence the Condition check above.

 

Optionally you can specify a ConfigPokeDirectory? and a ConfigOutputDirectory? property to override the default directories. The ConfigPokeDirectory? property should point to the directory where Sneal.Build.ConfigPoke.MSBuild.dll is located on your machine, if not specified this property will default to the current MSBuild project file directory. The ConfigOutputDirectory? property should point to the directory where the output configuration files are written too. If not specified this property will default to the current MSBuild project file directory.

 

For additional usage see the example in SVN, http://sneal.googlecode.com/svn/trunk/ConfigPoke/Sneal.Build.ConfigPoke.Example/Sneal.Build.ConfigPoke.Example.csproj

Sunday, January 13, 2008 1:39:35 AM (GMT Standard Time, UTC+00:00)  #    Disclaimer  |  Comments [0]  | 
# Monday, January 07, 2008

I have no proof of this other than my changes weren't showing up, but it seems like MSBuild project files that are directly imported into a project are cached in Visual Studio between builds.  I had to make changes to my imported build script, exit Visual Studio, then restart VS before my changes would take affect.  Luckily if you build from the command line this doesn't happen.

Monday, January 07, 2008 8:45:28 PM (GMT Standard Time, UTC+00:00)  #    Disclaimer  |  Comments [0]  | 

I often need to refer to tools and libraries in my SVN repository that live in a folder not underneath the current project folder.  For instance I may need to use NUnit from trunk\store\web, but NUnit lives under trunk\tools\nunit\bin. 

Relative to the trunk, I know where to find NUnit, but where's the trunk?  The trunk may be checked out to c:\source\mytrunk or e:\work, or even the user's documents and settings folder.  This means I must use relative paths to NUnit from the current project directory, but this gets tricky if I have an external MSBuild target file that is shared between projects.

The only way to tell the shared imported targets file where things are relative to it, is to have the parent MSBuild script to pass in a property telling it where the root of the source control folder is (or where the tools directory is).  In my case this is the 'SourceDirectory' property which should point to the root of my source control folder hierarchy.  This gets annoying after a while since everywhere I include the shared targets file I first must create the SourceDirectory property first, like so:

<PropertyGroup>
  <SourceDirectory>$(MSBuildProjectDirectory)\..\..</SourceDirectory>
</PropertyGroup>

 

Yes this works pretty well, but it makes the shared imported targets file not-so-self contained and I have to repeat this over and over again in every project file that wants to include the shared targets files, which in my case is every single csproj file.  I have to do this since my project files live at varying depths in the folder hierarchy.  Instead of specifying this property everywhere, I've put this at the top of my shared targets file:

<PropertyGroup Condition="'$(SourceDirectory)' == ''">
  <SourceDirectory Condition="'$(SourceDirectory)' == '' And Exists('$(MSBuildProjectDirectory)\Master.proj')">$(MSBuildProjectDirectory)</SourceDirectory>
  <SourceDirectory Condition="'$(SourceDirectory)' == '' And Exists('$(MSBuildProjectDirectory)\..\Master.proj')">$(MSBuildProjectDirectory)\..</SourceDirectory>
  <SourceDirectory Condition="'$(SourceDirectory)' == '' And Exists('$(MSBuildProjectDirectory)\..\..\Master.proj')">$(MSBuildProjectDirectory)\..\..</SourceDirectory>
  <SourceDirectory Condition="'$(SourceDirectory)' == '' And Exists('$(MSBuildProjectDirectory)\..\..\..\Master.proj')">$(MSBuildProjectDirectory)\..\..\..</SourceDirectory>
  <SourceDirectory Condition="'$(SourceDirectory)' == '' And Exists('$(MSBuildProjectDirectory)\..\..\..\..\Master.proj')">$(MSBuildProjectDirectory)\..\..\..\..</SourceDirectory>
  <SourceDirectory Condition="'$(SourceDirectory)' == '' And Exists('$(MSBuildProjectDirectory)\..\..\..\..\..\Master.proj')">$(MSBuildProjectDirectory)\..\..\..\..\..</SourceDirectory>
  <SourceDirectory Condition="'$(SourceDirectory)' == '' And Exists('$(MSBuildProjectDirectory)\..\..\..\..\..\..\Master.proj')">$(MSBuildProjectDirectory)\..\..\..\..\..\..</SourceDirectory>
  <SourceDirectory Condition="'$(SourceDirectory)' == '' And Exists('$(MSBuildProjectDirectory)\..\..\..\..\..\..\..\Master.proj')">$(MSBuildProjectDirectory)\..\..\..\..\..\..\..</SourceDirectory>
  <SourceDirectory Condition="'$(SourceDirectory)' == '' And Exists('$(MSBuildProjectDirectory)\..\..\..\..\..\..\..\..\Master.proj')">$(MSBuildProjectDirectory)\..\..\..\..\..\..\..\..</SourceDirectory>
</PropertyGroup>

 

What this does is look for a Master.proj file in each directory traversing upwards until it finds one, at which point the SourceDirectory property becomes set.  I can do this since I know that I only ever have one Master.proj file in the root of my SVN repository, and I never have a project more than 4 folders deep (although here I support 9 levels)  It's kind of cheesy, but it works well and saves me from having to do anything other than just import the shared targets file.

<Import Project="$(MSBuildProjectDirectory)\..\..\utils\gp.vs.targets" />

 

Hopefully Microsoft will add a well known property that allows you to get the current directory of the current build file, rather than just the parent script directory like MSBuildProjectDirectory does now.

Monday, January 07, 2008 7:46:42 PM (GMT Standard Time, UTC+00:00)  #    Disclaimer  |  Comments [0]  | 
# Tuesday, September 25, 2007

When extending existing MSBuild targets, its often wise to append to the build target properties rather than override an existing target.  Its appears there's no way to override a target and then call the base target, this really annoys me because often I just want to add additional behavior to the existing target.  It seems the way around this is to add a property or itemgroup to the DependsOn attribute of a target, even if the itemgroup is empty, it just leaves a place open in the future for extensibility.  Here's in example from my shared targets file:

<Target Name="CreateDiskImage" DependsOnTargets="@(BeforeDiskImage);Build">

Now in some script which imports this targets file I can just declare one or more BeforeDiskImage items and they will automatically get run before the CreateDiskImage target.  If you look at the the MS providded targets files you will see this is done in a similar manner all over the place.  Here's a good reference for the C# targets you can hook into:

http://blogs.msdn.com/msbuild/archive/2006/02/10/528822.aspx

Tuesday, September 25, 2007 4:35:04 PM (GMT Standard Time, UTC+00:00)  #    Disclaimer  |  Comments [0]  | 
# Friday, August 31, 2007

For reference, here's how you update your app.config appSettings using MSBuild Community Tasks XmlUpdate.  This example actually batches several app.configs that need changing.

<XmlUpdate

  XmlFileName="%(ConfigFilesToCobrand.FullPath)"

  XPath="//configuration/appSettings/add[@key='MarketSegmentID']/@value"

  Value="$(MarketSegmentId)" />

Friday, August 31, 2007 8:12:56 PM (GMT Standard Time, UTC+00:00)  #    Disclaimer  |  Comments [0]  | 

I think most people probably know how to version C# projects by now, but for my own future reference here is how I'm currently doing it. 

In my project's csproj file I append the following VersionProject target to the BuildDependsOn project.  You could override the BeforeBuild target, but appending a dependent target is safer according to Sayed Hashimi.  The following goes directly into each of my csproj files:

<PropertyGroup>

  <SourceDirectory>$(MSBuildProjectDirectory)\..\..\..</SourceDirectory>

  <ProjectDirectory>$(MSBuildProjectDirectory)</ProjectDirectory>

</PropertyGroup>

<Import Project="$(SourceDirectory)\utils\global.targets" />

<PropertyGroup>

  <BuildDependsOn>

    VersionProject;

    $(BuildDependsOn);

  </BuildDependsOn>

</PropertyGroup>

 

The VersionProject target is a custom target in my global.targets MSBuild project file which gets included into my csproj file.  This target is using the MSBuild community tasks' AssemblyInfo task.

<Target Name="VersionProject">

  <Message Text="Generating AssemblyInfo.cs Version: $(BuildVersion)"/>

  <MakeDir

    Directories="$(ProjectDirectory)\Properties"

    Condition="!Exists('$(ProjectDirectory)\Properties')" />

  <AssemblyInfo CodeLanguage="CS"

    OutputFile="$(ProjectDirectory)\Properties\AssemblyInfo.cs"

    AssemblyCompany="$(CompanyName)"

    AssemblyProduct="$(ProductName)"

    AssemblyCopyright="$(Copyright)"

    AssemblyVersion="$(BuildVersion)"

    AssemblyFileVersion="$(BuildVersion)"

    AssemblyTitle="$(ProductName)"

    AssemblyDescription="$(ProductName)"/>

</Target>

 

The great thing about this, I can reuse this target over and over again for each of my projects.  Some properties like the BuildVersion are set when running under CC.NET, others like CompanyName, Copyright are set once in the global.targets file as these don't change between projects.  By default my global.targets file will default the ProductName to the MSBuild project file name (minus the file extension), this of course can be overridden in any number of ways - including from CC.NET. 

The nice thing is, this will work when building from the command line with MSBuild.exe or from within Visual Studio.  This last point is important because we don't ever checkin the AssemblyInfo.cs source files to SVN (we set SVN ignore on these since they are autogenerated on every build) and most developers would like to checkout, open the solution, and finally CTRL SHIFT B.  Some OSS projects I've worked on require you to build from the command line first (to generate the AssemblyInfo.cs files) before you can build from Visual Studio; I believe this tends to confuse most .NET developers since most are so IDE dependant.

Another thing to note is that if a developer wanted to build a specifically versioned project from within Visual Studio all they would need to do would be to set an environment variable before starting Visual Studio, specifically: BUILDVERSION=1.0.0.1503.

Friday, August 31, 2007 7:07:50 AM (GMT Standard Time, UTC+00:00)  #    Disclaimer  |  Comments [0]  | 
# Thursday, August 30, 2007

Something I noticed today while cleaning up an MSBuild script is that there seems to be a direct correlation between the load time of bootstrapping MSBuild and your ItemGroups.  I noticed this because I have an ItemGroup that has an exclude to ignore any .svn folders on disk that I set at the parent folder level which contains a lot of files.

<ItemGroup>

  <System32Files

    Include="$(ProjectDirectory)\Libs\SdDriver\**\*.*"

    Exclude="$(ProjectDirectory)\**\$(SvnFolder)\**"/>

</ItemGroup

 

WHen I changed the above Exclude to $(ProjectDirectory)\Libs\**\$(SvnFolder)\** my build script load time was noticeably faster.  The interesting thing is, MSBuild doesn't count the loading of ItemGroups in its calculation of "Time Elapsed," running with either exclude setting produced identical Time Elapsed results - even though there was an obvious difference in run times.

Thursday, August 30, 2007 4:19:50 PM (GMT Standard Time, UTC+00:00)  #    Disclaimer  |  Comments [0]  | 
# Sunday, August 19, 2007

Project structure is probably one of the most important things to get right.  If things are not setup in a consistent manner you will have a hard time managing your software builds.  I find convention over configuration to be of great help especially when creating new builds.  We want the project structure to be organized and easy to use, which means it should be easy for new developers to figure out where things are and where new files should go.  For this series I will be using the fictitious FoodBankTracker application.

I like to use multiple solution files at the root of my source control repository, this allows me to share projects between different solutions as project references are so much easier to deal with than file based assembly references.

The root of the source control repository

In the SharedLibs folder I have all my 3rd party file based assembly references which are used during the build process, things like: NUnit, Castle, NHibernate, RhinoMocks etc.  Other than these 3rd party assemblies, all of my references are project based.

In the Tools folder I keep build tools checked in such as NUnit, Doxygen, and MSBuildCommunityTasks.  The Tools folder differs from the SharedLibs folder in one key aspect, the SharedLibs folder contains assemblies that may be deployed with the built application, where the executables and assemblies in the Tools folder are only used during the build process.   Tools are checked into source control for several reasons:

  • New workstations don't require so many 3rd party tool installs.
  • Upgrades across the team are as easy as svn update.
  • Relative tool paths are fixed, which makes automating builds that much easier.

Tools directory

I also keep a generic global.targets MSBuild file in the Tools folder which gets included in all my MSBuild project files.  Global.targets keeps shared settings and targets that are common to all projects; this saves me time when configuring a new project, but it also allows me to change a tool path or target without touching every single build file in source control.  The global.targets also includes the MSBuildCommunityTasks which contains additional targets that will be used; by including it in global.targets its just one less thing I have to include in each build file.  Most importantly this target file keeps all the paths to the different tools used during the build process, remember we want to keep things consistent across projects.  This target build script also sets some build properties like version, company name, and sets some paths used during the build process.

Before global.targets gets included, the parent build script needs to set the $(SourceDirectory) property which should point to the root of the source control repository.  This path is used to find where things are in the source control repository relative to the trunk.  The parent build file may also want to set a friendly product name property, but this is purely optional.

 

<!-- The SourceDirectory property must be set prior to including this file -->

<!-- The ProductName property should be set prior to including this file -->

 

<PropertyGroup>

  <BuildVersion>$(CCNetLabel)</BuildVersion>

  <BuildVersion Condition="'$(BuildVersion)' == ''">0.0.0.0</BuildVersion>

  <CompanyName>Sneal</CompanyName>

  <Configuration Condition="'$(Configuration)' == ''">Debug</Configuration>

  <ProductName Condition="'$(ProductName)' == ''">$(MSBuildProjectName)</ProductName>

</PropertyGroup>

 

<PropertyGroup>

  <NUnitPath>$(SourceDirectory)\Tools\NUnit\bin\nunit-console.exe</NUnitPath>

  <DevEnvPath>&quot;$(VS80COMNTOOLS)..\ide\devenv.com&quot;</DevEnvPath>

  <DoxygenPath>$(SourceDirectory)\Tools\Doxygen\doxygen.exe</DoxygenPath>

  <MSBuildCommunityTasksPath>

    $(SourceDirectory)\Tools\MSBuildCommunityTasks

  </MSBuildCommunityTasksPath>

</PropertyGroup>

 

<Import Project=

"$(SourceDirectory)\Tools\MSBuildCommunityTasks\MSBuild.Community.Tasks.Targets"/>

 

<PropertyGroup>

  <ArtifactDirectory>$(CCNetArtifactDirectory)</ArtifactDirectory>

  <ArtifactDirectory Condition="'$(ArtifactDirectory)' == ''">

    $(SourceDirectory)\artifacts\$(ProductName)

  </ArtifactDirectory>

  <PublishDirectory Condition="'$(PublishDirectory)' == ''">

    \\buildserver\builds\$(ProductName)\$(BuildVersion)

  </PublishDirectory>

  <TempZipDirectory Condition="'$(TempZipDirectory)' == ''">

    $(ArtifactDirectory)\ZipImage

  </TempZipDirectory>

</PropertyGroup>

 

Notice that this reusable targets file tends to be passive, i.e. it doesn't overwrite any properties if they've already been set.  One thing to note is that non-official CC.NET builds will be built using version 0.0.0.0, which would be used if a developer did a local build.  When CC.NET runs a build script it will set some additional properties, one of which would be build version.  Global.targets include some other paths and reusable targets that are not shown, but we won't talk about the reusable targets just yet.

Besides setting paths to build tools we also set properties for build directories, specifically: $(ArtifactDirectory), $(PublishDirectory), and $(TempZipDirectory).  The ArtifactDirectory property should point to where all build artifacts get spit out to.  Artifacts are things like XML result files from NUnit, MSBuild, and Doxyen output.  If running under CC.NET this will be the CC.NET artifact directory.  The TempZipDirectory property points to where all build output gets copied to, to build up a disk image in the proper layout for the zip file which will contain our ready for deployment build.  Everything in the temp zip directory gets zipped and then copied to the publish directory which is usually a network accessible location on the build server, here it would be \\buildserver\builds\FoodBankTracker\0.0.0.0

The commons folder contains a reusable library that I use between most, if not all, of my solutions.  This contains the actual library and another unit test project.  This project is built in the same fashion as the FoodBankTracker application, but will focus just on the one application.

FoodBankTracker application folder Now for the FoodBankTracker folder, without the FoodBankTracker application there would be no need for everything else we've talked about up to this point.  As you can see in the image, the application is broken out into four main logical areas: core, data, presentation, and web.  You will also notice that the main application and unit test projects are all at the same folder level and the database folder which contains the database scripts used to build the application's database.  There is generally a 1:1 ratio of unit test projects versus normal projects, but the important thing to note is that the test code is in a separate folder and project from the application code. 

All of the projects are class library projects except for the web project and the database project.  The web project is a normal web application project that only contains very thin display logic, while the database is a (.dbp) database project where we keep our SQL scripts. Solution explorer view of FoodBankTracker Inside the solution the different project types are logically organized between the core application and the Tests solution folder.  Separating out projects logically inside the solution instead of on disk allows me to easily move things around in the future without adversely effecting the build scripts.  Also since I've positioned my solution file at the root, I can easily add project references to my Commons library and unit test project.  This also allows me to add new shared "common" features that much quicker, because its just as easy to add it to the Commons project as it is to add it to one of the FoodBankTracker projects.

Sunday, August 19, 2007 4:34:01 PM (GMT Standard Time, UTC+00:00)  #    Disclaimer  |  Comments [0]  | 
# Sunday, July 29, 2007

By now most of us have figured out how to create sophisticated build scripts using NAnt, NAnt is the de-facto standard among .NET OSS projects and for good reason.  NAnt is the best build system on the .NET platform.  So with that out of the way, why would I be writing a series on MSBuild? 

Here's the biggest problem with NAnt as I see it, NAnt doesn't work in the de-facto .NET IDE, Visual Studio.  In Java, Ant build scripts work right there in your IDE.  As good as NAnt is, there's still the context switch cost of switching between Visual Studio and the command prompt.  Aw but I have hot keys you say.  OK, but some newbie dev on your team certainly doesn't, and in general most .NET developers are most comfortable within the realms of Visual Studio rather than the command line.  Like it or not, most .NET developers don't want to build through a command line before they can build through Visual Studio.

The other advantage MSBuild has is that csproj files work natively in the build engine.  If the C# project works in Visual Studio, then you know it will work on the command line with MSBuild.  There's no need to maintain two separate build scripts or any other silliness.  I have also found MSBuild to be really good at resolving dependant nested builds.  So without further ado, here's the rundown of posts I hope to hit in this series:

  1. The basics, project structure.
  2. Configuration file management.
  3. Building the database.
  4. Unit testing.
  5. Code documentation with Doxygen.
  6. Preparing your build artifacts for deployment.
  7. Integrating with CruiseControl.NET.
Sunday, July 29, 2007 5:56:21 AM (GMT Standard Time, UTC+00:00)  #    Disclaimer  |  Comments [0]  |