Let’s Get Chocolatey

It’s taken a little while, but we are pleased to announce that the Virtual Engine Toolkit (VET) and the App-V Configuration Editor (ACE) are now available on Chocolatey! If you have Chocolatey installed on your systems then you can get going by running the commands listed below.

image

Microsoft Package Management

It gets even better if you’re running the Windows 10 Insider preview, the latest WMF 5.0 April 2015 preview or have installed the latest experimental OneGet build on Windows 7 and up, you can download these directly – today!

To do this, run the Install-Package ace or Install-Package vet commands, like so:

image
Other Packages

It doesn’t end here though. We have also published the following packages that you might find useful to the public Chocolatey feed:

Now – go get Chocolatey 😀

Testing Private Functions with Pester

We’ve been busy beavering away on a new Powershell module that is comprised of many .ps1 files, that are loaded by a master/control .psm1 file when the module is imported. And, like all good Powershell citizens, we have many Pester unit tests for this code in accompanying .Tests.ps1 files.

As Powershell script modules permit us to control which functions should be exported with Export-ModuleMember and Pester allows us to test non-exported functions with the InModuleScope option, you might be wondering why we would ever need to be able to test private/internal functions?

Script Bundles

Whilst coding the new Powershell module, we have always had the desire to ensure that it could also be used as a ‘bundled’ .ps1 file. By bundle, we mean a single combined .ps1 file that can be included verbatim at the beginning of an existing script or by dot-sourcing it as required. Unfortunately – in this particular scenario – the internal module functions would be exposed and could potentially cause unnecessary confusion.

Here’s an example where both the ‘PublicFunction’ and ‘InnerPrivate’ functions would be exposed when bundled or dot-sourced into an existing .ps1 file.

function InnerPrivate {
    param ()
    Write-Output ‘InnerPrivate’
}

function PublicFunction {
    param()
    Write-Output (InnerPrivate)
}

If we only want the ‘PublicFunction’ visible then the simple solution to this is to nest the private function(s) inside the public function(s) like so:

function PublicFunction {
    param()

    function InnerPrivate {
        param ()
        Write-Output ‘InnerPrivate’
    }

   Write-Output (InnerPrivate)
}

Now only the ‘PublicFunction’ will be available. End of the story?

Internal Functions

Not quite; by hiding the functions we now cannot test them with Pester. I had a very brief conversation with Dave Wyatt on GitHub about this and it was agreed that this functionality should not be a part of the official Pester release.

To solve this particular issue, we have a simple function that will locate and return a function’s definition from within a .ps1 file as a script block. The function definition can then be dot-sourced into the current Pester scope to enable testing. Here’s an example Pester test file that will test our ‘InnerPrivate’ function:

$here = Split-Path –Parent $MyInvocation.MyCommand.Path
$sut = (Split-Path –Leaf $MyInvocation.MyCommand.Path).Replace(“.Tests.”, “.”)
# If only testing an internal function, there is no need to dot-source the entire contents..

Describe “InnerPrivate” {
    # Import the ‘InnerPrivate’ function into the current scope
    . (Get-FunctionDefinition –Path “$here\$sut” –Function InnerPrivate)

    It “tests the private, inner function” {
        InnerPrivate | Should Be ‘InnerPrivate’
    }
}

If this code is of interest you can simply save the following code as a .ps1 file in Pester’s \Function directory, for example \Functions\FunctionDefinition.ps1, and Pester will automatically load it.

Notes:

  1. As this code utilises the Abstract Syntax Tree (AST) it does require Powershell 3.0;
  2. If there are any dependencies on variables in the function’s parent scope these will need to be mocked/accounted for;
  3. Depending on how you install/update the Pester module, this might get overwritten when Pester is updated.

When we have more time we’ll put this up on Github so people can collaborate on changes. In the meantime, here’s a copy of the function.

#Requires -Version 3

<#
.SYNOPSIS
    Retrieves a function's definition from a .ps1 file or ScriptBlock.
.DESCRIPTION
    Returns a function's source definition as a Powershell ScriptBlock from an
    external .ps1 file or existing ScriptBlock. This module is primarily
    intended to be used to test private/nested/internal functions with Pester
    by dot-sourcsing the internal function into Pester's scope.
.PARAMETER Function
    The source function's name to return as a [ScriptBlock].
.PARAMETER Path
    Path to a Powershell script file that contains the source function's
    definition.
.PARAMETER LiteralPath
    Literal path to a Powershell script file that contains the source
    function's definition.
.PARAMETER ScriptBlock
    A Powershell [ScriptBlock] that contains the function's definition.
