Archive

Archive for October, 2013

Always Explicitly Set Your Parameter Set Variables For PowerShell v2.0 Compatibility

October 28th, 2013 2 comments

What are parameter sets anyways?

Parameter sets were introduced in PowerShell v2.0 and are useful for enforcing mutually exclusive parameters on a cmdlet.  Ed Wilson has a good little article explaining what parameter sets are and how to use them.  Essentially they allow us to write a single cmdlet that might otherwise have to be written as 2 or more cmdlets that took different parameters.  For example, instead of having to create Process-InfoFromUser, Process-InfoFromFile, and Process-InfoFromUrl cmdlets, we could create a single Process-Info cmdlet that has 3 mutually exclusive parameters, [switch]$PromptUser, [string]$FilePath, and [string]$Url.  If the cmdlet is called with more than one of these parameters, it throws an error.

You could just be lazy and not use parameter sets and allow all 3 parameters to be specified and then just use the first one, but the user won’t know which one of the 3 they provided will be used; they might assume that all 3 will be used.  This would also force the user to have to read the documentation (assuming you have provided it).  Using parameter sets enforces makes it clear to the user which parameters are able to be used with other parameters.  Also, most PowerShell editors process parameter sets to have the intellisense properly show the parameters that can be used with each other.

 

Ok, parameter sets sound awesome, I want to use them! What’s the problem?

The problem I ran into was in my Invoke-MsBuild module that I put on CodePlex, I had a [switch]$PassThru parameter that was part of a parameter set.  Within the module I had:

if ($PassThru) { do something... }
else { do something else... }

This worked great for me during my testing since I was using PowerShell v3.0.  The problem arose once I released my code to the public; I received an issue from a user who was getting the following error message:

Invoke-MsBuild : Unexpect error occured while building "<path>\my.csproj": The variable ‘$PassThru’ cannot be retrieved because it has not been set.

At build.ps1:84 char:25

  • $result = Invoke-MsBuild <<<< -Path "<path>\my.csproj" -BuildLogDirectoryPath "$scriptPath" -Pa

    rams "/property:Configuration=Release"

After some investigation I determined the problem was that they were using PowerShell v2.0, and that my script uses Strict Mode.  I use Set-StrictMode -Version Latest in all of my scripts to help me catch any syntax related errors and to make sure my scripts will in fact do what I intend them to do.  While you could simply not use strict mode and you wouldn’t have a problem, I don’t recommend that; if others are going to call your cmdlet (or you call it from a different script), there’s a good chance they may have Strict Mode turned on and your cmdlet may break for them.

 

So should I not use parameter sets with PowerShell v2.0? Is there a fix?

You absolutely SHOULD use parameter sets whenever you can and it makes sense, and yes there is a fix.  If you require your script to run on PowerShell v2.0, there is just one extra step you need to take, which is to explicitly set the values for any parameters that use a parameter set and don’t exist.  Luckily we can use the Test-Path cmdlet to test if a variable has been defined in a specific scope or not.

Here is an example of how to detect if a variable is not defined in the Private scope and set its default value.  We specify the scope in case a variable with the same name exists outside of the cmdlet in the global scope or an inherited scope.

# Default the ParameterSet variables that may not have been set depending on which parameter set is being used. This is required for PowerShell v2.0 compatibility.
if (!(Test-Path Variable:Private:SomeStringParameter)) { $SomeStringParameter = $null }
if (!(Test-Path Variable:Private:SomeIntegerParameter)) { $SomeIntegerParameter = 0 }
if (!(Test-Path Variable:Private:SomeSwitchParameter)) { $SomeSwitchParameter = $false }

If you prefer, instead of setting a default value for the parameter you could just check if it is defined first when using it in your script.  I like this approach however, because I can put this code right after my cmdlet parameters so I’m modifying all of my parameter set properties in one place, and I don’t have to remember to check if the variable is defined later when writing the body of my cmdlet; otherwise I’m likely to forget to do the “is defined” check, and will likely miss the problem since I do most of my testing in PowerShell v3.0.

Another approach rather than checking if a parameter is defined or not, is to check which Parameter Set Name is being used; this will implicitly let you know which parameters are defined.

switch ($PsCmdlet.ParameterSetName)
{
	"SomeParameterSetName"  { Write-Host "You supplied the Some variable."; break}
	"OtherParameterSetName"  { Write-Host "You supplied the Other variable."; break}
} 

I still prefer to default all of my parameters, but you may prefer this method.

I hope you find this useful.  Check out my other article for more PowerShell v2.0 vs. v3.0 differences.

Happy coding!

PowerShell Code To Ensure Client Is Using At Least The Minimum Required PowerShell Version

October 25th, 2013 3 comments

Here’s some simple code that will throw an exception if the client running your script is not using the version of PowerShell (or greater) that is required; just change the $REQUIRED_POWERSHELL_VERSION variable value to the minimum version that the script requires.

# Throw an exception if client is not using the minimum required PowerShell version.
$REQUIRED_POWERSHELL_VERSION = 3.0	# The minimum Major.Minor PowerShell version that is required for the script to run.
$POWERSHELL_VERSION = $PSVersionTable.PSVersion.Major + ($PSVersionTable.PSVersion.Minor / 10)
if ($REQUIRED_POWERSHELL_VERSION -gt $POWERSHELL_VERSION)
{ throw "PowerShell version $REQUIRED_POWERSHELL_VERSION is required for this script; You are only running version $POWERSHELL_VERSION. Please update PowerShell to at least version $REQUIRED_POWERSHELL_VERSION." }

— UPDATE {

Thanks to Robin M for pointing out that PowerShell has the built-in #Requires statement for this purpose, so you do not need to use the code above. Instead, simply place the following code anywhere in your script to enforce the desired PowerShell version required to run the script:

#Requires -Version 3.0

If the user does not have the minimum required version of PowerShell installed, they will see an error message like this:

The script ‘foo.ps1’ cannot be run because it contained a "#requires" statement at line 1 for Windows PowerShell version 3.0 which is incompatible with the installed Windows PowerShell version of 2.0.

} UPDATE —

So if your script requires, for example, PowerShell v3.0, just put this at the start of your script to have it error out right away with a meaningful error message; otherwise your script may throw other errors that mask the real issue, potentially leading the user to spend many hours troubleshooting your script, or to give up on it all together.

I’ve been bitten by this in the past a few times now, where people report issues on my Codeplex scripts where the error message seems ambiguous.  So now any scripts that I release to the general public will have this check in it to give them a proper error message.  I have also created a page on PowerShell v2 vs. v3 differences that I’m going to use to keep track of the differences that I encounter, so that I can have confidence in the minimum powershell version that I set on my scripts.  I also plan on creating a v3 vs. v4 page once I start using PS v4 features more.  Of course, the best test is to actually run your script in the minimum powershell version that you set, which I mention how to do on my PS v2 vs. v3 page.

Happy coding!

PowerShell Script To Get Path Lengths

October 24th, 2013 6 comments

