Tuesday, May 8, 2012

Uh Oh - don't stop the Application Information service!

The alternative title for this post is How to successfully move the SoftwareDistribution folder.

Today I had an issue... I could no longer elevate applications to Administrator level. Let me explain how I got into this mess.

I run Windows 7 and my boot drive is only 40GB. I needed to install yet another SDK, and I only had ½ GB or so left on that drive, so the installer immediately barfed and reported a drive space issue. So I go in and delete stuff from the various temp folders, then I run WinDirStat to see what else is taking space. After it has done its thing, a likely space-hogging candidate shows up - the SoftwareDistribution folder under C:\Windows.

Now an easy way to gain space is to move space hogging directories onto another drive, delete the original, and create a symlink in its place (as detailed in this and this blog post by Scott Hanselmann). Of course you need to move files/folders that Windows does not have open or is not constantly using. SoftwareDistribution fits that category - almost. There was one file I couldn't delete because it was held open by the Application Information service. The Application Information service looks innocuous, but you need to be very careful how you deal with it. It cannot be shut down cleanly. You can try to shut it down, but it errors every time you try, until it appears to end up in some kind of twilight state. So because I couldn't shut it down (in order to release its lock on the ReportingEvents.log file), I thought I would do another clever thing: set it's startup mode to disabled, and then reboot.

Of course this works, not a problem. But then I quickly discovered the flaw in my plan. In order to delete the remains of the SoftwareDistribution folder, I need to provide administrative permission, i.e. I have to agree to the UAC prompt. Therein lies the problem - the UAC uses the Application Information service to perform the elevation, but I've shut the service down and prevented it from being started. In fact I have a Catch-22 because I cannot do anything as Administrator, which means I also cannot restart the service or change its startup mode back to what it should be.

Big mistake. Here is where I am going to save you some time if you have the same problem - don't bother Googling the answer, because 100% of the answers I looked at were wrong. They either require you to run an elevated command prompt, or they require you to roll back to the last System Restore point. Remember that we can't elevate, and because I rebooted the last System Restore point is useless to me (I know because I tried it).

So how did I fix it? Quite simply I took advantage of a idiosyncracy in Windows that I didn't know about until now. I rebooted into safe mode, and then changed the Application Information service details from there. This works because UAC is not invoked in safe mode, if your user account is in the local Administrators group then anything you run is running as admin, unlike regular useage where the apps need to be individually elevated. While I was in safe mode I finished deleting the SoftwareDistribution folder and created the symlink, then I rebooted back into normal mode.

So the two critical things to remember if you are going to mess with important services or try and move the SoftwareDistribution folder:

  • make sure your user account is in the local Administrators group 
  • do the work in safe mode, or reboot into safe mode to fix issues 

Of course I could just buy a shiny new drive and reinstall Windows, but do you know how many hours is involved in repaving a development machine? Not to mention that you have way less fun if you do things the boring way!



keywords: application information service, safe mode, softwaredistribution, uac, appinfo

Saturday, July 9, 2011

Taking data binding, validation and MVVM to the next level - part 2

In part 1 of this series, you saw how to:
  • create a Validation rule
  • add that rule to the databinding of your TextBox
  • show a negative validation result in the tooltip of the TextBox

In this session, we are going to extend the validation rule to more completely check the file path entered by the user. If converters are the most useful and awesome additions to databinding, then validation rules have to be the second most useful and awesome. A couple of reasons why they are so awesome are:
  • you can use them declaratively in XAML
  • you can pass extra parameters to the validation rule
  • validation rules encapsulate logic and separate that logic out from your model or view model
  • validation rules are highly testable, unlike validation done via the IDataErrorInfo interface
  • you can specify multiple different validation rules on a binding
  • you have some control over when they are invoked

Okay, on to business. We are going to use two handy static methods on the Path class, GetInvalidPathChars and GetInvalidFileNameChars. We have to use these in the correct order - there are some characters that are legal in a path, but not in a file name, so there is no point testing the file name before the path. Here is some code:

using System;
using System.IO;
using System.Linq;
using System.Windows.Controls;

namespace FilePathValidation1
{
public class FilePathValidationRule : ValidationRule
{
public override ValidationResult Validate(object value, System.Globalization.CultureInfo cultureInfo)
{
if (value != null && value.GetType() != typeof(string))
return new ValidationResult(false, "Input value was of the wrong type, expected a string");

var filePath = value as string;

if (string.IsNullOrWhiteSpace(filePath))
return new ValidationResult(false, "The file path cannot be empty or whitespace.");

//check the path:
if (Path.GetInvalidPathChars().Any(x => filePath.Contains(x)))
return new ValidationResult(false, string.Format("The characters {0} are not permitted in a file path.", GetPrinatbleInvalidChars(Path.GetInvalidPathChars())));


return new ValidationResult(true, null);
}

/// <summary>
/// Gets the printable characters from the passed char array.
/// </summary>
/// <param name="chars">The array of characters to check.</param>
/// <returns>Returns a string containing the printable characters.</returns>
private string GetPrinatbleInvalidChars(char[] chars)
{
string invalidChars = string.Join("", chars.Where(x => !Char.IsWhiteSpace(x)));
return invalidChars;
}

}
}


So it is quite simple, we grab the list of invalid characters, then using LINQ check each one until we find the first failure, at which point we return a negative validation result, and include the printable invalid characters in the error message.