.EXAMPLE
    If the following functions are defined in a file named 'PrivateFunction.ps1'

    function PublicFunction {
        param ()

        function PrivateFunction {
            param ()
            Write-Output 'InnerPrivate'
        }

        Write-Output (PrivateFunction)
    }

    The 'PrivateFunction' function can be tested with Pester by dot-sourcing
    the required function in the either the 'Describe', 'Context' or 'It'
    scopes.

    Describe "PrivateFunction" {
        It "tests private function" {
            ## Import the 'PrivateFunction' definition into the current scope.
            . (Get-FunctionDefinition -Path "$here\$sut" -Function PrivateFunction)
            PrivateFunction | Should BeExactly 'InnerPrivate'
        }
    }
.LINK
    
Testing Private Functions with Pester
#> function Get-FunctionDefinition { [CmdletBinding(DefaultParameterSetName='Path')] [OutputType([System.Management.Automation.ScriptBlock])] param ( [Parameter(Position = 0, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true, ParameterSetName='Path')] [ValidateNotNullOrEmpty()] [Alias('PSPath','FullName')] [System.String] $Path = (Get-Location -PSProvider FileSystem), [Parameter(Position = 0, ValueFromPipelineByPropertyName = $true, ParameterSetName = 'LiteralPath')] [ValidateNotNullOrEmpty()] [System.String] $LiteralPath, [Parameter(Position = 0, ValueFromPipeline = $true, ParameterSetName = 'ScriptBlock')] [ValidateNotNullOrEmpty()] [System.Management.Automation.ScriptBlock] $ScriptBlock, [Parameter(Mandatory = $true, Position =1, ValueFromPipelineByPropertyName = $true)] [Alias('Name')] [System.String] $Function ) begin { if ($PSCmdlet.ParameterSetName -eq 'Path') { $Path = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($Path); } elseif ($PSCmdlet.ParameterSetName -eq 'LiteralPath') { ## Set $Path reference to the literal path(s) $Path = $LiteralPath; } } # end begin process { $errors = @(); $tokens = @(); if ($PSCmdlet.ParameterSetName -eq 'ScriptBlock') { $ast = [System.Management.Automation.Language.Parser]::ParseInput($ScriptBlock.ToString(), [ref] $tokens, [ref] $errors); } else { $ast = [System.Management.Automation.Language.Parser]::ParseFile($Path, [ref] $tokens, [ref] $errors); } [System.Boolean] $isFunctionFound = $false; $functions = $ast.FindAll({ $args[0] -is [System.Management.Automation.Language.FunctionDefinitionAst] }, $true); foreach ($f in $functions) { if ($f.Name -eq $Function) { Write-Output ([System.Management.Automation.ScriptBlock]::Create($f.Extent.Text)); $isFunctionFound = $true; } } # end foreach function if (-not $isFunctionFound) { if ($PSCmdlet.ParameterSetName -eq 'ScriptBlock') { $errorMessage = 'Function "{0}" not defined in script block.' -f $Function; } else { $errorMessage = 'Function "{0}" not defined in "{1}".' -f $Function, $Path; } Write-Error -Message $errorMessage; } } # end process } #end function Get-Function

App-V 5 Sequencer Template – Full VFS Write Mode

With the recent release of Hotfix 4 for App-V 5.0, Microsoft has now provided the ability to “Allow virtual applications full write permissions to the virtual file system”. This setting can be found in the sequencer under the “Advanced” tab as demonstrated in the screen shot below:

 Sequncer Full VFS

Should you wish to enable this setting as a default whenever you create a new package, then simply go ahead and add <FullVFSWriteMode>true<FullVFSWriteMode> into your sequencer template (.appvt), as you can see below. This setting is only valid where you have the App-V 5.0 SP2 Hotfix 4 sequencer installed.

