Office 365 / 2013 and App-V – Exclude apps

With the latest release (June 5) of Office Deployment Tool there is the ability to exclude applications when creating a package. For example, if you don’t want to deploy – say Lync? – even though you are technically licensed for it.

How does it work?

Create your XML-file

The XML defines what product you want to deploy / create an App-V package for.

A reference can be found on Technet, with the entire list of all applications that can be excluded. Do note that each application you want to exclude is a new line within the XML-file

image

<Configuration>

<Add SourcePath="c:\media\" OfficeClientEdition="32" >
<Product ID="O365ProPlusRetail">
<Language ID="en-us" />
<ExcludeApp ID="Access" />
<ExcludeApp ID="InfoPath" />
<ExcludeApp ID="Lync" />
</Product>
</Add>
<Display Level="None" AcceptEULA="TRUE" />
<Property Name="AUTOACTIVATE" Value="1" />
</Configuration>

Run the command-line

Download source media;

 setup.exe /download c:\media\configuration.xml

Create the App-V package;

setup.exe /packager c:\media\configuration.xml c:\media\package

Now you have a package!

Just to deploy!

Remember, Office is only supported to be deployed as a global package when using App-V

 

Read more about this on Technet!

App-V 5 clean uninstall

Did you uninstall an App-V 5 client, and expected that it would cleanup after the mess it created?

Well, not all the time. After some interesting encounters – here is a small cleanup-list;

Remove the following folders;

C:\Program Files\Microsoft Application Virtualization

C:\ProgramData\App-V

C:\ProgramData\Microsoft\App-V

Remove the following registry keys;

HKLM\Software\Microsoft\AppV

Reinstall, and hopefully you are good to go!

AppsNotify – Ping the Application Catalog

One downside with Configuration Manager 2012 compared to Configuration Manager 2007 is that applications deployed to users, only made available, will not give a notification on the endpoint. This caused some initial confusion that needed a blog-post to clarify the matter – as all admins were used to a single view on the client what could be installed.

Microsoft created the Application Catalog, and for newer devices (mobile and Windows 8+) they made a secondary interface called the Company Portal (read about deployment at Justin Chalfants blog). However, none of these give a notification to the end user if an application is only made available.

Therefore I created a script in PowerShell that can just do that – ping the Application Catalog – check if there are new apps, and if so notify the user.

How does it work?

A scheduled task is setup

image

It starts the application 15 minutes after logon (to avoid excessive workload..), and then runs every 15 minutes.

After that, the workflow is something like;

Connect to Application Catalog

  • If no previous check is completed, gather a list.No notification is given.
  • If a previous check is completed, compare it.
    • No difference? Do nothing
    • New applications available? Notify the user
      • Maintain a notification in the system tray (which directs the user to the Application Catalog if clicked)
    • Applications removed? Update the list, no notification to the end-user

 

For each check there is a log file within the users %TEMP% called appsnotify app.log. Sample output;

image

How does it look?

Like this;

appsnotify

 

How do I deploy it?

MSI-file to install it

To install it use the MSI file with the property APPCATALOG. Input should be;

APPCATALOG=http://localhost/CMApplicationCatalog

(no slash at the end)

The Application Catalog needs to be prepared for client interaction which is described by Microsoft in a blog-article. See the Getting Started section of Extending the Application Catalog in System Center 2012 Configuration Manager.

How do I make it my own?

In all Community spirit, here comes the code / files.

XML-file for creating the scheduled task – incase you want to build your own

<?xml version="1.0" encoding="UTF-16"?>
<Task version="1.2" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">
  <RegistrationInfo>
    <Author>PRECISION\Nicke</Author>
  </RegistrationInfo>
  <Triggers>
    <LogonTrigger>
      <Repetition>
        <Interval>PT15M</Interval>
        <StopAtDurationEnd>false</StopAtDurationEnd>
      </Repetition>
      <StartBoundary>1899-12-30T06:04:14</StartBoundary>
      <Enabled>true</Enabled>
      <Delay>PT15M</Delay>
    </LogonTrigger>
  </Triggers>
  <Principals>
    <Principal id="Author">
      <GroupId>S-1-5-32-545</GroupId>
      <RunLevel>LeastPrivilege</RunLevel>
    </Principal>
  </Principals>
  <Settings>
    <MultipleInstancesPolicy>IgnoreNew</MultipleInstancesPolicy>
    <DisallowStartIfOnBatteries>false</DisallowStartIfOnBatteries>
    <StopIfGoingOnBatteries>false</StopIfGoingOnBatteries>
    <AllowHardTerminate>false</AllowHardTerminate>
    <StartWhenAvailable>true</StartWhenAvailable>
    <RunOnlyIfNetworkAvailable>true</RunOnlyIfNetworkAvailable>
    <IdleSettings>
      <StopOnIdleEnd>true</StopOnIdleEnd>
      <RestartOnIdle>false</RestartOnIdle>
    </IdleSettings>
    <AllowStartOnDemand>true</AllowStartOnDemand>
    <Enabled>true</Enabled>
    <Hidden>true</Hidden>
    <RunOnlyIfIdle>false</RunOnlyIfIdle>
    <WakeToRun>false</WakeToRun>
    <ExecutionTimeLimit>PT0S</ExecutionTimeLimit>
    <Priority>7</Priority>
  </Settings>
  <Actions Context="Author">
    <Exec>
      <Command>C:\Program Files (x86)\Common Files\AppsNotify\AppsNotify 2.0.exe</Command>
      <Arguments>-appcatalog http://www.sample.com</Arguments>
    </Exec>
  </Actions>
</Task>

Source code for the PowerShell script. Wrapped it into a .exe with PowerShell Studio 2012. I have only tested this on Windows 7 x64 with PowerShell 3.0 / 2.0. Log-functions are from 9to5it

#========================================================================
# Created on: 2014-06-10
# Created by: Nicke Källén
# Organization:
# Filename: AppsNotify 2.0.pff
#========================================================================

