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);
    }
}

 

App-V 5 Configuration Editor User Guide

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

With any new application it’s great to have some user guides, right (RTFM)?!? Rest assured that will come when it’s officially released, in the mean time we wanted to create this short blog to guide you through the ACE interface. There is also 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:

SNAGHTML105760e

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:

SNAGHTML10a6c4e

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:

SNAGHTML1151fa5

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 all the defined Shortcuts within the package.

NOTE: at this time its Read Only but is great for getting an overview of all the Shortcuts available.

SNAGHTML123eb5d

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 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 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 Murgent 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.

SNAGHTML137e66e

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.

SNAGHTML144b2d1

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.

SNAGHTML14d302a

Scripts (System Context)

Very much like the Scripts tab under User Configuration you can define which scripts you’d like to add to which 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.

SNAGHTML1522226

XML

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

SNAGHTML18ecc7a

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.

SNAGHTML19082ed

Hopefully this brief guide has given you a good overview of how to use ACE. 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.

Documenting App-V 5.0 Packages

Continuing our series of posts on the Virtual Engine App-V 5.0 .APPV PowerShell CmdLets, this one will show you how to quickly document your .APPV packages’ contents. The Save-AppV5FileReport cmdlet generates a HTML report of information contained within the .APPV package contents. Here’s an example summary report (detailed reports are also available):

Generating Single Reports

Generating summary reports (such as the one above) simply requires running the following command:

[code]C:\PS> Save-AppV5FileReport –AppV C:\Mozilla_Firefox_v17.0\Mozilla_Firefox_v17.0.appv[/code]

This will generate a Mozilla_Firefox_V17.0_Report.html file in the source C:\Mozilla_Firefox_v17.0\ directory. If we wanted a detailed report instead we could run:

[code]C:\PS> Save-AppV5FileReport –AppV C:\Mozilla_Firefox_v17.0\Mozilla_Firefox_v17.0.appv –Detailed[/code]

Documenting Multiple Packages

Creating a report for a single package is fine, but what if we wanted to document all packages on a share or in a folder? Fortunately this is simple. If you wanted each package report in the source package directory, we can do so like this:

[code]C:\PS> Get-ChildItem C:\Packages\ -Include *.appv -Recurse | % { Save-AppV5FileReport -AppV $_.FullName }

Directory: C:\Packages\GoogleChrome_v23.0.1271.91

Mode                LastWriteTime     Length Name
—-                ————-     —— —-
-a—        02/05/2013     12:36       8884 GoogleChrome_v23.0.1271.91_Report.html

Directory: C:\Packages\Mozilla_Firefox_v17.0

Mode                LastWriteTime     Length Name
—-                ————-     —— —-
-a—        02/05/2013     12:36       8802 Mozilla_Firefox_v17.0_Report.html

Directory: C:\Packages\Paint.Net_v3.5.10

Mode                LastWriteTime     Length Name
—-                ————-     —— —-
-a—        02/05/2013     12:36       8770 Paint.NET_v3.5.10_Report.html[/code]

What about if we wanted all the reports in a single location? Again, pretty straight forward. The Save-AppV5FileReport cmdlet has a –FilePath parameter that we can specify the output location. Note: this folder is not automatically created so make sure it exists! Our command to achieve this is:

[code]C:\PS> Get-ChildItem C:\Packages\ -Include *.appv -Recurse | % { Save-AppV5FileReport -AppV $_.FullName -FilePath C:\Packages\Reports\ }

Directory: C:\Packages\Reports

Mode                LastWriteTime     Length Name
—-                ————-     —— —-
-a—        02/05/2013     12:40       8884 GoogleChrome_v23.0.1271.91_Report.html
-a—        02/05/2013     12:40       8802 Mozilla_Firefox_v17.0_Report.html
-a—        02/05/2013     12:40       8770 Paint.NET_v3.5.10_Report.html[/code]

Hopefully you’ll find these HTML reports useful. We are planning to add sequencer reports into this module in the near future too, so stay tuned! Happy PoShing 😀

Extracting files from an .APPV file with PowerShell

Whilst doing a lot of work with App-V 5.0 we have come across the requirement to look inside the .appv file with PowerShell. In our particular instance we’re after the package VersionId which is contained in the AppxManifest.xml file. As previously championed, we love automating and this should be easy!

The .appv file extension is a compressed archive and therefore, should be simple to crack open. After scouring the interweb, there is very little information on how to achieve this in code. We could use the built-in Shell32.dll functionality but this requires us to rename the file to .zip first. Ideally we want to avoid copying or renaming the source files. I did find one reference over on the Login Consultants forum which pointed me in the right direction.

Disclaimer: the following code requires the .Net Framework 4.5. The System.IO.Compression.FileSystem object is not available in previous releases. You can check in the C:\Windows\Microsoft.NET\assembly\GAC_MSIL\ folder and if you have the System.IO.Compression.FileSystem folder you should be good to go Smile with tongue out.

To get this new .Net functionality to work within PowerShell we will be calling the .Net assemblies directly and therefore need to create a couple of references. In our example we’ll be using both the System.IO.Compression and System.IO.Compression.FileSystem assemblies (two different DLLs hence the two references):