<?xml version="1.0" encoding="utf-8"?>
<SequencerTemplate xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
  <AllowMU>false</AllowMU>
  <AppendPackageVersionToFilename>false</AppendPackageVersionToFilename>
  <AllowLocalInteractionToCom>false</AllowLocalInteractionToCom>
  <AllowLocalInteractionToObject>false</AllowLocalInteractionToObject>
  <FullVFSWriteMode>true</FullVFSWriteMode>
  <ExcludePreExistingSxSAndVC>false</ExcludePreExistingSxSAndVC>
  <FileExclusions>
    <string>[{CryptoKeys}]</string>
    <string>[{Common AppData}]\Microsoft\Crypto</string>
    <string>[{Common AppData}]\Microsoft\Search\Data</string>
    <string>[{Cookies}]</string>
    <string>[{History}]</string>
    <string>[{Cache}]</string>
    <string>[{Local AppData}]</string>
    <string>[{Personal}]</string>
    <string>[{Profile}]\Local Settings</string>
    <string>[{Profile}]\NTUSER.DAT.LOG1</string>
    <string>[{Profile}]\NTUSER.DAT.LOG2</string>
    <string>[{Recent}]</string>
    <string>[{Windows}]\Debug</string>
    <string>[{Windows}]\Logs\CBS</string>
    <string>[{Windows}]\Temp</string>
    <string>[{Windows}]\WinSxS\ManifestCache</string>
    <string>[{Windows}]\WindowsUpdate.log</string>
    <string>[{AppVPackageDrive}]\$Recycle.Bin</string>
    <string>[{AppVPackageDrive}]\System Volume Information</string>
    <string>[{AppData}]\Microsoft\AppV</string>
    <string>[{Local AppData}]\Temp</string>
    <string>[{ProgramFilesX64}]\Microsoft Application Virtualization\Sequencer</string>
  </FileExclusions>
  <RegExclusions>
    <string>REGISTRY\MACHINE\SOFTWARE\Wow6432Node\Microsoft\Cryptography</string>
    <string>REGISTRY\MACHINE\SOFTWARE\Microsoft\Cryptography</string>
    <string>REGISTRY\USER\[{AppVCurrentUserSID}]\Software\Microsoft\Windows\CurrentVersion\Explorer\StreamMRU</string>
    <string>REGISTRY\USER\[{AppVCurrentUserSID}]\Software\Wow6432Node\Microsoft\Windows\CurrentVersion\Explorer\StreamMRU</string>
    <string>REGISTRY\USER\[{AppVCurrentUserSID}]\Software\Microsoft\Windows\CurrentVersion\Explorer\Streams</string>
    <string>REGISTRY\USER\[{AppVCurrentUserSID}]\Software\Wow6432Node\Microsoft\Windows\CurrentVersion\Explorer\Streams</string>
    <string>REGISTRY\MACHINE\SOFTWARE\Microsoft\AppV</string>
    <string>REGISTRY\MACHINE\SOFTWARE\Wow6432Node\Microsoft\AppV</string>
    <string>REGISTRY\USER\[{AppVCurrentUserSID}]\Software\Microsoft\AppV</string>
    <string>REGISTRY\USER\[{AppVCurrentUserSID}]\Software\Wow6432Node\Microsoft\AppV</string>
  </RegExclusions>
  <TargetOSes />
</SequencerTemplate>

And if you’re not currently using a template then I’d highly recommend you do. You’ll find a great article over at Rory Monaghan’s blog that explains the use of the sequencer template in more detail.

Nathan

Creating .CAB files with Powershell

During our on-going development of online updateable Powershell help (more on that later) you quickly come to realise that each culture-specific set of help files is stored within its own 1980’s style cabinet (or .cab) file. When packing the .cab files for release, I like to automate (if you hadn’t already guessed!) as it makes things quick, easy and certainly less error prone.

Working with .cab files in Powershell requires use of MAKECAB.EXE which, fortunately, is distributed with each edition of Windows. In the good ol’ days we could use the MakeCab.MakeCab.1 COM object, but this has been deprecated since Windows Vista. I had a quick Google and couldn’t find anything easily reusable (except Ed Wilson’s post here) and thus this blog post.

I set to work, creating a Powershell advanced function that would easily allow me to package each culture-specific help file (or one or more files) into its own cabinet file. It would probably be more prudent to utilise .NET’s StringBuilder class but performance is not (currently) an issue or priority!

You never know I might come back and show you how we use this with Psake in the future… Here are some examples of how you can use it:

EXAMPLE

New-CabinetFile -Name MyCabinet.cab -File "File01.exe","File02.txt"

This creates a new MyCabinet.cab file in the current directory and adds the File01.exe and File02.txt files to it, also from the current directory.
EXAMPLE

Get-ChildItem C:\CabFile\ | New-CabinetFile -Name MyCabinet.cab -DestinationPath C:\Users\UserA\Documents

This creates a new C:\Users\UserA\Documents\MyCabinet.cab file and adds all files within the C:\CabFile\ directory into it.

Here’s the full advanced function:

<#
.SYNOPSIS
    Creates a new cabinet .CAB file on disk.

.DESCRIPTION
    This cmdlet creates a new cabinet .CAB file using MAKECAB.EXE and adds
    all the files specified to the cabinet file itself.