A while ago I created a Path Length Checker tool in C# that has a “nice” GUI, and put it up on CodePlex.  One of the users reported that he was trying to use it to scan his entire C: drive, but that it was crashing.  Turns out that the System.IO.Directory.GetFileSystemEntries() call was throwing a permissions exception when trying to access the “C:\Documents and Settings” directory.  Even when running the app as admin it throws this exception.  In the meantime while I am working on implementing a workaround for the app, I wrote up a quick PowerShell script that the user could use to get all of the path lengths.  That is what I present to you here.

$pathToScan = "C:\Some Folder"	# The path to scan and the the lengths for (sub-directories will be scanned as well).
$outputFilePath = "C:\temp\PathLengths.txt"	# This must be a file in a directory that exists and does not require admin rights to write to.
$writeToConsoleAsWell = $true	# Writing to the console will be much slower.

# Open a new file stream (nice and fast) and write all the paths and their lengths to it.
$outputFileDirectory = Split-Path $outputFilePath -Parent
if (!(Test-Path $outputFileDirectory)) { New-Item $outputFileDirectory -ItemType Directory }
$stream = New-Object System.IO.StreamWriter($outputFilePath, $false)
Get-ChildItem -Path $pathToScan -Recurse -Force | Select-Object -Property FullName, @{Name="FullNameLength";Expression={($_.FullName.Length)}} | Sort-Object -Property FullNameLength -Descending | ForEach-Object {
    $filePath = $_.FullName
    $length = $_.FullNameLength
    $string = "$length : $filePath"
    
    # Write to the Console.
    if ($writeToConsoleAsWell) { Write-Host $string }
 
    #Write to the file.
    $stream.WriteLine($string)
}
$stream.Close()

Happy coding!

PowerShell Functions To Convert, Remove, and Delete IIS Web Applications

October 23rd, 2013 No comments

I recently refactored some of our PowerShell scripts that we use to publish and remove IIS 7 web applications, creating some general functions that can be used anywhere.  In this post I show these functions along with how I structure our scripts to make creating, removing, and deleting web applications for our various products fully automated and tidy.  Note that these scripts require at least PowerShell v3.0 and use the IIS Admin Cmdlets that I believe require IIS v7.0; the IIS Admin Cmdlet calls can easily be replaced though by calls to appcmd.exe, msdeploy, or any other tool for working with IIS that you want.

I’ll blast you with the first file’s code and explain it below (ApplicationServiceUtilities.ps1).

# Turn on Strict Mode to help catch syntax-related errors.
# 	This must come after a script's/function's param section.
# 	Forces a function to be the first non-comment code to appear in a PowerShell Module.
Set-StrictMode -Version Latest

# Define the code block that will add the ApplicationServiceInformation class to the PowerShell session.
# NOTE: If this class is modified you will need to restart your PowerShell session to see the changes.
$AddApplicationServiceInformationTypeScriptBlock = {
    # Wrap in a try-catch in case we try to add this type twice.
    try {
    # Create a class to hold an IIS Application Service's Information.
    Add-Type -TypeDefinition "
        using System;
    
        public class ApplicationServiceInformation
        {
            // The name of the Website in IIS.
            public string Website { get; set;}
        
            // The path to the Application, relative to the Website root.
            public string ApplicationPath { get; set; }

            // The Application Pool that the application is running in.
            public string ApplicationPool { get; set; }

            // Whether this application should be published or not.
            public bool ConvertToApplication { get; set; }

            // Implicit Constructor.
            public ApplicationServiceInformation() { this.ConvertToApplication = true; }

            // Explicit constructor.
            public ApplicationServiceInformation(string website, string applicationPath, string applicationPool, bool convertToApplication = true)
            {
                this.Website = website;
                this.ApplicationPath = applicationPath;
                this.ApplicationPool = applicationPool;
                this.ConvertToApplication = convertToApplication;
            }
        }
    "
    } catch {}
}
# Add the ApplicationServiceInformation class to this PowerShell session.
& $AddApplicationServiceInformationTypeScriptBlock

<#
    .SYNOPSIS
    Converts the given files to application services on the given Server.

    .PARAMETER Server
    The Server Host Name to connect to and convert the applications on.

    .PARAMETER ApplicationServicesInfo
    The [ApplicationServiceInformation[]] containing the files to convert to application services.
#>
function ConvertTo-ApplicationServices
{
    [CmdletBinding()]
    param
    (
        [string] $Server,
        [ApplicationServiceInformation[]] $ApplicationServicesInfo
    )

    $block = {
	    param([PSCustomObject[]] $ApplicationServicesInfo)
        $VerbosePreference = $Using:VerbosePreference
	    Write-Verbose "Converting To Application Services..."

        # Import the WebAdministration module to make sure we have access to the required cmdlets and the IIS: drive.
        Import-Module WebAdministration 4> $null	# Don't write the verbose output.
	
	    # Create all of the Web Applications, making sure to first try and remove them in case they already exist (in order to avoid a PS error).
	    foreach ($appInfo in [PSCustomObject[]]$ApplicationServicesInfo)
        {
            $website = $appInfo.Website
            $applicationPath = $appInfo.ApplicationPath
            $applicationPool = $appInfo.ApplicationPool
		    $fullPath = Join-Path $website $applicationPath

            # If this application should not be converted, continue onto the next one in the list.
            if (!$appInfo.ConvertToApplication) { Write-Verbose "Skipping publish of '$fullPath'"; continue }
		
		    Write-Verbose "Checking if we need to remove '$fullPath' before converting it..."
		    if (Get-WebApplication -Site "$website" -Name "$applicationPath")
		    {
			    Write-Verbose "Removing '$fullPath'..."
			    Remove-WebApplication -Site "$website" -Name "$applicationPath"
		    }

            Write-Verbose "Converting '$fullPath' to an application with Application Pool '$applicationPool'..."
            ConvertTo-WebApplication "IIS:\Sites\$fullPath" -ApplicationPool "$applicationPool"
        }
    }

    # Connect to the host Server and run the commands directly o that computer.
    # Before we run our script block we first have to add the ApplicationServiceInformation class type into the PowerShell session.
    $session = New-PSSession -ComputerName $Server
    Invoke-Command -Session $session -ScriptBlock $AddApplicationServiceInformationTypeScriptBlock
    Invoke-Command -Session $session -ScriptBlock $block -ArgumentList (,$ApplicationServicesInfo)
    Remove-PSSession -Session $session
}

<#
    .SYNOPSIS
    Removes the given application services from the given Server.

    .PARAMETER Server
    The Server Host Name to connect to and remove the applications from.

    .PARAMETER ApplicationServicesInfo
    The [ApplicationServiceInformation[]] containing the applications to remove.
