Saturday, 19 July 2008

Cannot uninstall a .NET Windows Service with installutil /u when the service executable has been moved or deleted - Fix

There are a few perils unique to developing Windows Services in .NET. This is one of them.

The other day, I renamed some of my subversion working folders. Unfortunately, one of the folders that I renamed actually included a service that I had registered via installutil.exe on my local machine.

There is a problem with installutil.exe which means that this could be an unrecoverable Catch-22 situation. Here's why:


  1. You cannot uninstall it. If you try to uninstall it with installutil /u and point to your service (e.g. "uninstall /u DDK.ProjectName.MyNotificationServiceName", it cannot find the file will and give a "Exception occurred while initializing the installation: System.IO.FileNotFoundException: Could not load file or assembly '[Full Path To My File]' or one of its dependencies. The system cannot find the file specified.."

  2. You cannot install the exe to a different location with installutil because the service is already installed. If you do try to install it with a new path, you will just get the error "An exception occurred during the Install phase.System.ComponentModel.Win32Exception: The specified service already exists".

So to install a service with the same at the new location, I would have to:



  1. Copy the old file back to the original Windows Service Location location (or restore it from a backup) and run installutil /u on it. If I don't have the file anymore, I would not be able to do this.

  2. OR Remove the registry entry for the service.

This would not be as much of an issue if installutil /u recognized the missing service and prompted me if I wanted to remove the registry entry - but it didn't. I understand you want to do cleanup of a service when an uninstall is called - but you shouldn't be left in an unrecoverable state because of a lack of functionality in the core installer utility.

So when you don't have access to the file/drive you originally installed a service to, you can fix this unrecoverable (from the perspective of installutil) situation by either:


  1. Opening regedit

  2. Going to HKEY_LOCAL_MACHINE/SYSTEM/CurrentControlSet/Services

  3. Removing the old registry entry for your service.

OR


Running "sc" from a command prompt - see screenshot below for parameters:



Adding simple Gzip compression for a 40-60% reduction in page size on your ASP.NET 2.0 Site

UPDATE (29 October 2008): If you can, try to use the following:
http://www.codeproject.com/KB/aspnet/httpcompression.aspx
This has a some major benefits such as compressing your axd files, combining your css and javascript and minifying the output.

There are a few different ways to get Gzip compression happening on your site. These include:

  1. Custom Http modules that implement IHttpModule such as http://blowery.org/httpcompress/

  2. 3rd party handlers such as http://www.port80software.com/products/httpzip/

  3. If you have full access to the IIS Box and metabase, use the built-in Gzip compression available in IIS 6.0 and above (See http://weblogs.asp.net/owscott/archive/2004/01/12/57916.aspx for more information)

  4. Modifying the global.asax to implement compression.
I briefly outline option 4 below. With ASP.NET, it is incredibly easy to get it up and running without any additonal server set up. Note that it is important that you don't gzip your axd files through this code. Some UI components such as Telerik RadControls will generate several javascript errors if you try to gzip its axd resource files. I also found that a page I created for dynamically rendering images started chop images off at the bottom. So I excluded them from any attempts at compression.

If you look at your page size in YSLow, it will typically be reduced by 40-60%. e.g. from 200K to 100K.

Here's some sample code that you can put into your global.asax with minor modifications appropriate to your project:




  //Gzip support

    void Application_BeginRequest(object sender, EventArgs e)

    {

        HttpApplication app = (HttpApplication)sender;

        if (app.Request.Url.ToString().Contains("ImageGenerator.aspx")  app.Request.Url.ToString().Contains("WebResource.axd")  app.Request.Url.ToString().Contains("ScriptResource.axd")) //Dont process this as it corrupts images/scripts

            return;

        string acceptEncoding = app.Request.Headers["Accept-Encoding"];

        Stream prevUncompressedStream = app.Response.Filter;

 

        if (acceptEncoding == null  acceptEncoding.Length == 0)

            return;

 

        acceptEncoding = acceptEncoding.ToLower();

 

        if (acceptEncoding.Contains("gzip"))

        {

            // gzip

            app.Response.Filter = new GZipStream(prevUncompressedStream,

                CompressionMode.Compress);

            app.Response.AppendHeader("Content-Encoding",

                "gzip");

        }

        else if (acceptEncoding.Contains("deflate"))

        {

            // deflate

            app.Response.Filter = new DeflateStream(prevUncompressedStream,

                CompressionMode.Compress);

            app.Response.AppendHeader("Content-Encoding",

                "deflate");

        }

    }
 
 
 


For more information on IIS and the built-in settings when you have full access to IIS, see the following articles for reference:

http://www.microsoft.com/technet/prodtechnol/WindowsServer2003/Library/IIS/d52ff289-94d3-4085-bc4e-24eb4f312e0e.mspx?mfr=true
http://www.microsoft.com/technet/prodtechnol/WindowsServer2003/Library/IIS/502ef631-3695-4616-b268-cbe7cf1351ce.mspx?mfr=true


The IIS compression dialog:





Removing all blank lines from a file with regular expressions in Visual Studio or UltraEdit

I often deal with files which have redundant empty lines in them. These are easily removed by either Visual Studio or one of the best text editors around, UltraEdit by IDM Solutions. The regular expression criteria matching blank lines in these 2 applications are slightly different (the end of line escape character "$" appears in a different order in each):

Visual Studio:

Press Ctrl+G
Select "Use Regular Expressions"
In Find specify "^$\n" (without the quotes).
Set the replace value to blank.
Click on "Replace All"

UltraEdit:

[from the UltraEdit FAQ at at http://www.ultraedit.com/support/faq.html]

To delete/strip blank lines from DOS/Unix/Mac formatted-files, use the following Perl-compatible regular expression. You can enable Perl-compatible regular expressions under Advanced -> Configuration -> Search -> Regular Expression Engine.

Replace: "^\r?\n?$" (without the quotes)
With "" (without the quotes - i.e. nothing).

Earlier versions of UltraEdit:

To delete blank lines with DOS line terminators you can use an UltraEdit-style regular expression replace as follows:

Replace: "^p$" (without the quotes)
With "" (without the quotes - i.e. nothing).

Run this replace until every blank line is deleted.

Amusing Image of the day from FailBlog.org...Why you should think before you post a comment on a blog :o)


You get the error "[Script Name].ps1 cannot be loaded because the execution of scripts is disabled on this system" when running Powershell scripts

If you get the error:
File D:\Sc\Global\DDK.Solution\dev\DDK.ProjectName\deploy.ps1 cannot be loaded because the execution of scripts is disabled on this system. Please see "get-help about_signing" for more details.

You get this error because the default setting for Powershell is "Restricted" (aka locked down Alcatraz mode). In this mode, it does not load configuration files or run scripts.

To resolve this issue, you can run Powershell (powershell is typically in C:\WINNT\system32\WindowsPowerShell\v1.0\powershell.exe if it is not already in your path) and change the execution policy. For example, you could run the command "Set-ExecutionPolicy Unrestricted" if you want to allow unsigned scripts to run.

Once you have set your security policy appropriately, you can execute your powershell scripts without this error. See http://technet.microsoft.com/en-us/library/bb978644(TechNet.10).aspx for more information.




Deleting Folders in MOSS via Web Services and CAML

Unfortunately, the lists.asmx web service that you use to manipulate MOSS lists doesn't have a "Delete()" method for folders. However, there is an "UpdateListItems()" method that allows you to pass in an Xml element parameter called "batchElement" to provide this functionality. You then can manipulate folders in Sharepoint to your hearts content through this parameter.

The typical format for the batch element Xml fragment is:

<Batch OnError='Return'>
<Method ID='1' Cmd='Delete'>
<Field Name='ID'>81</Field>
<Field Name='FileRef'>http://dev-moss/sites/home/PropertySharePoint/DocumentLibrary/300</Field>
</Method>
</Batch>


Your delete is successful if the return value is zero. You can test this out in the U2U CAML query builder from http://www.u2u.info/Blogs/Patrick/Lists/Posts/Post.aspx?ID=1315



This batchElement information can be passed into the sharepoint list web service as demonstrated in the method snippet below:



  /// <summary>

        /// Delete folders as per http://msdn2.microsoft.com/en-us/library/ms429658.aspx for LPP-205

        /// </summary>

        /// <param name="listName"></param>

        /// <param name="folderName"></param>

        /// <returns></returns>

        public XmlNode DeleteFolder(string listName, string folderName)

        {

            /*Use the CreateElement method of the document object to create elements for the parameters that use XML.*/

            System.Xml.XmlDocument xmlDoc = new System.Xml.XmlDocument();

            XmlElement query = xmlDoc.CreateElement("Query");

            XmlElement viewFields = xmlDoc.CreateElement("ViewFields");

            XmlElement queryOptions = xmlDoc.CreateElement("QueryOptions");

            string rowLimit = int.MaxValue.ToString();

            /*To specify values for the parameter elements (optional), assign CAML fragments to the InnerXml property of each element.*/

            System.Text.StringBuilder sb= new System.Text.StringBuilder();

            sb.Append("<Where><Eq><FieldRef Name=\"Title\" />");

            sb.Append(string.Format("<Value Type=\"Text\">{0}</Value></Eq></Where>", folderName));

            viewFields.InnerXml = "<FieldRef Name=\"ID\" /><FieldRef Name=\"Title\" />";

            query.InnerXml =  sb.ToString();

            queryOptions.InnerXml = "";

            System.Xml.XmlNode nodeListItems = _listWebService.GetListItems(listName, string.Empty, query, viewFields, rowLimit, queryOptions, null);

            string folderId = string.Empty;

            string fileRef = string.Empty; 

            XmlDocument doc = new XmlDocument();

            doc.LoadXml(nodeListItems.InnerXml);

            XmlNamespaceManager nsmgr = new XmlNamespaceManager(doc.NameTable);

            nsmgr.AddNamespace("z", "#RowsetSchema");

            nsmgr.AddNamespace("rs", "urn:schemas-microsoft-com:rowset");

            XmlNodeList xmlNodeList = doc.SelectNodes("/rs:data/z:row", nsmgr);

            foreach (XmlNode node in xmlNodeList)

            {

                folderId = node.Attributes["ows_ID"].Value;

                //fileRef = node.Attributes["ows_EncodedAbsUrl"].Value;

                fileRef = node.Attributes["ows_FileRef"].Value.Substring(node.Attributes["ows_FileRef"].Value.IndexOf("#") + 1);

                break;

            }

            System.Xml.XmlNode result = null; //Will be populated response from update batch.

            if (folderId != string.Empty)

            {

                System.IO.StringWriter sw = new System.IO.StringWriter();

                System.Xml.XmlTextWriter xw = new System.Xml.XmlTextWriter(sw);

                xw.WriteStartDocument();

                // build batch node

                xw.WriteStartElement("Batch");

                xw.WriteAttributeString("OnError", "Return");

                // Build method node

                xw.WriteStartElement("Method");

                // Set transaction ID - doesn't really matter what the number is

                xw.WriteAttributeString("ID", System.Guid.NewGuid().ToString("n"));

                xw.WriteAttributeString("Cmd", "Delete");

                // Build field ID

                xw.WriteStartElement("Field");

                xw.WriteAttributeString("Name", "ID");

                xw.WriteString(folderId);

                xw.WriteEndElement(); // Field end

                // Build FileRef

                xw.WriteStartElement("Field");

                xw.WriteAttributeString("Name", "FileRef");

                xw.WriteString(fileRef);

                xw.WriteEndElement(); // Field end

                xw.WriteEndElement(); // Method end

                xw.WriteEndElement(); // Batch end

                xw.WriteEndDocument();

                System.Xml.XmlDocument batchElement = new System.Xml.XmlDocument();

                batchElement.LoadXml(sw.GetStringBuilder().ToString());

                //Setup web service

                // send update request to sharepoint to create the document folder

                result = _listWebService.UpdateListItems(listName, batchElement);

            }

            return result;

        }

Tuesday, 8 July 2008

TortoiseSVN 1.5 and svnmerge Issue - "svn: This client is too old to work with working copy '.'; please get a newer Subversion client"


WARNING: TortoiseSVN 1.5 does silent upgrades (aka the touch of death) on your SVN working copies that renders it unusable with older SVN clients. This affects clients such as the 1.4 based version of svnmerge (the current version as I write this).

Before the most recent 1.5 upgrade for TortoiseSVN, the last major version was made over 2 years ago. So you could imagine that I was quite keen to upgrade to latest version when my TortoiseSVN notified me that I should upgrade to version 1.5.

I've generally had a good experience with TortoiseSVN thus far so I bit the bullet and downloaded the latest and greatest version of this handy tool. I only uncovered the implcations of this upgrade when all my svnmerge scripts started to fail (we use the svnmerge utility via a batch file to deploy our changes to trunk and onto our build server). Then things hit the proverbial fan. I started to get the following errors for all svnmerge operations (including simple status calls) :

"svn: This client is too old to work with working copy '.'; please get a newer Subversion client"

I thought it was unusual that:

  1. TortoiseSVN installer would affect svnmerge at all (I assumed some shared DLLs had been updated).
  2. (on a more minor note) That it would start telling me that I had an old version of the client tool when I had the very latest versions of TortoiseSVN and svnmerge (http://www.subversionary.org/binaries/installer-for-svnmerge). I would hope that a tool as popular as svnmerge would be updated within days of a new client version being released.

I thought the rollback process would be as simple as uninstalling 1.5 of TortoiseSVN - but I uninstalled and changed back to 1.4.8 - and got the same error! Even the 1.4 versions of svn started to get the same error.

It turns out that once you start working with the new version of the tool, the SVN metadata is silently upgraded to the latest version. Unfortunately, if you want to keep using svnmerge, the fix for this issue (until a new version of svnmerge comes out) is to:

  1. Roll back to a 1.4 version
  2. Essentially throw away your working copy
  3. Do a fresh checkout.

This will get you back to the place you started. This could take a while - especially if you're in Australia and the subversion server is in Atlanta! Phew!

The TortoiseSVN 1.5 touch of death at work (it is not backwards compatible with 1.4 clients):