$AppNotify.FormBorderStyle = 'FixedToolWindow'
Function Log-Start{
 <#
 .SYNOPSIS
 Creates log file

 .DESCRIPTION
 Creates log file with path and name that is passed. Checks if log file exists, and if it does deletes it and creates a new one.
 Once created, writes initial logging data

 .PARAMETER LogPath
 Mandatory. Path of where log is to be created. Example: C:\Windows\Temp

 .PARAMETER LogName
 Mandatory. Name of log file to be created. Example: Test_Script.log

 .PARAMETER ScriptVersion
 Mandatory. Version of the running script which will be written in the log. Example: 1.5

 .INPUTS
 Parameters above

 .OUTPUTS
 Log file created

 .NOTES
 Version: 1.0
 Author: Luca Sturlese
 Creation Date: 10/05/12
 Purpose/Change: Initial function development

 Version: 1.1
 Author: Luca Sturlese
 Creation Date: 19/05/12
 Purpose/Change: Added debug mode support

 .EXAMPLE
 Log-Start -LogPath "C:\Windows\Temp" -LogName "Test_Script.log" -ScriptVersion "1.5"
 #>

 [CmdletBinding()]

 Param ([Parameter(Mandatory=$true)][string]$LogPath, [Parameter(Mandatory=$true)][string]$LogName, [Parameter(Mandatory=$true)][string]$ScriptVersion)

 Process{
 $sFullPath = $LogPath + "\" + $LogName

 #Check if file exists and delete if it does
 If((Test-Path -Path $sFullPath)){
 Remove-Item -Path $sFullPath -Force
 }

 #Create file and start logging
 New-Item -Path $LogPath -Name $LogName –ItemType File

 Add-Content -Path $sFullPath -Value "***************************************************************************************************"
 Add-Content -Path $sFullPath -Value "Started processing at [$([DateTime]::Now)]."
 Add-Content -Path $sFullPath -Value "***************************************************************************************************"
 Add-Content -Path $sFullPath -Value ""
 Add-Content -Path $sFullPath -Value "Running script version [$ScriptVersion]."
 Add-Content -Path $sFullPath -Value ""
 Add-Content -Path $sFullPath -Value "***************************************************************************************************"
 Add-Content -Path $sFullPath -Value ""

 #Write to screen for debug mode
 Write-Debug "***************************************************************************************************"
 Write-Debug "Started processing at [$([DateTime]::Now)]."
 Write-Debug "***************************************************************************************************"
 Write-Debug ""
 Write-Debug "Running script version [$ScriptVersion]."
 Write-Debug ""
 Write-Debug "***************************************************************************************************"
 Write-Debug ""

 }
}

Function Log-Write{
 <#
 .SYNOPSIS
 Writes to a log file

 .DESCRIPTION
 Appends a new line to the end of the specified log file

 .PARAMETER LogPath
 Mandatory. Full path of the log file you want to write to. Example: C:\Windows\Temp\Test_Script.log

 .PARAMETER LineValue
 Mandatory. The string that you want to write to the log

 .INPUTS
 Parameters above

 .OUTPUTS
 None

 .NOTES
 Version: 1.0
 Author: Luca Sturlese
 Creation Date: 10/05/12
 Purpose/Change: Initial function development

 Version: 1.1
 Author: Luca Sturlese
 Creation Date: 19/05/12
 Purpose/Change: Added debug mode support

 .EXAMPLE
 Log-Write -LogPath "C:\Windows\Temp\Test_Script.log" -LineValue "This is a new line which I am appending to the end of the log file."
 #>

 [CmdletBinding()]

 Param ([Parameter(Mandatory=$true)][string]$LogPath, [Parameter(Mandatory=$true)][string]$LineValue)

 Process{
 Add-Content -Path $LogPath -Value $LineValue

 #Write to screen for debug mode
 Write-Debug $LineValue
 }
}

Function Log-Error{
 <#
 .SYNOPSIS
 Writes an error to a log file

 .DESCRIPTION
 Writes the passed error to a new line at the end of the specified log file

 .PARAMETER LogPath
 Mandatory. Full path of the log file you want to write to. Example: C:\Windows\Temp\Test_Script.log

 .PARAMETER ErrorDesc
 Mandatory. The description of the error you want to pass (use $_.Exception)

 .PARAMETER ExitGracefully
 Mandatory. Boolean. If set to True, runs Log-Finish and then exits script

 .INPUTS
 Parameters above

 .OUTPUTS
 None

 .NOTES
 Version: 1.0
 Author: Luca Sturlese
 Creation Date: 10/05/12
 Purpose/Change: Initial function development

 Version: 1.1
 Author: Luca Sturlese
 Creation Date: 19/05/12
 Purpose/Change: Added debug mode support. Added -ExitGracefully parameter functionality

 .EXAMPLE
 Log-Error -LogPath "C:\Windows\Temp\Test_Script.log" -ErrorDesc $_.Exception -ExitGracefully $True
 #>

 [CmdletBinding()]

 Param ([Parameter(Mandatory=$true)][string]$LogPath, [Parameter(Mandatory=$true)][string]$ErrorDesc, [Parameter(Mandatory=$true)][boolean]$ExitGracefully)

 Process{
 Add-Content -Path $LogPath -Value "Error: An error has occurred [$ErrorDesc]."

 #Write to screen for debug mode
 Write-Debug "Error: An error has occurred [$ErrorDesc]."

 #If $ExitGracefully = True then run Log-Finish and exit script
 If ($ExitGracefully -eq $True){
 Log-Finish -LogPath $LogPath
 Break
 }
 }
}