.PARAMETER Name
    The output file name of the cabinet .CAB file, such as MyNewCabinet.cab.
    This should not be the entire file path, only the target file name.

.PARAMETER File
    One or more file references that are to be added to the cabinet .CAB file.
    FileInfo objects (as generated by Get-Item etc) or strings can be passed
    in via the pipeline to be added to the cabinet file.

.PARAMETER DestinationPath
    The output file path that the cabinet file will be saved in. It is also
    used for resolving any ambiguous file references, i.e. any file passed in
    via file name and not full path.

    If not specified the current working directory is used for the output file
    and attempting to resolve all ambiguous file references.

.PARAMETER NoClobber
    Will not overwrite of an existing file. By default, if a file exists in the
    specified path, New-CabinetFile overwrites the file without warning.

.EXAMPLE
    New-CabinetFile -Name MyCabinet.cab -File "File01.exe","File02.txt"
    
    This creates a new MyCabinet.cab file in the current directory and adds the File01.exe and File02.txt files to it, also from the current directory.
.EXAMPLE
    Get-ChildItem C:\CabFile\ | New-CabinetFile -Name MyCabinet.cab -DestinationPath C:\Users\UserA\Documents

    This creates a new C:\Users\UserA\Documents\MyCabinet.cab file and adds all files within the C:\CabFile\ directory into it.
#>
function New-CabinetFile {
    [CmdletBinding()]
    Param(
        [Parameter(HelpMessage="Target .CAB file name.", Position=0, Mandatory=$true, ValueFromPipelineByPropertyName=$true)]
        [ValidateNotNullOrEmpty()]
        [Alias("FilePath")]
        [string] $Name,

        [Parameter(HelpMessage="File(s) to add to the .CAB.", Position=1, Mandatory=$true, ValueFromPipeline=$true)]
        [ValidateNotNullOrEmpty()]
        [Alias("FullName")]
        [string[]] $File,

        [Parameter(HelpMessage="Default intput/output path.", Position=2, ValueFromPipelineByPropertyName=$true)]
        [AllowNull()]
        [string[]] $DestinationPath,

        [Parameter(HelpMessage="Do not overwrite any existing .cab file.")]
        [Switch] $NoClobber
        )

    Begin { 
    
        ## If $DestinationPath is blank, use the current directory by default
        if ($DestinationPath -eq $null) { $DestinationPath = (Get-Location).Path; }
        Write-Verbose "New-CabinetFile using default path '$DestinationPath'.";
        Write-Verbose "Creating target cabinet file '$(Join-Path $DestinationPath $Name)'.";

        ## Test the -NoClobber switch
        if ($NoClobber) {
            ## If file already exists then throw a terminating error
            if (Test-Path -Path (Join-Path $DestinationPath $Name)) { throw "Output file '$(Join-Path $DestinationPath $Name)' already exists."; }
        }

        ## Cab files require a directive file, see 'http://msdn.microsoft.com/en-us/library/bb417343.aspx#dir_file_syntax' for more info
        $ddf = ";*** MakeCAB Directive file`r`n";
        $ddf += ";`r`n";
        $ddf += ".OPTION EXPLICIT`r`n";
        $ddf += ".Set CabinetNameTemplate=$Name`r`n";
        $ddf += ".Set DiskDirectory1=$DestinationPath`r`n";
        $ddf += ".Set MaxDiskSize=0`r`n";
        $ddf += ".Set Cabinet=on`r`n";
        $ddf += ".Set Compress=on`r`n";
        ## Redirect the auto-generated Setup.rpt and Setup.inf files to the temp directory
        $ddf += ".Set RptFileName=$(Join-Path $ENV:TEMP "setup.rpt")`r`n";
        $ddf += ".Set InfFileName=$(Join-Path $ENV:TEMP "setup.inf")`r`n";

        ## If -Verbose, echo the directive file
        if ($PSCmdlet.MyInvocation.BoundParameters["Verbose"].IsPresent) {
            foreach ($ddfLine in $ddf -split [Environment]::NewLine) {
                Write-Verbose $ddfLine;
            }
        }
    }

    Process {
   
        ## Enumerate all the files add to the cabinet directive file
        foreach ($fileToAdd in $File) {
        
            ## Test whether the file is valid as given and is not a directory
            if (Test-Path $fileToAdd -PathType Leaf) {
                Write-Verbose """$fileToAdd""";
                $ddf += """$fileToAdd""`r`n";
            }
            ## If not, try joining the $File with the (default) $DestinationPath
            elseif (Test-Path (Join-Path $DestinationPath $fileToAdd) -PathType Leaf) {
                Write-Verbose """$(Join-Path $DestinationPath $fileToAdd)""";
                $ddf += """$(Join-Path $DestinationPath $fileToAdd)""`r`n";
            }
            else { Write-Warning "File '$fileToAdd' is an invalid file or container object and has been ignored."; }
        }       
    }

    End {
    
        $ddfFile = Join-Path $DestinationPath "$Name.ddf";
        $ddf | Out-File $ddfFile -Encoding ascii | Out-Null;

        Write-Verbose "Launching 'MakeCab /f ""$ddfFile""'.";
        $makeCab = Invoke-Expression "MakeCab /F ""$ddfFile""";

        ## If Verbose, echo the MakeCab response/output
        if ($PSCmdlet.MyInvocation.BoundParameters["Verbose"].IsPresent) {
            ## Recreate the output as Verbose output
            foreach ($line in $makeCab -split [environment]::NewLine) {
                if ($line.Contains("ERROR:")) { throw $line; }
                else { Write-Verbose $line; }
            }
        }

        ## Delete the temporary .ddf file
        Write-Verbose "Deleting the directive file '$ddfFile'.";
        Remove-Item $ddfFile;

        ## Return the newly created .CAB FileInfo object to the pipeline
        Get-Item (Join-Path $DestinationPath $Name);
    }
}

 