#>
function Remove-ApplicationServices
{
    [CmdletBinding()]
    param
    (
        [string] $Server,
        [ApplicationServiceInformation[]] $ApplicationServicesInfo
    )

    $block = {
	    param([ApplicationServiceInformation[]] $ApplicationServicesInfo)
        $VerbosePreference = $Using:VerbosePreference
	    Write-Verbose "Removing Application Services..."

        # Import the WebAdministration module to make sure we have access to the required cmdlets and the IIS: drive.
        Import-Module WebAdministration 4> $null	# Don't write the verbose output.

	    # Remove all of the Web Applications, making sure they exist first (in order to avoid a PS error).
	    foreach ($appInfo in [ApplicationServiceInformation[]]$ApplicationServicesInfo)
        {
            $website = $appInfo.Website
            $applicationPath = $appInfo.ApplicationPath
		    $fullPath = Join-Path $website $applicationPath
		
		    Write-Verbose "Checking if we need to remove '$fullPath'..."
		    if (Get-WebApplication -Site "$website" -Name "$applicationPath")
		    {
			    Write-Verbose "Removing '$fullPath'..."
			    Remove-WebApplication -Site "$website" -Name "$applicationPath"
		    }
        }
    }

    # Connect to the host Server and run the commands directly on that computer.
    # Before we run our script block we first have to add the ApplicationServiceInformation class type into the PowerShell session.
    $session = New-PSSession -ComputerName $Server
    Invoke-Command -Session $session -ScriptBlock $AddApplicationServiceInformationTypeScriptBlock
    Invoke-Command -Session $session -ScriptBlock $block -ArgumentList (,$ApplicationServicesInfo)
    Remove-PSSession -Session $session
}

<#
    .SYNOPSIS
    Removes the given application services from the given Server and deletes all associated files.

    .PARAMETER Server
    The Server Host Name to connect to and delete the applications from.

    .PARAMETER ApplicationServicesInfo
    The [ApplicationServiceInformation[]] containing the applications to delete.

    .PARAMETER OnlyDeleteIfNotConvertedToApplication
    If this switch is supplied and the application services are still running (i.e. have not been removed yet), the services will not be removed and the files will not be deleted.

    .PARAMETER DeleteEmptyParentDirectories
    If this switch is supplied, after the application services folder has been removed, it will recursively check parent folders and remove them if they are empty, until the Website root is reached.
#>
function Delete-ApplicationServices
{
    [CmdletBinding()]
    param
    (
        [string] $Server,
        [ApplicationServiceInformation[]] $ApplicationServicesInfo,
        [switch] $OnlyDeleteIfNotConvertedToApplication,
        [switch] $DeleteEmptyParentDirectories
    )
    
    $block = {
	    param([ApplicationServiceInformation[]] $ApplicationServicesInfo)
        $VerbosePreference = $Using:VerbosePreference
	    Write-Verbose "Deleting Application Services..."

        # Import the WebAdministration module to make sure we have access to the required cmdlets and the IIS: drive.
        Import-Module WebAdministration 4> $null	# Don't write the verbose output.

	    # Remove all of the Web Applications and delete their files from disk.
	    foreach ($appInfo in [ApplicationServiceInformation[]]$ApplicationServicesInfo)
        {
            $website = $appInfo.Website
            $applicationPath = $appInfo.ApplicationPath
		    $fullPath = Join-Path $website $applicationPath
            $iisSitesDirectory = "IIS:\Sites\"
		
		    Write-Verbose "Checking if we need to remove '$fullPath'..."
		    if (Get-WebApplication -Site "$website" -Name "$applicationPath")
		    {
                # If we should only delete the files they're not currently running as a Web Application, continue on to the next one in the list.
                if ($Using:OnlyDeleteIfNotConvertedToApplication) { Write-Verbose "'$fullPath' is still running as a Web Application, so its files will not be deleted."; continue }

			    Write-Verbose "Removing '$fullPath'..."
			    Remove-WebApplication -Site "$website" -Name "$applicationPath"
		    }
            
            Write-Verbose "Deleting the directory '$fullPath'..."
            Remove-Item -Path "$iisSitesDirectory$fullPath" -Recurse -Force

            # If we should delete empty parent directories of this application.
            if ($Using:DeleteEmptyParentDirectories)
            {
                Write-Verbose "Deleting empty parent directories..."
                $parent = Split-Path -Path $fullPath -Parent

                # Only delete the parent directory if it is not the Website directory, and it is empty.
                while (($parent -ne $website) -and (Test-Path -Path "$iisSitesDirectory$parent") -and ((Get-ChildItem -Path "$iisSitesDirectory$parent") -eq $null))
                {
                    $path = $parent
                    Write-Verbose "Deleting empty parent directory '$path'..."
                    Remove-Item -Path "$iisSitesDirectory$path" -Force
                    $parent = Split-Path -Path $path -Parent
                }
            }
        }
    }

    # Connect to the host Server and run the commands directly on that computer.
    # Before we run our script block we first have to add the ApplicationServiceInformation class type into the PowerShell session.
    $session = New-PSSession -ComputerName $Server
    Invoke-Command -Session $session -ScriptBlock $AddApplicationServiceInformationTypeScriptBlock
    Invoke-Command -Session $session -ScriptBlock $block -ArgumentList (,$ApplicationServicesInfo)
    Remove-PSSession -Session $session
}