Function Log-Finish{
 <#
 .SYNOPSIS
 Write closing logging data & exit

 .DESCRIPTION
 Writes finishing logging data to specified log and then exits the calling script

 .PARAMETER LogPath
 Mandatory. Full path of the log file you want to write finishing data to. Example: C:\Windows\Temp\Test_Script.log

 .PARAMETER NoExit
 Optional. If this is set to True, then the function will not exit the calling script, so that further execution can occur

 .INPUTS
 Parameters above

 .OUTPUTS
 None

 .NOTES
 Version: 1.0
 Author: Luca Sturlese
 Creation Date: 10/05/12
 Purpose/Change: Initial function development

 Version: 1.1
 Author: Luca Sturlese
 Creation Date: 19/05/12
 Purpose/Change: Added debug mode support

 Version: 1.2
 Author: Luca Sturlese
 Creation Date: 01/08/12
 Purpose/Change: Added option to not exit calling script if required (via optional parameter)

 .EXAMPLE
 Log-Finish -LogPath "C:\Windows\Temp\Test_Script.log"

 .EXAMPLE
 Log-Finish -LogPath "C:\Windows\Temp\Test_Script.log" -NoExit $True
 #>

 [CmdletBinding()]

 Param ([Parameter(Mandatory=$true)][string]$LogPath, [Parameter(Mandatory=$false)][string]$NoExit)

 Process{
 Add-Content -Path $LogPath -Value ""
 Add-Content -Path $LogPath -Value "***************************************************************************************************"
 Add-Content -Path $LogPath -Value "Finished processing at [$([DateTime]::Now)]."
 Add-Content -Path $LogPath -Value "***************************************************************************************************"

 #Write to screen for debug mode
 Write-Debug ""
 Write-Debug "***************************************************************************************************"
 Write-Debug "Finished processing at [$([DateTime]::Now)]."
 Write-Debug "***************************************************************************************************"

 #Exit calling script if NoExit has not been specified or is set to False
 If(!($NoExit) -or ($NoExit -eq $False)){
 Exit
 }

 }
}

function Get-ScriptDirectory
{
if($hostinvocation -ne $null)
{
Split-Path $hostinvocation.MyCommand.path
}
else
{
Split-Path $script:MyInvocation.MyCommand.Path
}
} 

function Parse-Commandline
{
<#
 .SYNOPSIS
 Parses the Commandline of a package executable

 .DESCRIPTION
 Parses the Commandline of a package executable

 .PARAMETER Commandline
 The Commandline of the package executable

 .EXAMPLE
 $arguments = Parse-Commandline -Commandline $Commandline

 .INPUTS
 System.String

 .OUTPUTS
 System.Collections.Specialized.StringCollection
#>

 [OutputType([System.Collections.Specialized.StringCollection])]
 Param([string]$CommandLine) 

 $Arguments = New-Object System.Collections.Specialized.StringCollection 

 if($CommandLine)
 {
 #Find First Quote
 $index = $CommandLine.IndexOf('"') 

 while ( $index -ne -1)
 {#Continue as along as we find a quote
 #Find Closing Quote
 $closeIndex = $CommandLine.IndexOf('"',$index + 1)
 if($closeIndex -eq -1)
 {
 break #Can’t find a match
 }
 $value = $CommandLine.Substring($index + 1,$closeIndex – ($index + 1))
 [void]$Arguments.Add($value)
 $index = $closeIndex 

 #Find First Quote
 $index = $CommandLine.IndexOf('"',$index + 1)
 }
 }
 return $Arguments
}

function Convert-CommandLineToDictionary
{
 <#
 .SYNOPSIS
 Parses and converts the commandline of a packaged executable into a Dictionary

 .DESCRIPTION
 Parses and converts the commandline of a packaged executable into a Dictionary

 .PARAMETER Dictionary
 The Dictionary to load the value pairs into.

 .PARAMETER CommandLine
 The commandline of the package executable

 .PARAMETER ParamIndicator
 The character used to indicate what is a parameter.

 .EXAMPLE
 $Dictionary = New-Object System.Collections.Specialized.StringDictionary
 Convert-CommandLineToDictionary -Dictionary $Dictionary -CommandLine $Commandline -ParamIndicator '-'
 #>
 Param( [ValidateNotNull()]
 [System.Collections.Specialized.StringDictionary]$Dictionary,
 [string]$CommandLine,
 [char] $ParamIndicator) 

 $Params = Parse-Commandline $CommandLine

 for($index = 0; $index -lt $Params.Count; $index++)
 {
 [string]$param = $Params[$index]
 #Clear the values
 $key = ""
 $value = "" 

 if($param.StartsWith($ParamIndicator))
 {
 #Remove the indicator
 $key = $param.Remove(0,1)
 if($index + 1 -lt $Params.Count)
 {
 #Check if the next Argument is a parameter
 [string]$param = $Params[$index + 1]
 if($param.StartsWith($ParamIndicator) -ne $true )
 {
 #If it isn’t a parameter then set it as the value
 $value = $param
 $index++
 }
 }
 $Dictionary[$key] = $value
 }#else skip
 }
}

function Validate-IsURL
{
 <#
 .SYNOPSIS
 Validates if input is an URL

 .DESCRIPTION
 Validates if input is an URL

 .PARAMETER Url
 A string containing an URL address

 .INPUTS
 System.String

 .OUTPUTS
 System.Boolean
 #>
 [OutputType([Boolean])]
 param ([string]$Url)

 if($Url -eq $null)
 {
 return $false
 }

 return $Url -match "^(ht|f)tp(s?)\:\/\/[0-9a-zA-Z]([-.\w]*[0-9a-zA-Z])*(:(0-9)*)*(\/?)([a-zA-Z0-9\-\.\?\,\'\/\\\+&amp;%\$#_]*)?$"
}

function Get-CMUserApps {
 [CmdletBinding()]
 param
 (
 [Parameter(Mandatory=$True,
 ValueFromPipelineByPropertyName=$True,
 HelpMessage='URL for Application Catalogue')]
 $url,
 [Parameter(Mandatory=$True,
 ValueFromPipelineByPropertyName=$True,
 HelpMessage='Path to logfile')]
 $logfile,
 [Parameter(Mandatory=$True,
 ValueFromPipelineByPropertyName=$True,
 HelpMessage='Temp-file')]
 $temp
 )
 Begin {
 log-write -LogPath $logfile -LineValue "Create web service proxy"
 $catalogurl = $url;
 Log-Write -LogPath $logfile -LineValue "Connecting to $catalogurl"
 try {
 $url = $catalogurl+"/ApplicationViewService.asmx?WSDL";
 $service = New-WebServiceProxy $url -UseDefaultCredential;

 }
 catch {
 Log-Error -LogPath $logfile -ErrorDesc "AppCatalog no response" -ExitGraceFully $false
 Log-Finish -LogPath $logfilePath -NoExit $true
 break
 }
 }
 Process {

 $total = 0;
 try {
 Log-Write -LogPath $logfile -LineValue "Gathering applications"
 $service.GetApplications("Name",$null,"Name","",100,0,$true,"PackageProgramName",$false,$null,[ref]$total) | select ApplicationId,Name | Export-Clixml $temp
 return $true
 }

 catch {
 Log-Error -LogPath $logfile -ErrorDesc $error[0] -ExitGraceFully $false
 return $false
 }
 Remove-Variable -Name url
 Remove-Variable -Name total
 $service.dispose()

 }

}