Searching for String Properties with Powershell

I had a requirement recently to parse a configuration file (let’s just say for documentation purposes) and I needed to retrieve a property/value pair which may or may not be present in a text line. Now, depending on the product we wish to document, we might have a line in the configuration file constructed as follows:

set interface 1/3 -ifAlias DMZ -throughput 0 -bandwidthHigh 0 -bandwidthNormal 0 -intftype "Xen Virtual" -ifnum 1/3

Now, if wanted the “-intfType” value we could split the string using the Powershell .Split() method like so:

PS C:\> $SourceString = 'set interface 1/3 -ifAlias DMZ -throughput 0 -bandwidthHigh 0 -bandwidthNormal 0 -intftype "Xen Virtual" -ifnum 1/3';
$LineComponents = $SourceString.Split();
$LineComponents[12];
"Xen

However, there are two issues here:

  1. What happens if there are additional or missing properties (the index/order will change)?
  2. We were hoping/expecting to see “Xen Virtual” output and not just “Xen.

Here’s a quick function that will solve issue #1 (complete with case insensitivity):

#############################################################################
#.SYNOPSIS
# Get named property from a string.
#
#.DESCRIPTION
# Returns a case-insensitive property from a string, assuming the property is
# named before the actual property value and is separated by a space. For
# example, if the specified SearchString contained
# "-property1 <value1> -property2 <value2>”, searching # for "-Property1"
# would return "<value1>".
#
#.PARAMETER SearchString
# String to search for the specified property in.
#
#.PARAMETER PropertyName
# The property name to search for.
#
#.PARAMETER Default
# If the property is not found return the specified string. This parameter is
# optional and if not specified returns $null by default.
#
#.EXAMPLE
# $propertyValue = Get-StringProperty $StringToSearch "-property1"
#
#.EXAMPLE
# $propertyValue = Get-StringProperty $StringToSearch "-property3" "Not found"
##############################################################################
function Get-StringProperty([string]$SearchString, [string]$PropertyName, [string]$Default = $null)
{
    # Split the $SearchString based on one or more blank spaces
    $stringComponents = $SearchString.Split(' +',[StringSplitOptions]'RemoveEmptyEntries'); 
    for ($i = 0; $i -le $stringComponents.Length; $i++) {
        # The standard Powershell CompareTo method is case-sensitive
        if ([string]::Compare($stringComponents[$i], $PropertyName, $true) -eq 0) {
            # Check that we're not over the array boundary
            if ($i+1 -le $stringComponents.Length) {
                # If you wanted to trim quotation marks you could use this instead:
                #  return $StringComponents[$i+1].Trim('"');
                return $stringComponents[$i+1];
            }
        }
    }
    # If nothing has been found or we're over the array boundary, return the $EmptyString value
    return $Default;
}

$SourceString = 'set interface 1/3 -ifAlias DMZ -throughput 0 -bandwidthHigh 0 -bandwidthNormal 0 -intftype "Xen Virtual" -ifnum 1/3';
Get-StringProperty $SourceString "-intftype"

"Xen

Solving issue #2 is a little more involved as we need find the quoted text, escape it and then restore it when needed. The following Get-StringProperty advanced function will first replace all quoted spaces with “^” and then replace the quotes themselves with “^^”. This permits the split function to work as expected. Finally, everything is put back together just before we need it!

