396 lines
15 KiB
PowerShell
396 lines
15 KiB
PowerShell
<#
|
|
|
|
.SYNOPSIS
|
|
PSAppDeployToolkit - This script performs the installation or uninstallation of an application(s).
|
|
|
|
.DESCRIPTION
|
|
- The script is provided as a template to perform an install, uninstall, or repair of an application(s).
|
|
- The script either performs an "Install", "Uninstall", or "Repair" deployment type.
|
|
- The install deployment type is broken down into 3 main sections/phases: Pre-Install, Install, and Post-Install.
|
|
|
|
The script imports the PSAppDeployToolkit module which contains the logic and functions required to install or uninstall an application.
|
|
|
|
PSAppDeployToolkit is licensed under the GNU LGPLv3 License - (C) 2025 PSAppDeployToolkit Team (Sean Lillis, Dan Cunningham, Muhammad Mashwani, Mitch Richters, Dan Gough).
|
|
|
|
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the
|
|
Free Software Foundation, either version 3 of the License, or any later version. This program is distributed in the hope that it will be useful, but
|
|
WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
|
|
for more details. You should have received a copy of the GNU Lesser General Public License along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
.PARAMETER DeploymentType
|
|
The type of deployment to perform.
|
|
|
|
.PARAMETER DeployMode
|
|
Specifies whether the installation should be run in Interactive (shows dialogs), Silent (no dialogs), or NonInteractive (dialogs without prompts) mode.
|
|
|
|
NonInteractive mode is automatically set if it is detected that the process is not user interactive.
|
|
|
|
.PARAMETER AllowRebootPassThru
|
|
Allows the 3010 return code (requires restart) to be passed back to the parent process (e.g. SCCM) if detected from an installation. If 3010 is passed back to SCCM, a reboot prompt will be triggered.
|
|
|
|
.PARAMETER TerminalServerMode
|
|
Changes to "user install mode" and back to "user execute mode" for installing/uninstalling applications for Remote Desktop Session Hosts/Citrix servers.
|
|
|
|
.PARAMETER DisableLogging
|
|
Disables logging to file for the script.
|
|
|
|
.EXAMPLE
|
|
powershell.exe -File Invoke-AppDeployToolkit.ps1 -DeployMode Silent
|
|
|
|
.EXAMPLE
|
|
powershell.exe -File Invoke-AppDeployToolkit.ps1 -AllowRebootPassThru
|
|
|
|
.EXAMPLE
|
|
powershell.exe -File Invoke-AppDeployToolkit.ps1 -DeploymentType Uninstall
|
|
|
|
.EXAMPLE
|
|
Invoke-AppDeployToolkit.exe -DeploymentType "Install" -DeployMode "Silent"
|
|
|
|
.INPUTS
|
|
None. You cannot pipe objects to this script.
|
|
|
|
.OUTPUTS
|
|
None. This script does not generate any output.
|
|
|
|
.NOTES
|
|
Toolkit Exit Code Ranges:
|
|
- 60000 - 68999: Reserved for built-in exit codes in Invoke-AppDeployToolkit.ps1, and Invoke-AppDeployToolkit.exe
|
|
- 69000 - 69999: Recommended for user customized exit codes in Invoke-AppDeployToolkit.ps1
|
|
- 70000 - 79999: Recommended for user customized exit codes in PSAppDeployToolkit.Extensions module.
|
|
|
|
.LINK
|
|
https://psappdeploytoolkit.com
|
|
|
|
#>
|
|
|
|
[CmdletBinding()]
|
|
param
|
|
(
|
|
[Parameter(Mandatory = $false)]
|
|
[ValidateSet('Install', 'Uninstall', 'Repair')]
|
|
[PSDefaultValue(Help = 'Install', Value = 'Install')]
|
|
[System.String]$DeploymentType,
|
|
|
|
[Parameter(Mandatory = $false)]
|
|
[ValidateSet('Interactive', 'Silent', 'NonInteractive')]
|
|
[PSDefaultValue(Help = 'Interactive', Value = 'Interactive')]
|
|
[System.String]$DeployMode,
|
|
|
|
[Parameter(Mandatory = $false)]
|
|
[System.Management.Automation.SwitchParameter]$AllowRebootPassThru = $false,
|
|
|
|
[Parameter(Mandatory = $false)]
|
|
[System.Management.Automation.SwitchParameter]$TerminalServerMode = $false,
|
|
|
|
[Parameter(Mandatory = $false)]
|
|
[System.Management.Automation.SwitchParameter]$DisableLogging
|
|
)
|
|
|
|
|
|
##================================================
|
|
## MARK: Variables
|
|
##================================================
|
|
|
|
$adtSession = @{
|
|
# App variables.
|
|
AppVendor = 'NCHIS'
|
|
AppName = 'Default PDF Picker'
|
|
AppVersion = '0.1'
|
|
AppArch = 'x86'
|
|
AppLang = 'EN'
|
|
AppRevision = '01'
|
|
AppSuccessExitCodes = @(0)
|
|
AppRebootExitCodes = @(1641, 3010)
|
|
AppScriptVersion = '1.0.0'
|
|
AppScriptDate = '08/12/2025'
|
|
AppScriptAuthor = 'jxp066admin'
|
|
|
|
# Install Titles (Only set here to override defaults set by the toolkit).
|
|
InstallName = ''
|
|
InstallTitle = ''
|
|
|
|
# Script variables.
|
|
DeployAppScriptFriendlyName = $MyInvocation.MyCommand.Name
|
|
DeployAppScriptVersion = '4.0.6'
|
|
DeployAppScriptParameters = $PSBoundParameters
|
|
}
|
|
|
|
function Install-ADTDeployment
|
|
{
|
|
##================================================
|
|
## MARK: Pre-Install
|
|
##================================================
|
|
$adtSession.InstallPhase = "Pre-$($adtSession.DeploymentType)"
|
|
|
|
## Find the Users current default PDF application.
|
|
|
|
### Need a list of selectable variables here for the options, that feed into the SetUserFTA.exe parameters.
|
|
$PDFMap = @{
|
|
"AcroExch.Document.DC" = "Adobe Acrobat"
|
|
"AcroExch.Document" = "Adobe Reader"
|
|
"NitroPDF.Pro.v13" = "Nitro PDF Pro 13"
|
|
"NitroPDF.Pro.v14" = "Nitro PDF Pro 14"
|
|
"Bluebeam.Revu.PDF" = "Bluebeam Revu"
|
|
"RedlinePDFfile" = "Bluebeam Revu PDF"
|
|
"MSEdgePDF" = "Microsoft Edge"
|
|
}
|
|
|
|
$CurrentPDFLocal = 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Explorer\FileExts\.pdf\UserChoice'
|
|
$CurrentPDF = (Get-ItemProperty $CurrentPDFLocal -ErrorAction SilentlyContinue).ProgId
|
|
if ($CurrentPDF) {
|
|
Write-ADTLogEntry -Message ("Detected current PDF ProgID: {0}" -f $CurrentPDF) -Severity 1
|
|
}
|
|
else {
|
|
Write-ADTLogEntry -Message 'No current PDF association set' -Severity 2
|
|
}
|
|
|
|
# Show current default (friendly name if known) in interactive mode
|
|
try {
|
|
$currentFriendly = if ($CurrentPDF -and $PDFMap.ContainsKey($CurrentPDF)) { $PDFMap[$CurrentPDF] } else { $CurrentPDF }
|
|
if ($adtSession.DeployMode -eq 'Interactive') {
|
|
$msgFriendly = if ([string]::IsNullOrWhiteSpace($currentFriendly)) { 'Unknown' } else { $currentFriendly }
|
|
Show-ADTInstallationPrompt -Message ("Current default PDF application: {0}" -f $msgFriendly) -ButtonRightText 'OK'
|
|
}
|
|
} catch { Write-ADTLogEntry -Message ("Failed to show current default prompt: {0}" -f $_) -Severity 2 }
|
|
|
|
|
|
|
|
|
|
|
|
function Is-ProgIDPresent($progId) {
|
|
$paths = @("HKCR:\$progId","HKCU:\Software\Classes\$progId","HKLM:\Software\Classes\$progId")
|
|
foreach ($p in $paths) { if (Test-Path $p) { return $true } }
|
|
return $false
|
|
}
|
|
|
|
function Get-InstalledPDFProgIDs {
|
|
$installed = @()
|
|
foreach ($progId in $PDFMap.Keys) { if (Is-ProgIDPresent $progId) { $installed += $progId } }
|
|
return $installed
|
|
}
|
|
|
|
function Get-CurrentPDFHandler {
|
|
try {
|
|
$keyPath = "HKCU:\Software\Microsoft\Windows\CurrentVersion\Explorer\FileExts\.pdf\UserChoice"
|
|
if (Test-Path $keyPath) {
|
|
$pid = (Get-ItemProperty -Path $keyPath -Name ProgId -ErrorAction Stop).ProgId
|
|
return $pid
|
|
} else { return "Unknown" }
|
|
} catch { return "Error" }
|
|
}
|
|
|
|
function Select-PDFProgId {
|
|
param(
|
|
[string[]]$Candidates,
|
|
[string]$Current
|
|
)
|
|
# If not interactive, prefer current; otherwise present a simple selection UI.
|
|
if ($adtSession.DeployMode -ne 'Interactive') { return $Current }
|
|
|
|
try {
|
|
Add-Type -AssemblyName System.Windows.Forms | Out-Null
|
|
Add-Type -AssemblyName System.Drawing | Out-Null
|
|
|
|
$form = New-Object System.Windows.Forms.Form
|
|
$form.Text = 'Select Default PDF Handler'
|
|
$form.StartPosition = 'CenterScreen'
|
|
$form.Width = 520
|
|
$form.Height = 180
|
|
$form.Topmost = $true
|
|
|
|
$label = New-Object System.Windows.Forms.Label
|
|
$label.AutoSize = $true
|
|
$label.Text = 'Choose the application to handle .pdf files:'
|
|
$label.Location = New-Object System.Drawing.Point(12, 12)
|
|
$form.Controls.Add($label)
|
|
|
|
$combo = New-Object System.Windows.Forms.ComboBox
|
|
$combo.DropDownStyle = 'DropDownList'
|
|
$combo.Width = 480
|
|
$combo.Location = New-Object System.Drawing.Point(12, 40)
|
|
|
|
# Build display list of "Friendly (ProgID)"
|
|
$items = @()
|
|
foreach ($pid in $Candidates) {
|
|
$friendly = if ($PDFMap.ContainsKey($pid)) { $PDFMap[$pid] } else { $pid }
|
|
$items += ("{0} ({1})" -f $friendly, $pid)
|
|
}
|
|
$combo.Items.AddRange($items)
|
|
|
|
if ($Current -and $Candidates -contains $Current) {
|
|
$friendlyCur = if ($PDFMap.ContainsKey($Current)) { $PDFMap[$Current] } else { $Current }
|
|
$displayCur = ("{0} ({1})" -f $friendlyCur, $Current)
|
|
$combo.SelectedItem = $displayCur
|
|
} elseif ($combo.Items.Count -gt 0) {
|
|
$combo.SelectedIndex = 0
|
|
}
|
|
$form.Controls.Add($combo)
|
|
|
|
$ok = New-Object System.Windows.Forms.Button
|
|
$ok.Text = 'OK'
|
|
$ok.Width = 80
|
|
$ok.Location = New-Object System.Drawing.Point(332, 80)
|
|
$ok.DialogResult = [System.Windows.Forms.DialogResult]::OK
|
|
$form.AcceptButton = $ok
|
|
$form.Controls.Add($ok)
|
|
|
|
$cancel = New-Object System.Windows.Forms.Button
|
|
$cancel.Text = 'Cancel'
|
|
$cancel.Width = 80
|
|
$cancel.Location = New-Object System.Drawing.Point(412, 80)
|
|
$cancel.DialogResult = [System.Windows.Forms.DialogResult]::Cancel
|
|
$form.CancelButton = $cancel
|
|
$form.Controls.Add($cancel)
|
|
|
|
$result = $form.ShowDialog()
|
|
if ($result -ne [System.Windows.Forms.DialogResult]::OK) { return $Current }
|
|
|
|
$selection = [string]$combo.SelectedItem
|
|
if (-not $selection) { return $Current }
|
|
# Extract ProgID from "Name (ProgID)"
|
|
$m = [regex]::Match($selection, '\(([^)]+)\)$')
|
|
if ($m.Success) { return $m.Groups[1].Value } else { return $Current }
|
|
} catch {
|
|
Write-ADTLogEntry -Message ("Selection UI failed: {0}" -f $_) -Severity 2
|
|
return $Current
|
|
}
|
|
}
|
|
|
|
##================================================
|
|
## MARK: Install
|
|
##================================================
|
|
$adtSession.InstallPhase = $adtSession.DeploymentType
|
|
|
|
# Determine desired ProgID and set association via SetUserFTA
|
|
$installedProgIds = Get-InstalledPDFProgIDs
|
|
$currentProgId = Get-CurrentPDFHandler
|
|
$targetProgId = Select-PDFProgId -Candidates $installedProgIds -Current $currentProgId
|
|
|
|
if ($targetProgId -and $targetProgId -ne 'Unknown' -and $targetProgId -ne 'Error') {
|
|
$args = ".pdf $targetProgId"
|
|
Write-ADTLogEntry -Message ("Setting .pdf handler to {0}" -f $targetProgId) -Severity 1
|
|
Start-ADTProcessAsUser -FilePath "C:\ProgramData\SetUserFTA\SetUserFTA.exe" -ArgumentList $args -SuccessExitCodes 0, 500
|
|
} else {
|
|
Write-ADTLogEntry -Message 'Skipping SetUserFTA: no valid target ProgID determined' -Severity 2
|
|
}
|
|
|
|
##================================================
|
|
## MARK: Post-Install
|
|
##================================================
|
|
$adtSession.InstallPhase = "Post-$($adtSession.DeploymentType)"
|
|
|
|
## Master Wrapper detection
|
|
Set-ADTRegistryKey -Key "HKLM\SOFTWARE\InstalledApps\NCHIS_Default PDF Picker_0.1"
|
|
}
|
|
|
|
function Uninstall-ADTDeployment
|
|
{
|
|
##================================================
|
|
## MARK: Pre-Uninstall
|
|
##================================================
|
|
$adtSession.InstallPhase = "Pre-$($adtSession.DeploymentType)"
|
|
|
|
##================================================
|
|
## MARK: Uninstall
|
|
##================================================
|
|
$adtSession.InstallPhase = $adtSession.DeploymentType
|
|
|
|
##================================================
|
|
## MARK: Post-Uninstallation
|
|
##================================================
|
|
$adtSession.InstallPhase = "Post-$($adtSession.DeploymentType)"
|
|
|
|
## Master Wrapper detection
|
|
Remove-ADTRegistryKey -Key "HKLM\SOFTWARE\InstalledApps\NCHIS_Default PDF Picker_0.1"
|
|
}
|
|
|
|
function Repair-ADTDeployment
|
|
{
|
|
##================================================
|
|
## MARK: Pre-Repair
|
|
##================================================
|
|
$adtSession.InstallPhase = "Pre-$($adtSession.DeploymentType)"
|
|
|
|
##================================================
|
|
## MARK: Repair
|
|
##================================================
|
|
$adtSession.InstallPhase = $adtSession.DeploymentType
|
|
|
|
##================================================
|
|
## MARK: Post-Repair
|
|
##================================================
|
|
$adtSession.InstallPhase = "Post-$($adtSession.DeploymentType)"
|
|
|
|
## Master Wrapper detection
|
|
Set-ADTRegistryKey -Key "HKLM\SOFTWARE\InstalledApps\NCHIS_Default PDF Picker_0.1"
|
|
}
|
|
|
|
|
|
##================================================
|
|
## MARK: Initialization
|
|
##================================================
|
|
|
|
# Set strict error handling across entire operation.
|
|
$ErrorActionPreference = [System.Management.Automation.ActionPreference]::Stop
|
|
$ProgressPreference = [System.Management.Automation.ActionPreference]::SilentlyContinue
|
|
Set-StrictMode -Version 1
|
|
|
|
# Import the module and instantiate a new session.
|
|
try
|
|
{
|
|
$moduleName = if ([System.IO.File]::Exists("$PSScriptRoot\PSAppDeployToolkit\PSAppDeployToolkit.psd1"))
|
|
{
|
|
Get-ChildItem -LiteralPath $PSScriptRoot\PSAppDeployToolkit -Recurse -File | Unblock-File -ErrorAction Ignore
|
|
"$PSScriptRoot\PSAppDeployToolkit\PSAppDeployToolkit.psd1"
|
|
}
|
|
else
|
|
{
|
|
'PSAppDeployToolkit'
|
|
}
|
|
Import-Module -FullyQualifiedName @{ ModuleName = $moduleName; Guid = '8c3c366b-8606-4576-9f2d-4051144f7ca2'; ModuleVersion = '4.0.6' } -Force
|
|
try
|
|
{
|
|
$iadtParams = Get-ADTBoundParametersAndDefaultValues -Invocation $MyInvocation
|
|
$adtSession = Open-ADTSession -SessionState $ExecutionContext.SessionState @adtSession @iadtParams -PassThru
|
|
}
|
|
catch
|
|
{
|
|
Remove-Module -Name PSAppDeployToolkit* -Force
|
|
throw
|
|
}
|
|
}
|
|
catch
|
|
{
|
|
$Host.UI.WriteErrorLine((Out-String -InputObject $_ -Width ([System.Int32]::MaxValue)))
|
|
exit 60008
|
|
}
|
|
|
|
|
|
##================================================
|
|
## MARK: Invocation
|
|
##================================================
|
|
|
|
try
|
|
{
|
|
Get-Item -Path $PSScriptRoot\PSAppDeployToolkit.* | & {
|
|
process
|
|
{
|
|
Get-ChildItem -LiteralPath $_.FullName -Recurse -File | Unblock-File -ErrorAction Ignore
|
|
Import-Module -Name $_.FullName -Force
|
|
}
|
|
}
|
|
& "$($adtSession.DeploymentType)-ADTDeployment"
|
|
Close-ADTSession
|
|
}
|
|
catch
|
|
{
|
|
Write-ADTLogEntry -Message ($mainErrorMessage = Resolve-ADTErrorRecord -ErrorRecord $_) -Severity 3
|
|
Show-ADTDialogBox -Text $mainErrorMessage -Icon Stop | Out-Null
|
|
Close-ADTSession -ExitCode 60001
|
|
}
|
|
finally
|
|
{
|
|
Remove-Module -Name PSAppDeployToolkit* -Force
|
|
}
|
|
|