function Compare-CMUserApps {
 [CmdletBinding()]
 param
 (
 [Parameter(Mandatory=$True,
 ValueFromPipelineByPropertyName=$True,
 HelpMessage='Permanent-file')]
 $file,
 [Parameter(Mandatory=$True,
 ValueFromPipelineByPropertyName=$True,
 HelpMessage='Temp-file')]
 $temp,
 [Parameter(Mandatory=$True,
 ValueFromPipelineByPropertyName=$True,
 HelpMessage='Path to logfile')]
 $logfile
 )
Process {
 Log-Write -LogPath $logfile -LineValue "Comparing applications lists"
 If (-Not (Test-Path $file)) {
 Log-Write -LogPath $logfile -LineValue "No previous version of apps list"
 try {
 Rename-Item $temp "$prefix apps.xml"
 }
 catch {
 Remove-Item $temp
 Log-Error -LogPath $logfile -ErrorDesc "Unable to create initial list" -ExitGracefully $false
 }

 }
 Else {

 Log-Write -LogPath $logfile -LineValue "Starting check......"
 # $diffs = (Compare-Object -ReferenceObject $(Get-Content $file) -DifferenceObject $(Get-Content $temp)) | Where {$_.SideIndicator -eq '<='}
 # $diffsserver = (Compare-Object -ReferenceObject $(Get-Content $file) -DifferenceObject $(Get-Content $temp)) | Where {$_.SideIndicator -eq '=>'}

 If ((Compare-Object -ReferenceObject $(Get-Content $file -ReadCount 0) -DifferenceObject $(Get-Content $temp -ReadCount 0)) -eq $null) {
 Log-Write -LogPath $logfile -LineValue "No new applications"
 Log-Write -LogPath $logfile -LineValue "Removing temporary file"

 try {
 Remove-Item $temp
 }
 catch {

 Log-Error -LogPath $logfile -ErrorDesc "Unable to remove temp list" -ExitGracefully $false
 }

 }
 Elseif (((Compare-Object -ReferenceObject $(Get-Content $file -ReadCount 0) -DifferenceObject $(Get-Content $temp -ReadCount 0)) | Where {$_.SideIndicator -eq '<='}) -ne $null -and ((Compare-Object -ReferenceObject $(Get-Content $file -ReadCount 0) -DifferenceObject $(Get-Content $temp -ReadCount 0)) | Where {$_.SideIndicator -eq '=>'}) -eq $null ) {
 Log-Write -LogPath $logfile -LineValue "Less applications received"
 try {
 Log-Write -LogPath $logfile -LineValue "Remove permanent list"
 Remove-Item $file
 }
 catch {
 Remove-Item $temp
 Log-Error -LogPath $logfile -ErrorDesc "Unable to remove permanent list" -ExitGracefully $false
 }

 try {
 Log-Write -LogPath $logfile -LineValue "Rename temporary list"
 Rename-Item $temp "$prefix apps.xml"
 }
 catch {
 Log-Error -LogPath $logfile -ErrorDesc "Unable to switch temp-list to permanent" -ExitGracefully $false
 }

 }

 Else {
 Log-Write -LogPath $logfile -LineValue "New applications found"

 $newapps = $true

 }

 }
 If ($newapps -eq $true) {
 return $True

 }

 }

}

function OnApplicationLoad {
 #Note: This function is not called in Projects
 #Note: This function runs before the form is created
 #Note: To get the script directory in the Packager use: Split-Path $hostinvocation.MyCommand.path
 #Note: To get the console output in the Packager (Windows Mode) use: $ConsoleOutput (Type: System.Collections.ArrayList)
 #Important: Form controls cannot be accessed in this function
 #TODO: Add snapins and custom code to validate the application load

 return $true #return true for success or false for failure
}

function OnApplicationExit {
 #Note: This function is not called in Projects
 #Note: This function runs after the form is closed
 #TODO: Add custom code to clean up and unload snapins when the application exits
 #Log-Finish -LogPath $logfilePath -NoExit $true
 $script:ExitCode = 0 #Set the exit code for the Packager
 Log-Finish -LogPath $logfilePath -NoExit $false
 break
}

$AppNotify_Load={
 #TODO: Initialize Form Controls here
 $NotifyIcon.ShowBalloonTip(30000,"New Application","You have new applications available", 'Info')
}

#region Control Helper Functions
function Show-NotifyIcon
{
<#
 .SYNOPSIS
 Displays a NotifyIcon's balloon tip message in the taskbar's notification area.

 .DESCRIPTION
 Displays a NotifyIcon's a balloon tip message in the taskbar's notification area.

 .PARAMETER NotifyIcon
 The NotifyIcon control that will be displayed.

 .PARAMETER BalloonTipText
 Sets the text to display in the balloon tip.

 .PARAMETER BalloonTipTitle
 Sets the Title to display in the balloon tip.

 .PARAMETER BalloonTipIcon
 The icon to display in the ballon tip.

 .PARAMETER Timeout
 The time the ToolTip Balloon will remain visible in milliseconds.
 Default: 0 - Uses windows default.
#>
 param(
 [Parameter(Mandatory = $true, Position = 0)]
 [ValidateNotNull()]
 [System.Windows.Forms.NotifyIcon]$NotifyIcon,
 [Parameter(Mandatory = $true, Position = 1)]
 [ValidateNotNullOrEmpty()]
 [String]$BalloonTipText,
 [Parameter(Position = 2)]
 [String]$BalloonTipTitle = '',
 [Parameter(Position = 3)]
 [System.Windows.Forms.ToolTipIcon]$BalloonTipIcon = 'None',
 [Parameter(Position = 4)]
 [int]$Timeout = 0
 )

 if($NotifyIcon.Icon -eq $null)
 {
 #Set a Default Icon otherwise the balloon will not show
 $NotifyIcon.Icon = [System.Drawing.Icon]::ExtractAssociatedIcon([System.Windows.Forms.Application]::ExecutablePath)
 }

 $NotifyIcon.ShowBalloonTip($Timeout, $BalloonTipTitle, $BalloonTipText, $BalloonTipIcon)
}