<#
.SYNOPSIS
   Get a named property value from a string.
.DESCRIPTION
   Returns a case-insensitive property from a string, assuming the property is
   named before the actual property value and is separated by a space. For
   example, if the specified SearchString contained "-property1 <value1>
   -property2 <value2>”, searching for "-Property1" would return "<value1>".
.PARAMETER SearchString
   String to search for the specified property name.
.PARAMETER PropertyName
   The property name to search the SearchString for.
.PARAMETER Default
   If the property is not found returns the specified string. This parameter is
   optional and if not specified returns $null (by default) if the property is
   not found.
.EXAMPLE
   Get-StringProperty -SearchString $StringToSearch -PropertyName "-property1"

   This command searches the $StringToSearch variable for the presence of the property
   "-property1" and returns its value, if found. If the property name is not found,
   the default $null will be returned.
.EXAMPLE
   Get-StringProperty $StringToSearch "-property3" "Not found"

   This command searches the $StringToSearch variable for the presence of the property
   "-property3" and returns its value, if found. If the property name is not found,
   the "Not Found" string will be returned.
.NOTES
   Author - Iain Brighton - @iainbrighton, iain.brighton@virtualengine.co.uk
#>
function Get-StringProperty {

    [CmdletBinding(HelpUri = 'https://virtualengine.co.uk/2014/searching-for-string-properties-with-powershell/')]
    [OutputType([String])]
    Param
    (
        [Parameter(Mandatory=$true, ValueFromPipeline=$true, ValueFromPipelineByPropertyName=$true, Position=0)]
        [ValidateNotNullOrEmpty()]
        [Alias("Search")]
        [string]
        $SearchString,

        [Parameter(Mandatory=$true, ValueFromPipelineByPropertyName=$true, Position=1)]
        [ValidateNotNullOrEmpty()]
        [Alias("Name","Property")]
        [string]
        $PropertyName,

        [Parameter(ValueFromPipelineByPropertyName=$true, Position=2)]
        [AllowNull()]
        [String]
        $Default = $null
    )

    Begin { }

    Process {
        # Locate and replace quotes with '^^' and quoted spaces '^' to aid with parsing, until there are none left
        while ($SearchString.Contains('"')) {
            # Store the right-hand side temporarily, skipping the first quote
            $searchStringRight = $SearchString.Substring($SearchString.IndexOf('"') +1);
            # Extract the quoted text from the original string
            $quotedString = $SearchString.Substring($SearchString.IndexOf('"'), $searchStringRight.IndexOf('"') +2);
            # Replace the quoted text, replacing spaces with '^' and quotes with '^^'
            $SearchString = $SearchString.Replace($quotedString, $quotedString.Replace(" ", "^").Replace('"', "^^"));
        }

        # Split the $SearchString based on one or more blank spaces
        $stringComponents = $SearchString.Split(' +',[StringSplitOptions]'RemoveEmptyEntries'); 
        for ($i = 0; $i -le $stringComponents.Length; $i++) {
            # The standard Powershell CompareTo method is case-sensitive
            if ([string]::Compare($stringComponents[$i], $PropertyName, $True) -eq 0) {
                # Check that we're not over the array boundary
                if ($i+1 -le $stringComponents.Length) {
                    # Restore any escaped quotation marks and spaces
                    # If you wanted to trim quotation marks you could use this instead:
                    #  return $stringComponents[$i+1].Replace("^^", '"').Replace("^", " ").Trim('"');
                    return $stringComponents[$i+1].Replace("^^", '"').Replace("^", " ");
                }
            }
        }
        # If nothing has been found or we're over the array boundary, return the default value
        return $Default;
    }

    End { }
}

$SourceString = 'set interface 1/3 -ifAlias DMZ -throughput 0 -bandwidthHigh 0 -bandwidthNormal 0 -intftype "Xen Virtual" -ifnum 1/3';
Get-StringProperty $SourceString "-intftype"

"Xen Virtual"

That’s much better Smile. Just for good luck I have also created a complimentary Test-StringProperty advanced function that tests whether a property name is present or not. This removes a lot of if ((Get-StringPropertyValue $SearchString “PropertyName”) -ne $null) { Do-Something } calls.

<#
.SYNOPSIS
   Test for a named property value in a string.
.DESCRIPTION
   Tests for the presence of a property value in a string and returns a boolean
   value. For example, if the specified SearchString contained "-property1
   -property2 <value2>”, searching for "-Property1" or "-Property2" would return
   $true, but searching for "-Property3" would return $false