Checking the file name itself is quite similar:

      public override ValidationResult Validate(object value, System.Globalization.CultureInfo cultureInfo)
{
if (value != null && value.GetType() != typeof(string))
return new ValidationResult(false, "Input value was of the wrong type, expected a string");

var filePath = value as string;

if (string.IsNullOrWhiteSpace(filePath))
return new ValidationResult(false, "The file path cannot be empty or whitespace.");

//check the path:
if (Path.GetInvalidPathChars().Any(x => filePath.Contains(x)))
return new ValidationResult(false, string.Format("The characters {0} are not permitted in a file path.", GetPrinatbleInvalidChars(Path.GetInvalidPathChars())));

//check the filename (if one can be isolated out):
string fileName = Path.GetFileName(filePath);
if (Path.GetInvalidFileNameChars().Any(x => fileName.Contains(x)))
return new ValidationResult(false, string.Format("The characters {0} are not permitted in a file name.", GetPrinatbleInvalidChars(Path.GetInvalidFileNameChars())));


return new ValidationResult(true, null);
}


Because we have already dealt with a possibly null filePath value earlier on in the function, we don't need to worry about the GetFileName() function returning a null, it will return either the file name, or string.Empty.
However.... we have a catch-22 situation here - we need to get the file name so we can check it for invalid characters, but GetFileName() will itself throw an ArgumentException if it encounters an invalid character. So the answer is to wrap that statement in a try...catch:

          //check the filename (if one can be isolated out):
try
{
string fileName = Path.GetFileName(filePath);
if (Path.GetInvalidFileNameChars().Any(x => fileName.Contains(x)))
throw new ArgumentException(string.Format("The characters {0} are not permitted in a file name.", GetPrinatbleInvalidChars(Path.GetInvalidFileNameChars())));
}
catch (ArgumentException e) { return new ValidationResult(false, e.Message); }


Rather than code up two different lines returning a ValidationResult, I have employed the cheap'n'nasty hack of returning it from the catch clause, and throwing my own ArgumentException if necessary to get to it. I wouldn't do this in real code, I'm only doing it here to keep things shorter, and I warned you in the last post that this is example code not coded for prettiness. By doing this I can piggyback upon the exception message returned by the call to GetFileName().

Now one final thing - let's tidy up that empty/null entry condition checking. We are going to add a boolean property to the FilePathValidationRule to indicate whether it is allowable to have a null or empty path, we will add a new check into the rule that uses the new property, and we will set that new property from XAML.

    public class FilePathValidationRule : ValidationRule
{

public override ValidationResult Validate(object value, System.Globalization.CultureInfo cultureInfo)
{
if (value != null && value.GetType() != typeof(string))
return new ValidationResult(false, "Input value was of the wrong type, expected a string");

var filePath = value as string;

//check for empty/null file path:
if (string.IsNullOrEmpty(filePath))
{
if (!AllowEmptyPath)
return new ValidationResult(false, "The file path may not be empty.");
else
return new ValidationResult(true, null);
}

//null & empty has been handled above, now check for pure whitespace:
if (string.IsNullOrWhiteSpace(filePath))
return new ValidationResult(false, "The file path cannot consist only of whitespace.");

//check the path:
if (Path.GetInvalidPathChars().Any(x => filePath.Contains(x)))
return new ValidationResult(false, string.Format("The characters {0} are not permitted in a file path.", GetPrinatbleInvalidChars(Path.GetInvalidPathChars())));

//check the filename (if one can be isolated out):
try
{
string fileName = Path.GetFileName(filePath);
if (Path.GetInvalidFileNameChars().Any(x => fileName.Contains(x)))
throw new ArgumentException(string.Format("The characters {0} are not permitted in a file name.", GetPrinatbleInvalidChars(Path.GetInvalidFileNameChars())));
}
catch (ArgumentException e) { return new ValidationResult(false, e.Message); }

return new ValidationResult(true, null);
}

/// <summary>
/// Gets and sets a flag indicating whether an empty path forms an error condition or not.
/// </summary>
public bool AllowEmptyPath { get; set; }


private string GetPrinatbleInvalidChars(char[] chars)
{
string invalidChars = string.Join("", chars.Where(x => !Char.IsWhiteSpace(x)));
return invalidChars;
}

}


<Window x:Class="FilePathValidation1.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow"
SizeToContent="WidthAndHeight"

xmlns:this="clr-namespace:FilePathValidation1"
>

<Window.Resources>
<Style TargetType="TextBox">
<Style.Triggers>
<Trigger Property="Validation.HasError" Value="true">
<Setter Property="ToolTip" Value="{Binding RelativeSource={RelativeSource Self}, Path=(Validation.Errors)[0].ErrorContent}"/>
</Trigger>
</Style.Triggers>
</Style>
</Window.Resources>

<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<StackPanel Orientation="Horizontal" Margin="20" >
<TextBlock Text="Enter the path to your file" VerticalAlignment="Bottom" />
<TextBox x:Name="FilePathTextBox" Width="350" Margin="5,0,0,0">
<TextBox.Text>
<Binding Path="FilePath" UpdateSourceTrigger="PropertyChanged" >
<Binding.ValidationRules>
<this:FilePathValidationRule AllowEmptyPath="True" />
</Binding.ValidationRules>
</Binding>
</TextBox.Text>
</TextBox>
<Button x:Name="FileBrowseButton"
Content="..."
Command="{Binding FileBrowseCommand}"
Width="20" Margin="5,0,0,0"
/>
</StackPanel>
</Grid>
</Window>


As you can see, the only change to the XAML was the use of the new AllowEmptyPath property (which you only have to set if you need a value different from its default of false). From the next three images, you'll see that our new rule conditions are working quite nicely:






But.... remember before when I said that there were a lot of edge cases, and the functions built into the .Net framework were not going to be able to do all the work for you? Check this nasty path, which according to our rules is valid:



Tune in for the next post, where I show you how to catch this, and also illustrate a nice edge case regarding path length (which is not always limited to 260 characters! A-ha!).