#endregion

$NotifyIcon_MouseDoubleClick=[System.Windows.Forms.MouseEventHandler]{
#Event Argument: $_ = [System.Windows.Forms.MouseEventArgs]
 #TODO: Place custom script here
 Log-Write -LogPath $logfilePath -LineValue "User clicked icon"
 Log-Write -LogPath $logfilePath -LineValue "Sending user to $appcatalog"
 Start-Process $appcatalog
 $NotifyIcon.Visible = $false

 try {
 Log-Write -LogPath $logfilePath -LineValue "Removing $filepath"
 Remove-Item $filePath
 Log-Write -LogPath $logfilePath -LineValue "Renaming $tempfilePath"
 Rename-Item -Path "$tempfilePath" -NewName "$prefix apps.xml" -Force

 }
 catch {
 Remove-Item $tempfilePath
 Log-Error -LogPath $logfile -ErrorDesc "Unable to remove permanent list" -ExitGracefully $false
 }
 $AppNotify.Close()
 $NotifyIcon.Dispose()
 #Log-Finish -LogPath $logfilePath -NoExit $false
 #$AppNotify.Close()
 #$timer1.Start()

}

$NotifyIcon_MouseClick=[System.Windows.Forms.MouseEventHandler]{
#Event Argument: $_ = [System.Windows.Forms.MouseEventArgs]
 $NotifyIcon.Visible = $true
 $NotifyIcon.ShowBalloonTip(30000,"New Application","You have new applications available", 'Info')
}

$NotifyIcon_BalloonTipClicked={
 Log-Write -LogPath $logfilePath -LineValue "User clicked ballontip"
 Log-Write -LogPath $logfilePath -LineValue "Sending user to $appcatalog"
 Start-Process $appcatalog
 $NotifyIcon.Visible = $false

 try {
 Log-Write -LogPath $logfilePath -LineValue "Removing $filepath"
 Remove-Item $filePath
 Log-Write -LogPath $logfilePath -LineValue "Renaming $tempfilePath"
 Rename-Item -Path "$tempfilePath" -NewName "$prefix apps.xml" -Force

 }
 catch {
 Remove-Item $tempfilePath
 Log-Error -LogPath $logfile -ErrorDesc "Unable to remove permanent list" -ExitGracefully $false
 }
 $AppNotify.Close()
 $NotifyIcon.Dispose()
 #Log-Finish -LogPath $logfilePath -NoExit $true
 #exit
 #$timer1.Start()
}

#Get path which scripts run from
$CurrentPath = Get-ScriptDirectory

#Prefix for all generated files in user's %TEMP%
$prefix = "appsnotify"

#Logfile
$logfilePath = $env:temp+"\$prefix app.log"

$check=Get-Process AppsNotify -ErrorAction SilentlyContinue | Measure-Object

if ($check.count -lt "2") {

}
else {
 Log-Error -LogPath $logfilePath -ErrorDesc "AppsNotify is already running. Terminating. " -ExitGraceFully $false
 exit
}

#Temporary file to store applications
$tempfilePath = $env:temp+"\$prefix app_temp.xml"
#Permanent file to store applications
$filePath = $env:temp +"\$prefix apps.xml"
#Reset log-file for this session
Remove-Item $logfilePath

################################################################################################################
Log-Start -LogPath $env:temp -LogName "$prefix app.log" -ScriptVersion "2.0"

#Verify that the $CommandLine variable exists
if($CommandLine -ne $null -and $CommandLine -ne "")
{
 #Log-Write -LogPath $logfilePath -LineValue "There is a command-line"
 Log-Write -LogPath $logfilePath -LineValue "Command-line is:"
 Log-Write -LogPath $logfilePath -LineValue "$CommandLine"
 #$Arguments = Parse-Commandline $CommandLine
 #Convert the Arguments. Use – as the Argument Indicator
 $Dictionary = New-Object System.Collections.Specialized.StringDictionary
 Convert-CommandLineToDictionary -Dictionary $Dictionary -CommandLine $Commandline -ParamIndicator '-'
}
else
{
 #Not running in a packager or no command line arguments passed
 Log-Error -LogPath $logfilePath -ErrorDesc "No command-line argument. Use -appcatalog <url>" -ExitGraceFully $false
 Log-Finish -LogPath $logfilePath -NoExit $false
 break
}

$appcatalog = $Dictionary["appcatalog"]
if($appcatalog -ne $null -and $appcatalog -ne "") {
 Log-Write -LogPath $logfilePath -LineValue "Passed Application Catalog is $appcatalog"
 if (Validate-IsURL -Url $appcatalog) {
 Log-Write -LogPath $logfilePath -LineValue "Passed Application Catalog is a URL"
 }
 Else {
 Log-Error -LogPath $logfilePath -ErrorDesc "This is not a url" -ExitGraceFully $false
 Log-Finish -LogPath $logfilePath -NoExit $false
 break
 }

}
else {
 #Address to Application Catalogue
 Log-Error -LogPath $logfilePath -ErrorDesc "We need an Application Catalog" -ExitGraceFully $false
 Log-Finish -LogPath $logfilePath -NoExit $false
 break
}