.PARAMETER SearchString
   String to search for the specified property name.
.PARAMETER PropertyName
   The property name to search the SearchString for.
.EXAMPLE
   Test-StringProperty -SearchString $StringToSearch -PropertyName "-property1"

   This command searches the $StringToSearch variable for the presence of the property
   "-property1". If the property name is found it returns $true. If the property name
   is not found, it will return $false.
.NOTES
   Author - Iain Brighton - @iainbrighton, iain.brighton@virtualengine.co.uk
#>
function Test-StringProperty {

    [CmdletBinding(HelpUri = 'https://virtualengine.co.uk/2014/searching-for-string-properties-with-powershell/')]
    [OutputType([bool])]
    Param
    (
        [Parameter(Mandatory=$true, ValueFromPipeline=$true, ValueFromPipelineByPropertyName=$true, Position=0)]
        [ValidateNotNullOrEmpty()]
        [Alias("Search")]
        [string]
        $SearchString,

        [Parameter(Mandatory=$true, ValueFromPipelineByPropertyName=$true, Position=1)]
        [ValidateNotNullOrEmpty()]
        [Alias("Name","Property")]
        [string]
        $PropertyName
    )

    Begin { }

    Process {
        # Split the $SearchString based on one or more blank spaces
        $stringComponents = $SearchString.Split(' +',[StringSplitOptions]'RemoveEmptyEntries'); 
        for ($i = 0; $i -le $stringComponents.Length; $i++) {
            # The standard Powershell CompareTo method is case-sensitive
            if ([string]::Compare($stringComponents[$i], $PropertyName, $True) -eq 0) { return $true; }
        }
        # If nothing has been found or we're over the array boundary, return the default value
        return $false;
    }

    End { }
}

Some final words of warning:

  • If there are escaped (double) quotes then this function won’t work without some modification.
  • If the $SearchString just so happens to natively contain either “^” and/or “^^” it won’t (currently) work either.
  • If you have any improvements or feedback then please let me know.
  • Please test in your environment before putting any 3rd party or external code into production!

RES IT Store Web Portal Setup

We’ve had some confusion both internally and externally with some customers with the installation of the RES IT Store Web Portal. During the installation you will be prompted as to how you would like the IIS website to be configured:

image

This dialog is a little bit unclear as to what it is actually asking you. If you follow these simple guidelines, I’m sure you’ll be a whole lot clearer as to what is going on:

  • The RES IT Store Web Portal setup will create a new website – no ifs, buts or maybes.
    • It will not install to the default IIS website.
  • If you select the “Yes” option, the new website will be bound to port TCP port 80:
    • The existing default IIS website will also be bound to port 80 and therefore only one will start.
    • You will need to manually change the IIS default website port bindings.
  • If you select the “No” option, the new website can be bound to a specific TCP port:
    • Be careful as it will default to TCP port 80 but you can manually alter it.
    • The wizard will bind the Hostname specified to the new website as an IIS Host Header Name.

Our recommendation is to always select “No”, leave the default port binding and configure the host header value with your load-balanced fully qualified DNS alias, i.e. itstore.virtualengine.co.uk.

Easy!

Windows 8.1 Update KB 2919355 Error 0x8007003

It’s been long, too long since the last post. However, never fear as we have plenty of new updates in the pipeline (pun intended!). This post is a little off topic but one that might help one or two people with the latest Windows 8.1 Update 1.

Whilst installing the latest Windows 8.1 Update 1 (and also KB2894853) on a couple of machines I saw the following error:

Installation Failure: Windows failed to install the following update with error 0x8007003: Update for Windows (KB2919355)

A quick Google landed me here which mentions that you will receive this error if the profiles directory is redirected with a registry key. As we have small SSDs in the office machines, the profile directory is indeed redirected to a bigger, spinning disk. This article is a little confusing (hence the post) as it states:

After putting a copy of UserProfiles on the C: drive, installation finished normally.

To fix the installation error we need to ensure that the Default directory is present in the redirected folder location, not on the C: drive. A quick copy of C:\Users\Default to D:\Users\Default fixed the installation issues with both updates.

VET v1.4 Released!

Virtual Engine are pleased to announce the general availability of version 1.4 of the Virtual Engine Toolkit (VET). The latest Windows installer and documentation is available for download now on the Virtual Engine web site.

There’s no “What’s New” video this time but to view existing videos on the Virtual Engine Toolkit, please check out our YouTube channel.

App-V 5 Configuration Editor User Guide