[code]### The System.IO.Compression.FileSystem requires at least .Net Framework 4.5
[System.Reflection.Assembly]::LoadWithPartialName(“System.IO.Compression”) | Out-Null;
[System.Reflection.Assembly]::LoadWithPartialName(“System.IO.Compression.FileSystem”) | Out-Null;[/code]

Next we can create our FileStream object (with read only access) required by the ZipArchive object class.

[code]### Open the ZipArchive with read access
$FileStream = New-Object System.IO.FileStream($SourceAppV5Archive, [System.IO.FileMode]::Open);
$AppV5Archive = New-Object System.IO.Compression.ZipArchive($FileStream);[/code]

In fact we can shorten this down to a single line:

[code]### Open the ZipArchive with read access
$AppV5Archive = New-Object System.IO.Compression.ZipArchive(New-Object System.IO.FileStream($SourceAppV, [System.IO.FileMode]::Open));[/code]

Once we have opened our .ZIP (.appv) file we can retrieve the AppXManifest.xml file entry:

[code]### Locate the AppxManifest.xml file
$AppxManifestEntry = $AppV5Archive.GetEntry(“AppxManifest.xml”);[/code]

Having the ZipArchiveEntry object we can extract it with the ExtractToFile method:

[code]### Extract the $ZipArchiveEntry
$ZipArchiveEntry.ExtractToFile($SaveAs);[/code]

Unfortunately this does work and reports the following error:

[code]Method invocation failed because [System.IO.Compression.ZipArchiveEntry] doesn’t contain a method named ‘ExtractToFile’.[/code]

Eh!? WT… Looking on the ZipArchiveEntry reference page on MSDN, the ExtractToFile is an Extension Method. Therefore, we need to utilise the underlying object method, the ZipFileExtensions.ExtractToFile method. For more information on Extension Methods in PowerShell see here and here. Now our code should look like this:

[code]### Extract the ZipArchiveEntry (ZipArchiveEntry.ExtractToFile is an extension method)
[System.IO.Compression.ZipFileExtensions]::ExtractToFile($AppxManifestEntry, $SaveAs, $Overwrite);[/code]

Finally we need to ensure that we correctly dispose of the ZipArchive object otherwise we’ll leave it open:

[code]### Ensure we close the file handle otherwise the file will be left open
$AppV5Archive.Dispose();[/code]

That’s it! It you want a simpler way of doing this, just download the Virtual Engine App-V 5.0 Package PowerShell CmdLets. You can achieve all this in just a single command:

[code]Save-AppV5FileXml -AppV c:\package.appv -XML AppxManifest[/code]

Full PowerShell Code Snippet

Here is the full code listing:

[code]### The System.IO.Compression.FileSystem requires at least .Net Framework 4.5
[System.Reflection.Assembly]::LoadWithPartialName(“System.IO.Compression”) | Out-Null;
[System.Reflection.Assembly]::LoadWithPartialName(“System.IO.Compression.FileSystem”) | Out-Null;

### Open the ZipArchive with read access
$AppV5Archive = New-Object System.IO.Compression.ZipArchive(New-Object System.IO.FileStream($SourceAppV5Archive, [System.IO.FileMode]::Open));

### Locate the AppxManifest.xml file
$AppxManifestEntry = $AppV5Archive.GetEntry(“AppxManifest.xml”);
### ZipArchiveEntry.ExtractToFile is an extension method
[System.IO.Compression.ZipFileExtensions]::ExtractToFile($AppxManifestEntry, $SaveAs, $true);

### Ensure we close the file handle otherwise the file will be left open
$AppV5Archive.Dispose();[/code]

Transferring Files to RES HyperDrive

As I’ve discussed previously, connecting the RES HyperDrive appliance via SSH is more involved than is typical for other Linux appliances. My assumption is that, as SSH is used by OS X clients and is exposed to the big bad world, it needs be secured. And tightly!

I have come across numerous times that I’ve needed to transfer files to or from the virtual appliance. This normally involves copying SSL certificates and keys and grabbing log files etc. Various people have asked me how they can achieve this so I thought I’d document the process. It’s fairly straight forward and assuming you have have your SSH private key and have downloaded WinSCP (or your SCP client of choice) you’re all set. WinSCP will transfer files over SSH and therefore, the process is almost identical to the earlier Remotely Administering RES HyperDrive post.

Note: If you have RES Automation Manager 2012 deployed then you can always transfer files to the appliance with the built-in Linux/Unix Resource Download task. If you don’t or want to know how to do this manually, feel free to continue..

After launching WinSCP you need to enter the connection information. Enter the hostname/FQDN, port number, username and private key as highlighted below (replace the hostname accordingly!). Make sure that you enter the username as hyperdrive and leave the password blank!:

image

When you connect by clicking the Login button you’ll be asked whether you trust the server’s key, so go ahead and do so. Once connected you should be able to transfer and drag ‘n drop files from left to right.

image

As we’re connecting as the hyperdrive user account we can only really copy files into the hyperdrive user’s home directory (/home/hyperdrive). After you’ve copied the files into the home directory you’ll need to move the files via the command line, i.e. via the console/SSH (don’t forget to change the owner and permissions as required!). Reading files is generally less of an issue, but you might need to relocate them into the /home/hyperdrive directory before you can copy them out; diagnostic or log files for example.

Good luck! Iain