if ((Get-CMUserApps -url $appcatalog -logfile $logfilePath -temp $tempfilePath) -eq $true) {

 if ((Compare-CMUserApps -file $filePath -temp $tempfilePath -logfile $logfilePath) -eq $true) {

 try {
 $NotifyIcon.Visible = $true
 }
 catch {
 Log-Write -LogPath $logfilePath -LineValue "Exception"
 }
 $NotifyIcon.ShowBalloonTip(30000,"New Application","You have new applications available", 'Info')

 }
 Else {
 Log-Finish -LogPath $logfilePath -NoExit $false
 break
 }

}
Else {
 Log-Finish -LogPath $logfilePath -NoExit $false
 break
}

$NotifyIcon_BalloonTipShown={
 #TODO: Place custom script here
 Log-Write -LogPath $logfilePath -LineValue "Notifying user"
}

} 

Adobe PDF Addon download

Previously I discussed the deployment of Adobe PDF Addon with a virtualized instance of Adobe Acrobat. The Adobe PDF Addon is also known as the Adobe PDF Printer or the Adobe Distiller. In the end – its a piece of software that contains a driver and therefore can not be virtualized.

Extracting this from a generic piece of Adobe Acrobat media is rather painful, if at all possible, however the Adobe Distiller (aka Adobe PDF Addon) is available as a standalone installer.

How would one retrieve this standalone installer?

Well, by an odd-chance I bypassed the Creative Cloud Packager and downloaded the Adobe FrameMaker 12 from the Adobe Licensing Website. Hidden within these source-files there is a folder named;

AdobePDFCreationAddOn11_x86_x64

image

There are a few things needed to silently install this msi (distillr.msi).

Visual C++ 2010 SP1 (x64) is a prerequisite for the application.

There is a check by the installer to ensure that it is not installed standalone. Within the InstallExecuteSequence table the following CustomAction-reference needs to be removed;

image

With the above in place – you are all set togo!

SMART Notebook 11.4 and App-V 5.0

After some discussion during AppV User Group in Amsterdam about sequencing SMART Notebook I decided to post some steps and topics that could show up when sequencing SMART Notebook with App-V 5.0 SP2.

Pre-sequencing

Download the web-installer, the drivers and any potential galleries you would like to use from SMARTs homepage. Drivers have to be deployed seperately as they can not be virtualized, and therefore it might also be a good idea to install them as a prerequisite on the sequencer.

Regardless if you have been using App-V 4.6 or App-V 5.0, the architecture of your operating system is important. Notebook has some hard-coded paths, and therefore my recommendation would be to create a package for each type of architecture. In this post, Windows 7 x64 will be the one used – both for my sequencer and my client.

Sequencing

Apart from performing an ordinary installation the following choices were made;

PVAD was set to c:\dummy

Installation was set to the default folder suggested by the installer.

I did not start any application during sequencing.

To disable the automatic check for updates everyday the following needs to be set.

1. Set the following registry key.


Windows Registry Editor Version 5.00

[HKEY_LOCAL_MACHINE\SOFTWARE\Wow6432Node\SMART Technologies\Product Update]
"CheckUpdates"=dword:00000000 

2. Remove the shortcut to SMART Product Update

Post-sequencing

Enable interaction with the local system using the Advanced-tab in Editing mode.

image

SMART Notebook does come with a few drivers, and these drivers can not be virtualized. To leverage them you would need to extract them and deploy them natively. Here comes methods which you can use to extract and deploy the drivers;

Drivers downloaded

To gain full functionality you would need to extract the drivers. The web-installer will download everything for you into the following folder, however it will remove all the contents once it is completed the installation.

%TEMP%\SMARTInstallWrapper\Wrapper_11.4.520.1\

Primarily the following contents are important if you want to deploy the drivers;

SMART Common Files, SMART Product Drivers

They are downloaded with easy to deploy MSI-files and corresponding language files, so to add them as a seperate pre-requisites shouldn’t be to complicated.

Drivers installed

In addition to the above drivers, there are additional drivers installed – which the sequencer will warn us about. image

Look within the installation of SMART and find the following folder;


C:\Program Files (x86)\SMART Technologies\Education Software\Printer Drivers\

Extract the folder, and save a copy. To install the driver, you would need to use the following command-line;

"C:\driver\XPSPrintCapturex64.exe" -i -p "C:\driver\\"

C:\driver would contain the entire contents of Printer Drivers, looking something like this;

image

We can find the installation-routine by checking the CustomAction table from the downloaded MSI-installation of SMART Notebook;

image

Using the same method the uninstall command can also be found;

image

Summary

You should now have the ability to gather Everything you need to virtualize SMART Notebook. Depending on your infrastructure, all of this can be wrapped into multiple installers, scripts or similiar things to easily deploy to your endpoints.

Adobe Creative Cloud and integration (and the challenge with AppV)

Adobe Creative Cloud is the latest way that Adobe will now offer software on a subscription model (or software as a service, or the cloud, or… well whats the new buzzword?). This allows customers to pick and select what software they want to use, download the media and then install the software they desire.

The installers are quite hefty and if you download almost all software within a suite you end up with a big-bundle of software in a rock-solid 16gb installation package. Even splitting up everything into an installation package per software (such as Photoshop..) the installation itself usually weigh in at 1-2gb.

Usually the software are standalone applications, however two pieces within the suite offer integration across applications. Adobe Bridge and Extendscript Toolkit seem to integrate into just about any piece of software.

Lets run through a few examples;

Any software allows the immediate jump to Adobe Bridge;

image

Adobe Bridge has the ability to execute startup scripts and each software installed will provide their own set;

image

Extendscript Toolkit will list all software (and the objects they are using);

image

It seems that the ability to integrate between these different pieces of software is a mixture of feature enablement and simple listing of folders.

The startup-scripts for Adobe Bridge for example seem to directly correlate to the amount of folders listed within the Startupscripts CC folder;

image

The ability to directly see different pieces of software through Extendscript Toolkit and the ability to invoke Adobe Bridge doesn’t seem to be related to the enumeration of any folder.

After reviewing procmon activity for quite a while there seems to only be single processing of a file that I didn’t fully explain. Now, this seems “easy to identify”, right? Keep in mind that I stated that there is _one_ file which is processed during startup that I didn’t fully explained. This means; I have taken the time to understand every single file which Bridge reads, or attempts to read during startup. I also processed captures when starting Bridge from any other suite, or when Extendscript Toolkit started.