ACEWe’ve been working hard getting the App-V 5 Configuration Editor (ACE) ready for official release; take a look at the ACE page for a bit more information about why it was developed.

The purpose of this short blog to guide you through the ACE interface. There is an assumption here you have an understanding of the App-V 5 Dynamic Configuration files and how they are used, if not you might want to take a look at this Technet article.

USER INTERFACE

Main Toolbar:

You will notice there are three main buttons in the tool bar as shown below:

Main Toolbar

image Opens an App-V XML file, i.e. a UserConfig.xml or DeploymentConfig.xml file. Once the file has been opened the contents will be parsed and displayed under the various tabs within the GUI.

image Saves the current App-V XML file, including any changes that have been made. You can give it a new name and Save As a different file, keeping your original one as is if necessary.

image Previews the changes that will be made to the App-V XML file before saving. This gives you the ability to check out the structure of the generated XML. It’s probably a good idea to point out here that you don’t need to preview the changes prior to performing a save.

Package Details:

This sections displays the Package Display Name, Package ID and Type of XML file opened, i.e. DeploymentConfig or UserConfig. Here is an example DeploymentConfig.xml opened below:

Package Details

MAIN CONFIGURATION TABS

Once an App-V 5 configuration XML file has been opened you can then begin to make changes as required using the tabs set out below.

User Configuration

Under the User Configuration tab you can change and view various options and configurations:

User Configuration

Options

Various global options change be changed here if you so desire, e.g. altering the COM integration mode.

Shortcuts

This tab allows you to View, Add, Edit or Delete any Shortcuts within the package.

If you want to delete an existing shortcut, simply select the row that contains the shortcut and press delete. Should you wish to add a new shortcut, I’d suggest you copy and paste an existing row and then edit the fields accordingly; we’ve added a context menu to make that task easy, should you not fancy using Ctrl+C and Ctrl+V  Smile.

Shortcuts

Scripts (User Context)

This is really where ACE starts to make life simple Smile. You can easily define which scripts you’d like to add and to which script actions, e.g. PublishPackage, UnpublishPackage, StartVirtualEnvironment, TerminateVirtualEnvironment, StartProcess and ExitProcess. There is no need to worry about getting the syntax in the XML file right. There are some excellent blogs out there talking about using scripts in App-V 5.0, so I suggest you take a look here at one from Tim Mangan and Microsoft’s own Steve Thompson if you need some further background information.

NOTE: You might have noticed that not all the script actions are available under this tab, that’s simply because those excluded aren’t permitted to run under the User Configuration section of the XML file.

I think most of the options are self explanatory but, it’s good to point out that leaving the Timeout value at 0 means no timeout period will be set, i.e. it will wait indefinitely for it to finish so use with caution.

Scripts (User Context)

Machine Configuration

Under the Machine Configuration tab you can alter global options, configure scripts and control the termination of processes.

NOTE: this tab will only be available when you open a DeploymentConfig.xml file. This is because machine configuration items cannot be set in the UserConfig.xml file.

Machine Configuration

Options

Here you’ll find any options that can be changed if you so desire.

Terminate Child Processes

You can define the path to an executable, that when closed, will terminate any child process running within the virtual environment.

Terminate Child Processes

Scripts (System Context)

Very much like the Scripts tab under User Configuration you can define which scripts you’d like to add to which machine script actions, e.g. AddPackage, RemovePackage, PublishPackage and UnpublishPackage.

NOTE: You might have noticed that not all the script actions are available under this tab, that’s simply because those excluded aren’t permitted to run under the Machine Configuration section of the XML file.

Scripts (System Context)

XML

You can view both the source (original) XML and/or preview the generated XML under this tab.

Source XML

Source XML

This is simply where you can view your source App-V XML file as it was when you opened it.

Generated XML

Once you click the Preview button image this pane will display any changes that will be made to the App-V XML file, giving you the ability to check out the structure of the XML before saving if you wish. NOTE: You don’t have to preview the changes prior to performing a save.

The example below (highlighted in yellow) shows the changes made by ACE in the generated XML format. NOTE: ACE will not highlight the changes in the XML, we’ve done it here for clarity purposes only.

Generated XML

With any luck this brief guide has given you a good overview of how to use ACE and hopefully you’ll agree its pretty intuitive to use and should make editing the App-V 5 Dynamic Configuration files a lot, lot easier (well we think so anyway!)? 🙂

DISCLAIMER: THE APP-V CONFIGURATION EDITOR IS FREE TO USE AT YOUR OWN RISK, WE CANNOT BE HELD RESPONSIBLE FOR ANY DAMAGE IT MIGHT CAUSE.