This first file contains all of the meat.  At the top it declares (in C#) the ApplicationServiceInformation class that is used to hold the information about a web application; mainly the Website that the application should go in, the ApplicationPath (where within the website the application should be created), and the Application Pool that the application should run under.  Notice that the $AddApplicationServiceInformationTypeScriptBlock script block is executed right below where it is declared, in order to actually import the ApplicationServiceInformation class type into the current PowerShell session.

There is one extra property on this class that I found I needed, but you may be able to ignore; that is the ConvertToApplication boolean.  This is inspected by our ConvertTo-ApplicationServices function to tell it whether the application should actually be published or not.  I required this field because we have some web services that should only be “converted to applications” in specific environments (or only on a developers local machine), but whose files we still want to delete when using the Delete-ApplicationServices function.  While I could just create 2 separate lists of ApplicationServiceInformation objects depending on which function I was calling (see below), I decided to instead just include this one extra property.

Below the class declaration are our functions to perform the actual work:

  • ConvertTo-ApplicationServices: Converts the files to an application using the ConvertTo-WebApplication cmdlet.
  • Remove-ApplicationServices: Converts the application back to regular files using the Remove-WebApplication cmdlet.
  • Delete-ApplicationServices: First removes any applications, and then deletes the files from disk.
    The Delete-ApplicationServices function includes an couple additional switches.  The $OnlyDeleteIfNotConvertedToApplication switch can be used as a bit of a safety net to ensure that you only delete files for application services that are not currently running as a web application (i.e. the web application has already been removed).  If this switch is omitted, the web application will be removed and the files deleted.  The $DeleteEmptyParentDirectories switch that may be used to remove parent directories once the application files have been deleted. This is useful for us because we version our services, so they are all placed in a directory corresponding to a version number. e.g. \Website\[VersionNumber]\App1 and \Website\[VersionNumber]\App2. This switch allows the [VersionNumber] directory to be deleted automatically once the App1 and App2 directories have been deleted.
    Note that I don’t have a function to copy files to the server (i.e. publish them); I assume that the files have already been copied to the server, as we currently have this as a separate step in our deployment process.

My 2nd file (ApplicationServiceLibrary.ps1) is optional and is really just a collection of functions used to return the ApplicationServiceInformation instances that I require as an array, depending on which projects I want to convert/remove/delete.

# Get the directory that this script is in.
$THIS_SCRIPTS_DIRECTORY = Split-Path $script:MyInvocation.MyCommand.Path

# Include the required ApplicationServiceInformation type.
. (Join-Path $THIS_SCRIPTS_DIRECTORY ApplicationServiceUtilities.ps1)

#=================================
# Replace all of the functions below with your own.
# These are provided as examples.
#=================================

function Get-AllApplicationServiceInformation([string] $Release)
{
    [ApplicationServiceInformation[]] $appServiceInfo = @()

    $appServiceInfo += Get-RqApplicationServiceInformation -Release $Release
    $appServiceInfo += Get-PublicApiApplicationServiceInformation -Release $Release
    $appServiceInfo += Get-IntraApplicationServiceInformation -Release $Release

    return $appServiceInfo    
}

function Get-RqApplicationServiceInformation([string] $Release)
{
    return [ApplicationServiceInformation[]] @(
	    (New-Object ApplicationServiceInformation -Property @{Website = "Application Services"; ApplicationPath = "$Release/Core.Reporting.Services"; ApplicationPool = "RQ Services .NET4"}),
	    (New-Object ApplicationServiceInformation -Property @{Website = "Application Services"; ApplicationPath = "$Release/Core.Services"; ApplicationPool = "RQ Core Services .NET4"}),
	    (New-Object ApplicationServiceInformation -Property @{Website = "Application Services"; ApplicationPath = "$Release/DeskIntegration.Services"; ApplicationPool = "RQ Services .NET4"}),
	    (New-Object ApplicationServiceInformation -Property @{Website = "Application Services"; ApplicationPath = "$Release/Retail.Integration.Services"; ApplicationPool = "RQ Services .NET4"}),

        # Simulator Services that are only for Dev; we don't want to convert them to an application, but do want to remove their files that got copied to the web server.
        (New-Object ApplicationServiceInformation -Property @{Website = "Application Services"; ApplicationPath = "$Release/Simulator.Services"; ApplicationPool = "Simulator Services .NET4"; ConvertToApplication = $false}))
}

function Get-PublicApiApplicationServiceInformation([string] $Release)
{
    return [ApplicationServiceInformation[]] @(
        (New-Object ApplicationServiceInformation -Property @{Website = "API Services"; ApplicationPath = "$Release/PublicAPI.Host"; ApplicationPool = "API Services .NET4"}),
	    (New-Object ApplicationServiceInformation -Property @{Website = "API Services"; ApplicationPath = "$Release/PublicAPI.Documentation"; ApplicationPool = "API Services .NET4"}))
}

function Get-IntraApplicationServiceInformation([string] $Release)
{
    return [ApplicationServiceInformation[]] @(
        (New-Object ApplicationServiceInformation -Property @{Website = "Intra Services"; ApplicationPath = "$Release"; ApplicationPool = "Intra Services .NET4"}))
}

You can see the first thing it does is dot source the ApplicationServiceUtilities.ps1 file (I assume all these scripts are in the same directory).  This is done in order to include the ApplicationServiceInformation type into the PowerShell session.  Next I just have functions that return the various application service information that our various projects specify.  I break them apart by project so that I’m able to easily publish one project separately from another, but also have a Get-All function that returns back all of the service information for when we deploy all services together.  We deploy many of our projects in lock-step, so having a Get-All function makes sense for us, but it may not for you.  We have many more projects and services than I show here; I just show these as an example of how you can set yours up if you choose.

One other thing you may notice is that my Get-*ApplicationServiceInformation functions take a $Release parameter that is used in the ApplicationPath; this is because our services are versioned.  Yours may not be though, in which case you can omit that parameter for your Get functions (or add any additional parameters that you do need).

Lastly, to make things nice and easy, I create ConvertTo, Remove, and Delete scripts for each of our projects, as well as a scripts to do all of the projects at once.  Here’s an example of what one of these scripts would look like:

param
(
	[parameter(Position=0,Mandatory=$true,HelpMessage="The 3 hex-value version number of the release (x.x.x).")]
	[ValidatePattern("^\d{1,5}\.\d{1,5}\.\d{1,5}$")]
	[string] $Release
)

# Get the directory that this script is in.
$THIS_SCRIPTS_DIRECTORY = Split-Path $script:MyInvocation.MyCommand.Path

# Include the functions used to perform the actual operations.
. (Join-Path $THIS_SCRIPTS_DIRECTORY ApplicationServiceLibrary.ps1)

ConvertTo-ApplicationServices -Server "Our.WebServer.local" -ApplicationServicesInfo (Get-RqApplicationServiceInformation -Release $Release) -Verbose

The first thing it does is prompt for the $Release version number; again, if you don’t version your services then you can omit that.

The next thing it does is dot-source the ApplicationServicesLibrary.ps1 script to make all of the Get-*ApplicationServiceInformation functions that we defined in the previous file available.  I prefer to use the ApplicationServicesLibrary.ps1 file to place all of our services in a common place, and to avoid copy/pasting the ApplicationServiceInformation for each project into each Convert/Remove/Delete script; but that’s my personal choice and if you prefer to copy-paste the code into a few different files instead of having a central library file, go hard.  If you omit the Library script though, then you will instead need to dot-source the ApplicationServiceUtilities.ps1 file here, since our Library script currently dot-sources it in for us.

The final line is the one that actually calls our utility function to perform the operation.  It provides the web server hostname to connect to, and calls the library’s Get-*ApplicationServiceInformation to retrieve the information for the web applications that should be created.  Notice too that it also provides the –Verbose switch.  Some of the IIS operations can take quite a while to run and don’t generate any output, so I like to see the verbose output so I can gauge the progress of the script, but feel free to omit it.

So this sample script creates all of the web applications for our Rq product and can be ran very easily.  To make the corresponding Remove and Delete scripts, I would just copy this file and replace “ConvertTo-” with “Remove-” and “Delete-” respectively.  This allows you to have separate scripts for creating and removing each of your products that can easily be ran automatically or manually, fully automating the process of creating and removing your web applications in IIS.

If I need to remove the services for a bunch of versions, here is an example of how I can just create a quick script that calls my Remove Services script for each version that needs to be removed:

# Get the directory that this script is in.
$thisScriptsDirectory = Split-Path $script:MyInvocation.MyCommand.Path

# Remove Rq application services for versions 4.11.33 to 4.11.43.
$majorMinorVersion = "4.11"
33..43 | foreach {
    $Release = "$majorMinorVersion.$_"
    Write-Host "Removing Rq '$Release' services..."
    & "$thisScriptsDirectory\Remove-RqServices.ps1" $Release
}

If you have any questions or suggestions feel free to leave a comment.  I hope you find this useful.

Happy coding!

PowerShell 2.0 vs. 3.0 Syntax Differences And More

October 22nd, 2013 1 comment

I’m fortunate enough to work for a great company that tries to stay ahead of the curve and use newer technologies.  This means that when I’m writing my PowerShell (PS) scripts I typically don’t have to worry about only using PS v2.0 compatible syntax and cmdlets, as all of our PCs have v3.0 (soon to have v4.0).  This is great, until I release these scripts (or snippets from the scripts) for the general public to use; I have to keep in mind that many other people are still stuck running older versions of Windows, or not allowed to upgrade PowerShell.  So to help myself release PS v2.0 compatible scripts to the general public, I’m going to use this as a living document of the differences between PowerShell 2.0 and 3.0 that I encounter (so it will continue to grow over time; read as, bookmark it).  Of course there are other sites that have some of this info, but I’m going to try and compile a list of the ones that are relevant to me, in a nice simple format.

Before we get to the differences, here are some things you may want to know relating to PowerShell versions.

How to check which version of PowerShell you are running

All PS versions:

$PSVersionTable.PSVersion

 

How to run/test your script against an older version of PowerShell (source)

All PS versions:  use PowerShell.exe –Version [version] to start a new PowerShell session, where [version] is the PowerShell version that you want the session to use, then run your script in this new session.  Shorthand is PowerShell –v [version]

PowerShell.exe -Version 2.0

Note: You can’t run PowerShell ISE in an older version of PowerShell; only the Windows PowerShell console.

 

PowerShell v2 and v3 Differences:

 

Where-Object no longer requires braces (source)

PS v2.0:

Get-Service | Where { $_.Status -eq ‘running’ }

PS v3.0:

Get-Service | Where Status -eq ‘running

PS V2.0 Error Message:

Where : Cannot bind parameter ‘FilterScript’. Cannot convert the “[PropertyName]” value of the type “[Type]” to type “System.Management.Automation.ScriptBlock”.

 

Using local variables in remote sessions (source)

PS v2.0:

$class = "win32_bios"
Invoke-Command -cn dc3 {param($class) gwmi -class $class} -ArgumentList $class

PS v3.0:

$class = "win32_bios"
Invoke-Command -cn dc3 {gwmi -class $Using:class}

 

Variable validation attributes (source)

PS v2.0: Validation only available on cmdlet/function/script parameters.

PS v3.0: Validation available on cmdlet/function/script parameters, and on variables.

[ValidateRange(1,5)][int]$someLocalVariable = 1

 

Stream redirection (source)

The Windows PowerShell redirection operators use the following characters to represent each output type:
        *   All output
        1   Success output
        2   Errors
        3   Warning messages
        4   Verbose output
        5   Debug messages

NOTE: The All (*), Warning (3), Verbose (4) and Debug (5) redirection operators were introduced
       in Windows PowerShell 3.0. They do not work in earlier versions of Windows PowerShell.

 

PS v2.0: Could only redirect Success and Error output.

# Sends errors (2) and success output (1) to the success output stream.
Get-Process none, Powershell 2>&1

PS v3.0: Can also redirect Warning, Verbose, Debug, and All output.

# Function to generate each kind of output.
function Test-Output { Get-Process PowerShell, none; Write-Warning "Test!"; Write-Verbose "Test Verbose"; Write-Debug "Test Debug"}

# Write every output stream to a text file.
Test-Output *> Test-Output.txt

 

Explicitly set parameter set variable values when not defined (source)

PS v2.0 will throw an error if you try and access a parameter set parameter that has not been defined.  The solution is to give it a default value when it is not defined. Specify the Private scope in case a variable with the same name exists in the global scope or an inherited scope:

# Default the ParameterSet variables that may not have been set depending on which parameter set is being used. This is required for PowerShell v2.0 compatibility.
if (!(Test-Path Variable:Private:SomeStringParameter)) { $SomeStringParameter = $null }
if (!(Test-Path Variable:Private:SomeIntegerParameter)) { $SomeIntegerParameter = 0 }
if (!(Test-Path Variable:Private:SomeSwitchParameter)) { $SomeSwitchParameter = $false }

PS v2.0 Error Message:

The variable ‘$[VariableName]’ cannot be retrieved because it has not been set.

 

Parameter attributes require the equals sign

PS v2.0:

[parameter(Position=1,Mandatory=$true)] [string] $SomeParameter

PS v3.0:

[parameter(Position=1,Mandatory)] [string] $SomeParameter

PS v2.0 Error Message:

The “=” operator is missing after a named argument.

 

Cannot use String.IsNullOrWhitespace (or any other post .Net 3.5 functionality)

PS v2.0:

[string]::IsNullOrEmpty($SomeString)

PS v3.0:

[string]::IsNullOrWhiteSpace($SomeString)

PS v2.0 Error Message:

IsNullOrWhitespace : Method invocation failed because [System.String] doesn’t contain a method named ‘IsNullOrWhiteSpace’.

PS v2.0 compatible version of IsNullOrWhitespace function:

# PowerShell v2.0 compatible version of [string]::IsNullOrWhitespace.
function StringIsNullOrWhitespace([string] $string)
{
    if ($string -ne $null) { $string = $string.Trim() }
    return [string]::IsNullOrEmpty($string)
}

 

Get-ChildItem cmdlet’s –Directory and –File switches were introduced in PS v3.0

PS v2.0:

Get-ChildItem -Path $somePath | Where-Object { $_.PSIsContainer }	# Get directories only.
Get-ChildItem -Path $somePath | Where-Object { !$_.PSIsContainer }	# Get files only.

PS v3.0:

Get-ChildItem -Path $somePath -Directory
Get-ChildItem -Path $somePath -File

 

 

Other Links

Creating Strongly Typed Objects In PowerShell, Rather Than Using An Array Or PSCustomObject

October 21st, 2013 1 comment

I recently read a great article that explained how to create hashtables, dictionaries, and PowerShell objects.  I already knew a bit about these, but this article gives a great comparison between them, when to use each of them, and how to create them in the different versions of PowerShell.

Right now I’m working on refactoring some existing code into some general functions for creating, removing, and destroying IIS applications (read about it here).  At first, I thought that this would be a great place to use PSCustomObject, as in order to perform these operations I needed 3 pieces of information about a website; the Website name, the Application Name (essentially the path to the application under the Website root), and the Application Pool that the application should run in.

 

Using an array

So initially the code I wrote just used an array to hold the 3 properties of each application service:

# Store app service info as an array of arrays.
$AppServices = @(
	("MyWebsite", "$Version/Reporting.Services", "Services .NET4"),
	("MyWebsite", "$Version/Core.Services", "Services .NET4"),
	...
)

# Remove all of the Web Applications.
foreach ($appInfo in $AppServices )
{
	$website = $appInfo[0]
	$appName = $appInfo[1]
	$appPool = $appInfo[2]
	...
}

There is nothing “wrong” with using an array to store the properties; it works.  However, now that I am refactoring the functions to make them general purpose to be used by other people/scripts,  this does have one very undesirable limitation; The properties must always be stored in the correct order in the array (i.e. Website in position 0, App Name in 1, and App Pool in 2).  Since the list of app services will be passed into my functions, this would require the calling script to know to put the properties in this order.  Boo.

Another option that I didn’t consider when I originally wrote the script was to use an associative array, but it has the same drawbacks as using a PSCustomObject discussed below.

 

Using PSCustomObject

So I thought let’s use a PSCustomObject instead, as that way the client does not have to worry about the order of the information; as long as their PSCustomObject has Website, ApplicationPath, and ApplicationPool properties then we’ll be able to process it.  So I had this:

[PSCustomObject[]] $applicationServicesInfo = @(
	[PSCustomObject]@{Website = "MyWebsite"; ApplicationPath = "$Version/Reporting.Services"; ApplicationPool = "Services .NET4"},
	[PSCustomObject]@{Website = "MyWebsite"; ApplicationPath = "$Version/Core.Services"; ApplicationPool = "Services .NET4},
	...
)

function Remove-ApplicationServices
{
	param([PSCustomObject[]] $ApplicationServicesInfo)

	# Remove all of the Web Applications.
	foreach ($appInfo in [PSCustomObject[]]$ApplicationServicesInfo)
	{
		$website = $appInfo.Website
		$appPath = $appInfo.ApplicationPath
		$appPool = $appInfo.ApplicationPool
		...
	}
}

I liked this better as the properties are explicitly named, so there’s no guess work about which information the property contains, but it’s still not great.  One thing that I don’t have here (and really should), is validation to make sure that the passed in PSCustomObjects actually have Website, ApplicationPath, and ApplicationPool properties on them, otherwise an exception will be thrown when I try to access them.  So with this approach I would still need to have documentation and validation to ensure that the client passes in a PSCustomObject with those properties.

 

Using a new strongly typed object

I frequently read other PowerShell blog posts and recently stumbled across this one.  In the article he mentions creating a new compiled type by passing a string to the Add-Type cmdlet; essentially writing C# code in his PowerShell script to create a new class.  I knew that you could use Add-Type to import other assemblies, but never realized that you could use it to import an assembly that doesn’t actually exist (i.e. a string in your PowerShell script).  This is freaking amazing! So here is what my new solution looks like:

try {	# Wrap in a try-catch in case we try to add this type twice.
# Create a class to hold an IIS Application Service's Information.
Add-Type -TypeDefinition @"
	using System;
	
	public class ApplicationServiceInformation
	{
		// The name of the Website in IIS.
		public string Website { get; set;}
		
		// The path to the Application, relative to the Website root.
		public string ApplicationPath { get; set; }

		// The Application Pool that the application is running in.
		public string ApplicationPool { get; set; }

		// Implicit Constructor.
		public ApplicationServiceInformation() { }

		// Explicit constructor.
		public ApplicationServiceInformation(string website, string applicationPath, string applicationPool)
		{
			this.Website = website;
			this.ApplicationPath = applicationPath;
			this.ApplicationPool = applicationPool;
		}
	}
"@
} catch {}

$anotherService = New-Object ApplicationServiceInformation
$anotherService.Website = "MyWebsite"
$anotherService.ApplicationPath = "$Version/Payment.Services"
$anotherService.ApplicationPool = "Services .NET4"
	
[ApplicationServiceInformation[]] $applicationServicesInfo = @(
	(New-Object ApplicationServiceInformation("MyWebsite", "$Version/Reporting.Services", "Services .NET4")),
	(New-Object ApplicationServiceInformation -Property @{Website = "MyWebsite"; ApplicationPath = "$Version/Core.Services"; ApplicationPool = "Services .NET4}),
	$anotherService,
	...
)

function Remove-ApplicationServices
{
	param([ApplicationServiceInformation[]] $ApplicationServicesInfo)

	# Remove all of the Web Applications.
	foreach ($appInfo in [ApplicationServiceInformation[]]$ApplicationServicesInfo)
	{
		$website = $appInfo.Website
		$appPath = $appInfo.ApplicationPath
		$appPool = $appInfo.ApplicationPool
		...
	}
}

I first create a simple container class to hold the application service information, and now all of my properties are explicit like with the PSCustomObject, but also I’m guaranteed the properties will exist on the object that is passed into my function.  From there I declare my array of ApplicationServiceInformation objects, and the function that we can pass them into. Note that I wrap each New-Object call in parenthesis, otherwise PowerShell parses it incorrectly and will throw an error.

As you can see from the snippets above and below, there are several different ways that we can initialize a new instance of our ApplicationServiceInformation class:

$service1 = New-Object ApplicationServiceInformation("Explicit Constructor", "Core.Services", ".NET4")

$service2 = New-Object ApplicationServiceInformation -ArgumentList ("Explicit Constructor ArgumentList", "Core.Services", ".NET4")

$service3 = New-Object ApplicationServiceInformation -Property @{Website = "Using Property"; ApplicationPath = "Core.Services"; ApplicationPool = ".NET4"}

$service4 = New-Object ApplicationServiceInformation
$service4.Website = "Properties added individually"
$service4.ApplicationPath = "Core.Services"
$service4.ApplicationPool = "Services .NET4"

 

Caveats

  • Note that I wrapped the call to Add-Type in a Try-Catch block.  This is to prevent PowerShell from throwing an error if the type tries to get added twice.  It’s sort of a hacky workaround, but there aren’t many good alternatives, since you cannot unload an assembly.
  • This means that while developing if you make any changes to the class, you’ll have to restart your PowerShell session for the changes to be applied, since the Add-Type cmdlet will only work properly the first time that it is called in a session.

I hope you found something in here useful.

Happy coding!

PowerShell Functions To Delete Old Files And Empty Directories

October 15th, 2013 22 comments

I thought I’d share some PowerShell (PS) functions that I wrote for some clean-up scripts at work.  I use these functions to delete files older than a certain date. Note that these functions require PS v3.0; slower PS v2.0 compatible functions are given at the end of this article.

# Function to remove all empty directories under the given path.
# If -DeletePathIfEmpty is provided the given Path directory will also be deleted if it is empty.
# If -OnlyDeleteDirectoriesCreatedBeforeDate is provided, empty folders will only be deleted if they were created before the given date.
# If -OnlyDeleteDirectoriesNotModifiedAfterDate is provided, empty folders will only be deleted if they have not been written to after the given date.
function Remove-EmptyDirectories([parameter(Mandatory)][ValidateScript({Test-Path $_})][string] $Path, [switch] $DeletePathIfEmpty, [DateTime] $OnlyDeleteDirectoriesCreatedBeforeDate = [DateTime]::MaxValue, [DateTime] $OnlyDeleteDirectoriesNotModifiedAfterDate = [DateTime]::MaxValue, [switch] $OutputDeletedPaths, [switch] $WhatIf)
{
    Get-ChildItem -Path $Path -Recurse -Force -Directory | Where-Object { (Get-ChildItem -Path $_.FullName -Recurse -Force -File) -eq $null } | 
        Where-Object { $_.CreationTime -lt $OnlyDeleteDirectoriesCreatedBeforeDate -and $_.LastWriteTime -lt $OnlyDeleteDirectoriesNotModifiedAfterDate } | 
        ForEach-Object { if ($OutputDeletedPaths) { Write-Output $_.FullName } Remove-Item -Path $_.FullName -Force -WhatIf:$WhatIf }

    # If we should delete the given path when it is empty, and it is a directory, and it is empty, and it meets the date requirements, then delete it.
    if ($DeletePathIfEmpty -and (Test-Path -Path $Path -PathType Container) -and (Get-ChildItem -Path $Path -Force) -eq $null -and
        ((Get-Item $Path).CreationTime -lt $OnlyDeleteDirectoriesCreatedBeforeDate) -and ((Get-Item $Path).LastWriteTime -lt $OnlyDeleteDirectoriesNotModifiedAfterDate))
    { if ($OutputDeletedPaths) { Write-Output $Path } Remove-Item -Path $Path -Force -WhatIf:$WhatIf }
}

# Function to remove all files in the given Path that were created before the given date, as well as any empty directories that may be left behind.
function Remove-FilesCreatedBeforeDate([parameter(Mandatory)][ValidateScript({Test-Path $_})][string] $Path, [parameter(Mandatory)][DateTime] $DateTime, [switch] $DeletePathIfEmpty, [switch] $OutputDeletedPaths, [switch] $WhatIf)
{
    Get-ChildItem -Path $Path -Recurse -Force -File | Where-Object { $_.CreationTime -lt $DateTime } | 
		ForEach-Object { if ($OutputDeletedPaths) { Write-Output $_.FullName } Remove-Item -Path $_.FullName -Force -WhatIf:$WhatIf }
    Remove-EmptyDirectories -Path $Path -DeletePathIfEmpty:$DeletePathIfEmpty -OnlyDeleteDirectoriesCreatedBeforeDate $DateTime -OutputDeletedPaths:$OutputDeletedPaths -WhatIf:$WhatIf
}

# Function to remove all files in the given Path that have not been modified after the given date, as well as any empty directories that may be left behind.
function Remove-FilesNotModifiedAfterDate([parameter(Mandatory)][ValidateScript({Test-Path $_})][string] $Path, [parameter(Mandatory)][DateTime] $DateTime, [switch] $DeletePathIfEmpty, [switch] $OutputDeletedPaths, [switch] $WhatIf)
{
    Get-ChildItem -Path $Path -Recurse -Force -File | Where-Object { $_.LastWriteTime -lt $DateTime } | 
	ForEach-Object { if ($OutputDeletedPaths) { Write-Output $_.FullName } Remove-Item -Path $_.FullName -Force -WhatIf:$WhatIf }
    Remove-EmptyDirectories -Path $Path -DeletePathIfEmpty:$DeletePathIfEmpty -OnlyDeleteDirectoriesNotModifiedAfterDate $DateTime -OutputDeletedPaths:$OutputDeletedPaths -WhatIf:$WhatIf
}

The Remove-EmptyDirectories function removes all empty directories under the given path, and optionally (via the DeletePathIfEmpty switch) the path directory itself if it is empty after cleaning up the other directories. It also takes a couple parameters that may be specified if you only want to delete the empty directories that were created before a certain date, or that haven’t been written to since a certain date.

The Remove-FilesCreatedBeforeDate and Remove-FilesNotModifiedAfterDate functions are very similar to each other.  They delete all files under the given path whose Created Date or Last Written To Date, respectfully, is less than the given DateTime.  They then call the Remove-EmptyDirectories function with the provided date to clean up any left over empty directories.

To call the last 2 functions, just provide the path to the file/directory that you want it to delete if older than the given date-time.  Here are some examples of calling all the functions:

# Delete all files created more than 2 days ago.
Remove-FilesCreatedBeforeDate -Path "C:\Some\Directory" -DateTime ((Get-Date).AddDays(-2)) -DeletePathIfEmpty

# Delete all files that have not been updated in 8 hours.
Remove-FilesNotModifiedAfterDate -Path "C:\Another\Directory" -DateTime ((Get-Date).AddHours(-8))

# Delete a single file if it is more than 30 minutes old.
Remove-FilesCreatedBeforeDate -Path "C:\Another\Directory\SomeFile.txt" -DateTime ((Get-Date).AddMinutes(-30))

# Delete all empty directories in the Temp folder, as well as the Temp folder itself if it is empty.
Remove-EmptyDirectories -Path "C:\SomePath\Temp" -DeletePathIfEmpty

# Delete all empty directories created after Jan 1, 2014 3PM.
Remove-EmptyDirectories -Path "C:\SomePath\WithEmpty\Directories" -OnlyDeleteDirectoriesCreatedBeforeDate ([DateTime]::Parse("Jan 1, 2014 15:00:00"))

# See what files and directories would be deleted if we ran the command.
Remove-FilesCreatedBeforeDate -Path "C:\SomePath\Temp" -DateTime (Get-Date) -DeletePathIfEmpty -WhatIf

# Delete all files and directories in the Temp folder, as well as the Temp folder itself if it is empty, and output all paths that were deleted.
Remove-FilesCreatedBeforeDate -Path "C:\SomePath\Temp" -DateTime (Get-Date) -DeletePathIfEmpty -OutputDeletedPaths

Notice that I am using Get-Date to get the current date and time, and then subtracting the specified amount of time from it in order to get a date-time relative to the current time; you can use any valid DateTime though, such as a hard-coded date of January 1st, 2014 3PM.

I use these functions in some scripts that we run nightly via a scheduled task in Windows.  Hopefully you find them useful too.

 

PowerShell v2.0 Compatible Functions

As promised, here are the slower PS v2.0 compatible functions.  The main difference is that they use $_.PSIsContainer in the Where-Object clause rather than using the –File / –Directory Get-ChildItem switches.  The Measure-Command cmdlet shows that using the switches is about 3x faster than using the where clause, but since we are talking about milliseconds here you likely won’t notice the difference unless you are traversing a large file tree (which I happen to be for my scripts that we use to clean up TFS builds).

# Function to remove all empty directories under the given path.
# If -DeletePathIfEmpty is provided the given Path directory will also be deleted if it is empty.
# If -OnlyDeleteDirectoriesCreatedBeforeDate is provided, empty folders will only be deleted if they were created before the given date.
# If -OnlyDeleteDirectoriesNotModifiedAfterDate is provided, empty folders will only be deleted if they have not been written to after the given date.
function Remove-EmptyDirectories([parameter(Mandatory=$true)][ValidateScript({Test-Path $_})][string] $Path, [switch] $DeletePathIfEmpty, [DateTime] $OnlyDeleteDirectoriesCreatedBeforeDate = [DateTime]::MaxValue, [DateTime] $OnlyDeleteDirectoriesNotModifiedAfterDate = [DateTime]::MaxValue, [switch] $OutputDeletedPaths, [switch] $WhatIf)
{
    Get-ChildItem -Path $Path -Recurse -Force | Where-Object { $_.PSIsContainer -and (Get-ChildItem -Path $_.FullName -Recurse -Force | Where-Object { !$_.PSIsContainer }) -eq $null } | 
        Where-Object { $_.CreationTime -lt $OnlyDeleteDirectoriesCreatedBeforeDate -and $_.LastWriteTime -lt $OnlyDeleteDirectoriesNotModifiedAfterDate } | 
        ForEach-Object { if ($OutputDeletedPaths) { Write-Output $_.FullName } Remove-Item -Path $_.FullName -Force -WhatIf:$WhatIf }

    # If we should delete the given path when it is empty, and it is a directory, and it is empty, and it meets the date requirements, then delete it.
    if ($DeletePathIfEmpty -and (Test-Path -Path $Path -PathType Container) -and (Get-ChildItem -Path $Path -Force) -eq $null -and
        ((Get-Item $Path).CreationTime -lt $OnlyDeleteDirectoriesCreatedBeforeDate) -and ((Get-Item $Path).LastWriteTime -lt $OnlyDeleteDirectoriesNotModifiedAfterDate))
    { if ($OutputDeletedPaths) { Write-Output $Path } Remove-Item -Path $Path -Force -WhatIf:$WhatIf }
}

# Function to remove all files in the given Path that were created before the given date, as well as any empty directories that may be left behind.
function Remove-FilesCreatedBeforeDate([parameter(Mandatory=$true)][ValidateScript({Test-Path $_})][string] $Path, [parameter(Mandatory)][DateTime] $DateTime, [switch] $DeletePathIfEmpty, [switch] $OutputDeletedPaths, [switch] $WhatIf)
{
    Get-ChildItem -Path $Path -Recurse -Force | Where-Object { !$_.PSIsContainer -and $_.CreationTime -lt $DateTime } | 
		ForEach-Object { if ($OutputDeletedPaths) { Write-Output $_.FullName } Remove-Item -Path $_.FullName -Force -WhatIf:$WhatIf }
    Remove-EmptyDirectories -Path $Path -DeletePathIfEmpty:$DeletePathIfEmpty -OnlyDeleteDirectoriesCreatedBeforeDate $DateTime -OutputDeletedPaths:$OutputDeletedPaths -WhatIf:$WhatIf
}

# Function to remove all files in the given Path that have not been modified after the given date, as well as any empty directories that may be left behind.
function Remove-FilesNotModifiedAfterDate([parameter(Mandatory=$true)][ValidateScript({Test-Path $_})][string] $Path, [parameter(Mandatory)][DateTime] $DateTime, [switch] $DeletePathIfEmpty, [switch] $OutputDeletedPaths, [switch] $WhatIf)
{
    Get-ChildItem -Path $Path -Recurse -Force | Where-Object { !$_.PSIsContainer -and $_.LastWriteTime -lt $DateTime } | 
	ForEach-Object { if ($OutputDeletedPaths) { Write-Output $_.FullName } Remove-Item -Path $_.FullName -Force -WhatIf:$WhatIf }
    Remove-EmptyDirectories -Path $Path -DeletePathIfEmpty:$DeletePathIfEmpty -OnlyDeleteDirectoriesNotModifiedAfterDate $DateTime -OutputDeletedPaths:$OutputDeletedPaths -WhatIf:$WhatIf
}

Happy coding!

Windows Phone Developers: Do not renew your subscription until the expiry DAY or else Microsoft steals your money

October 1st, 2013 No comments

The Problem

So as I found out today, if you renew your Windows Phone Developer subscription early, it does not renew it for a year from the expiry date, it renews it for a year from the date you paid to have it renewed.  So essentially you pay for a 12 month subscription, but receive less than 12 months.  I’m not sure if the Windows Store subscription has the same problem or not, but beware.

 

The Story

After this happened I started up a support request chat with MS to have them extend the expiry date to what it should be, but was told that they are not able to do this.  Here is our chat transcript:

General Info

Chat start time

Oct 1, 2013 6:16:27 PM EST

Chat end time

Oct 1, 2013 6:40:49 PM EST

Duration (actual chatting time)

00:24:21

Operator

Adrian

Chat Transcript

info: Please wait for an agent to respond.  You are currently ‘1’ in the queue.
info: Privacy Statement
You are now chatting with ‘Adrian’.
Adrian: Hello, Dan
Dan: Hi there, I just renewed my Windows Phone Developer subscription today
Dan: My old expiry date was 10/24/2013, but when I renewed it the new expiry date is 10/1/2014
Dan: but it should be 10/24/2014
Adrian: So I understand that you have renewed your subscription before the expiration date and it seems that you have lost several days on your subscription.
Dan: yup, that’s basically it
Dan: I got the email notification about it expiring soon today, so I thought I would do it now before I forgot about it
Adrian: As it turns out, renewing your subscription manually is only available within 30 days of the expiration, but currently it does not stack the subscription.
Adrian: We recommend that you wait closer to your renewal date or let the account auto-renew (on by default) so that you do not lose any days.
Dan: so can you manually adjust my expiration date to be 10/24/2014 like it should, and submit a bug for them to fix that?
Adrian: I apologize for the inconvenience since I currently do not have a way to extend the subscription or modify the expiration date. Our engineers are already aware of the renewal behavior, but there is no estimated date on when a change will be implemented.
Dan: so can you guys credit my credit card for the difference then? You know that’s essentially jut stealing then…
Dan: Or just escalate me to a supervisor/manager/engineer who does have the ability to change the expiration date?
Adrian: I apologize as there is not a way to modify the expiration date within the system. My team works here as peers so there is no escalation path. The prorated amount for 24 days would amount to 1.28 USD out of 19 USD for a years subscription however we do not offer partial refunds per Microsoft policy.
Adrian: At best, I can refund the full amount and cancel your current subscription.  
Adrian: If there is any other way that I can offer my assistance I will be glad to help.
Adrian: Hello, are you there? I have not received any response from you.
Adrian: I have not yet received any response from you. I’ll wait for a minute before closing this chat session. Please feel free to initiate a new chat session so that we can assist you further.
Adrian: For some reason, possibly due to technical difficulty, I have not received a response from you. I will update the case notes and end this session. Please feel free to initiate a new chat session so that we can assist you further. Thank you for contacting MarketPlace Chat Support. Have a great day!

I started this chat up while at work and unfortunately had to leave my desk for 15 minutes, so Adrian closed our chat before I could reply.  The Windows Phone Developer subscription is now only $19/year, but I actually used a promo code that I received many months ago when it was still $100/year; so while Adrian mentioned that the missed days only added up to $1.28, the cost would actually be closer to $10.28 for the people who gave me the promo code.  Also, by this time next year the price may go back up to $100/year, in which case I’ll be forking over the $10.28 to pay for the month of October 2014.

Also, while Adrian admits that “Our engineers are already aware of the renewal behavior, but there is no estimated date on when a change will be implemented.”, this behaviour is not stated anywhere on the web page when renewing your account.  This seems pretty irresponsible to me, especially when it directly affects payments.  Can you imagine if your internet provider was allowed to just charge you for an extra month of service without warning or agreement.

Adrian mentioned that the recommended thing to do is to just let your annual subscription auto-renew.  This is likely the ideal situation, but I’m often paranoid that automatic transactions won’t go through and will be unnoticed (I’ve been bit by this in the past), or that by the time the renewal comes around my credit card info will have changed, etc., so I often manually renew my annual subscriptions.  Especially when the consequence of not renewing your membership means your apps are removed from the store and you stop making money off of them.  MS is basically stealing money from those who choose to manually renew their subscription.

I’m not going to bother pursuing this with MS as $10 isn’t worth the time or stress, but I wanted to try and let others know so that you don’t get burned as well.