image

Yes, figuring this out took me about 3 days of just reviewing and explaning every single line that Process Monitor gave me during a few use cases. Process Monitor doesn’t tell you whats wrong – it tells you what happens.

 

image

Once realising that this file was unexplained it is time to understand it a bit more…

The PCD.DB file, which is an unknown extension, was processed during each startup. If opening the file with notepad the following showed up;

image

SQLite Format 3 seems to relate to a database and after a quick search a utility to browse the database was found -  named SQLite Database Browser.

Cracking the PCD.DB file with SQLite DB Browser immediately showed this;

image

Reviewing the data using Browse Data tab the contents could be immediately spotted;

image

 

Why is this relevant? It seems that the integration between different pieces of the Adobe-suite has a check against this database if the component that it wants to integrate with is actually installed. As everything is contained into a single file it becomes a nightmare to manage in – lets say… – a Connection Group within App-V. In essence it means that the last file that gets read is the one that sets the stage for all Adobe-software. For example:

You create a package with Adobe Photoshop.

You create a new package with Adobe Bridge

If the Photoshop-package is the last one to load, the db-file will not contain any information about Adobe Bridge (as it is effectively overwritten by the one created in the Photoshop-package).

How do you handle it? You start with the worst case scenario!

Generate a package with all software you are licensed to install, install it and then save a copy of the PCD.DB.

Insert this file into every single Adobe (CC)-package you will create and the integration will work without issues!

Apple itunes 11.1.4 and Software License agreement (and Process Monitor)

After discussing the an upgrade of iTunes throughout the organization and the implications of suppressing the forced Software License Agreement within iTunes on the initial launch I decided to go on a discovery with the iTunes application.

Previously all packagers have surpressed the Software License Agreement by providing the iTunesPrefs.xml file within the package and placed a copy within both %APPDATA% and %LOCALAPPDATA%. During an upgrade the fact that such a file would be replaced of course overwrites any user preferences. Potentially we could provide some additional scripting to crack open the files and replace any particular value that would tell iTunes that the Software License Agreement is accepted. The value (for 11.1.4) looks like this in %APPDATA%;


<key>license-agreements</key>

<dict> <key>EA1068</key> <true/>

</dict>

Thats a lot of work. And I am lazy.

Let’s review the start-up process of iTunes, without having accepted the Software License Agreement in Process Monitor

The actual license-agreement is obtained from a file called License.rtf, so we can easily search for this file within Process Monitor to see just about where iTunes is deciding to show the Software License Agreement.

image

If we review the activity above we can spot that pre-reading the License.rtf file (sv.lproj is for Swedish – so I am getting a Swedish license agreement) it checks a few registry keys and the file iTunesPrefs.xml. Obviously the checking of the iTunesPrefs.xml-file is to check wether or not this particular user had accepted the license agreement. However, the check for the registry key within HKEY_LOCAL_MACHINE was a bit unexpected. Actually it is looking for the registry value SLA – Software License Agreement. Unfortunately there is no documentation of this value anywhere. Obvious one is just to create a DWORD with a value of either 1 or 0. Neither changes the behavior of iTunes, however it can be confirmed that iTunes does read the value. Creating a string (REG_SZ) with a 1,0,Yes,No,Accepted, iTunes or any other value doesn’t change anything.

It seems to be a perfect fit though? The name SLA seems to fit the scenario, however what value can actually change the behavior of iTunes? Within %APPDATA%\Apple Computer\iTunes and the file iTunesPrefs.xml there actually is an answer to the question. It seems that setting the same value as located within iTunesPrefs.xml gets iTunes to suppress the presentation of SLA for all users on machine.

image

The value seems to change for every new version of iTunes– so with a new version of iTunes one would have to accept it once manually and extract the necessary value from the iTunesPrefs.xml-file

Final registry key from a Windows 7 x64;


Windows Registry Editor Version 5.00

[HKEY_LOCAL_MACHINE\SOFTWARE\Wow6432Node\Apple Computer, Inc.\iTunes]
"SLA"="EA1068"

Let’s wrap up a MST-file for easy deployment!

Desktop shortcut

Stops the desktop shortcut from beeing created

Goto the InstallExecuteSequence-table and set the following;

image

Language / Software Update / Suppress reboot / SLA

Forces the language to English, disable the Software Update and suppress any reboot – aswell as allow the installation to complete by accepting SLA

Goto the Property-table and set the following;

image

iTunes lockdown and SLA

Lockdowns any feature you want of iTunes and suppresses the SLA prompt. For a full explanation of the Parental Control feature within iTunes you can read the Apple-published article; How to manage iTunes Control features. The suggested value below will do some basic lockdown such as disabling checks for new versions

Goto the Registry-table;

image

Finally a nice clean installation for iTunes!

Redistribute Failed Packages in ConfigMgr

Since the topic of redistributing failed packages is quite often surfacing in larger environments and there are quite a few PowerShell scripts out there to achieve this.

David O´Brien has written a PowerShell script that redistributes all packages that has any state (but successfull) to all DPs. In a larger environment this would be very risky (consider the amount of bandwidth you could potentially consume).

David went about the task by looking up the current state of the SMS_PackageStatusDistPointsSummarizer which has 7 states of a package , and then looping through all packages for all DPs and initiate the operation RefreshNow for each package and DP.

Within SCCM 2012 R2 there seems to be 9 possible states of a package, where a state 7 and 8 seems to be undocumented. State 7 would indicate that the source-files were not reachable for the SCCM 2012 server, and State 8 would indicate that a package validation failed (for any reason).

Quite often the need is more targeted and in particular we are required to only verify a single package or distribution point. As we would go through the console to check the state of a package and look under Content Status to see – it would be easiest to simply trigger a redistribute action for all DPs that are reported as failed. Previously Greg Ramsey released the great tool to start the action Validate All DPs, which can be initiated from any package under Content Status. Great tool! Lets take that one step further and create two additional menus within Configuration Manager console!

Redistribute a package to all DPs where it failed under Content Status

image

image


Param(
[Parameter(Mandatory=$true)]
[String]$SiteSrv,
[Parameter(Mandatory=$false)]
[String]$SiteNamespace,
[Parameter(Mandatory=$True)]
[String]$PackageID
)

$SiteCode = $SiteNamespace.Substring(14)

Write-Host "Checking" $PackageID -ForegroundColor red
Write-host "Will check for Installed Failed status (3) and Validation Failed (8)"
Write-Host ""

$sOpt = New-CimSessionOption –Protocol DCOM
$SiteServerCIM = New-CimSession -ComputerName $SiteSrv -SessionOption $sOpt

$DPs =  Get-CimInstance -CimSession $SiteServerCIM -Namespace $SiteNamespace -ClassName SMS_PackageStatusDistPointsSummarizer -Filter "PackageID = '$($PackageID)' AND (State = '3' OR State = '8')"
if ($DPs -ne $null)
{
$Count = $DPs |measure
Write-Host "There are $($Count.count) failed DPs at the moment."
Write-Host "Press any key to redistribute..."
Write-Host ""
$a = $host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown")
foreach ($dp in $DPs) {
Write-Host "Redistributing $($DP.PackageID) to $($DP.ServerNALPath.Substring(12,7))...." -ForegroundColor Green
try {

Get-CimInstance -CimSession $SiteServerCIM -Namespace $SiteNamespace -ClassName SMS_DistributionPoint -Filter "PackageID='$($DP.PackageID)' and ServerNALPath like '%$($DP.ServerNALPath.Substring(12,7))%'" | Set-CimInstance -Property @{RefreshNow = $true}
}
catch {
Write-Host "Failed to redistribute to $($DP.ServerNALPath.Substring(12,7))" -ForegroundColor Red
}

}
Write-Host "Press any key to close window...."
$a = $host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown")
}
else
{
Write-Host "There are no Failed DPs" -ForegroundColor Green
Write-Host "Press any key to close..."
$a = $host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown")

}

Remove-CimSession -CimSession $SiteServerCIM 

Create the following XML-file to enable the right-click menu under Content Status. The file should be placed in the following folder;

14214306-59f0-46cf-b453-a649f2a249e1


<ActionDescription Class="Executable" DisplayName="Redistribute to all failed DPs" MnemonicDisplayName="Redistribute to all failed DPs" Description = "Redistribute to all failed DPs" RibbonDisplayType="TextAndSmallImage">
<ShowOn>
<string>ContextMenu</string>
<string>DefaultHomeTab</string>
</ShowOn>
<Executable>
<FilePath>PowerShell.exe</FilePath>
<Parameters>-Executionpolicy bypass -nologo -WindowStyle normal -command "&amp; 'C:\PowerShellScripts\RedistFailed.ps1' '##SUB:__Server##' '##SUB:__Namespace##' '##SUB:PackageID##'" </Parameters>
</Executable>
</ActionDescription>

To enable a second menu under Distribution Point Configuration Status you can use the following script;

image

image


Param(
[Parameter(Mandatory=$true)]
[String]$SiteSrv,
[Parameter(Mandatory=$false)]
[String]$SiteNamespace,
[Parameter(Mandatory=$True)]
[String]$Server
)

$SiteCode = $SiteNamespace.Substring(14)

Write-Host "Checking" $Server -ForegroundColor red
Write-host "Will check for all failed packages (NOT State 0)"
Write-Host ""

$sOpt = New-CimSessionOption –Protocol DCOM
$SiteServerCIM = New-CimSession -ComputerName $SiteSrv -SessionOption $sOpt
try {
$pkgs =  Get-CimInstance -CimSession $SiteServerCIM -Namespace $SiteNamespace -ClassName SMS_PackageStatusDistPointsSummarizer -Filter "ServerNALPath like '%$($Server)%' AND (State != '0')"

if ($pkgs -ne $null)
{
$Count = $pkgs |measure
Write-Host "There are $($Count.count) failed packages at the moment."
Write-Host "Press any key to redistribute..."
Write-Host ""
$a = $host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown")
foreach ($pkg in $pkgs) {
Write-Host "Redistributing $($pkg.PackageID) to $($Server)...." -ForegroundColor Green
try {
Get-CimInstance -CimSession $SiteServerCIM -Namespace $SiteNamespace -ClassName SMS_DistributionPoint -Filter "PackageID='$($pkg.PackageID)' and ServerNALPath like '%$($Server)%'" | Set-CimInstance -Property @{RefreshNow = $true}
}
catch {
Write-Host "Failed to redistribute to $($pkg.ServerNALPath.Substring(12,7))" -ForegroundColor Red
}

}
Write-Host "Press any key to close window...."
$a = $host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown")
}
else
{
Write-Host "There are no Failed pkgs" -ForegroundColor Green
Write-Host "Press any key to close..."
$a = $host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown")

}

Remove-CimSession -CimSession $SiteServerCIM

}
Catch {
Write-Error "Failed to query $($Sitesrv)"
Remove-CimSession -CimSession $SiteServerCIM
} 

To enable the right-click menu, create a new XML-file under the following folder;

d8718784-99d5-4449-bc28-a26631fafc07

Content;

<ActionDescription Class="Executable" DisplayName="Redistribute all failed Pkgs" MnemonicDisplayName="Redistribute all failed Pkgs" Description = "Redistribute all failed Pkgs" RibbonDisplayType="TextAndSmallImage">
<ShowOn>
<string>ContextMenu</string>
<string>DefaultHomeTab</string>
</ShowOn>
<Executable>
<FilePath>PowerShell.exe</FilePath>
<Parameters>-Executionpolicy bypass -nologo -WindowStyle normal -command "&amp; 'C:\PowerShellScripts\redistfailedpkgstodp.ps1' '##SUB:__Server##' '##SUB:__Namespace##' '##SUB:NAME##'" </Parameters>
</Executable>
</ActionDescription> 

You can download the scripts from here, but you need to copy the XML-files into the folder of the Admin-Console yourself to make them visible.

Location;

c:\Program Files (x86)\Microsoft Configuration Manager\AdminConsole\XmlStorage\Extensions\Actions

1 2 3 14