Skip to content

Instantly share code, notes, and snippets.

@rileyz
Last active November 5, 2024 15:44
Show Gist options
  • Save rileyz/46fdd88b7360b25cef72d063e7798a43 to your computer and use it in GitHub Desktop.
Save rileyz/46fdd88b7360b25cef72d063e7798a43 to your computer and use it in GitHub Desktop.
<#
.SYNOPSIS
Script to assist with launching ClickOnce applications as a Microsoft RemoteApp or Citrix
Virtual Apps.
.DESCRIPTION
Intended Use
This script was produced to assist in launching ClickOnce applications where the update
mechanism must remain intact.
Update housekeeping variables as desired. The two important variables are $LogPath and
$ClickOnceReferenceFilePath.
The reference file must be tab delimited to avoid possible issues with special characters in a
URL. The reference file must contain the following header information
UniqueReference;DetectionExecutable;UniformResourceLocator. Obviously replace semicolons with
tab.
About
I created this script to solve the issue of presenting ClickOnce application in a RemoteApp or
Virtual Apps environment whilst keeping the ClickOnce updating intact. Due to the environment
ClickOnce application do not launch as they normally do on traditional desktop environments.
Created with scalability in mind, this script and reference file and be stored and run from a
network share.
Known Defects/Bugs
* Defect/Bug none known.
Code Snippet Credits
* https://stackoverflow.com/questions/11833641/using-rundll32-exe-to-launch-a-click-once-deployment-url
* https://community.citrix.com/forums/topic/185288-seamless-clickonce
* https://clickonceget.azurewebsites.net/
Version History
1.00 05/11/2024
Initial release.
Copyright & Intellectual Property
Feel to copy, modify and redistribute, but please pay credit where it is due.
Feedback is welcome, please contact me on LinkedIn.
.LINK
Author:.......https://www.linkedin.com/in/rileylim
Source Code:..https://gist.github.com/rileyz/46fdd88b7360b25cef72d063e7798a43
Article:......https://www
.EXAMPLE
powershell.exe -File \\NetworkShare\Start-ClickOnceApplication.ps1 -UniqueReference ClickOnceWpfTetris
.EXAMPLE
Reference file example, please use a text editor that shows special characters, tabs, carriage
returns, etc. Copy and paste the below to a new text file.
UniqueReference DetectionExecutable UniformResourceLocator
exampleUniqueReference exampleDetectionExecutable.exe https://exampleUniformResourceLocator.com
ClickOnceWpfTetris WpfTetris.exe https://clickonceget.azurewebsites.net/app/WpfTetris/WpfTetris.application
ClickOnceWpfTetris is an untrusted example, use at your own risk.
https://www.virustotal.com/gui/file/0e2b0f70ea0f5706585ad21a81a6a2f77fc411fd53824958598a0238d2231656
#>
# Parameters List #################################################################################
param (
[string]$UniqueReference
)
#<<< End Of Parameters List >>>
# Function List ###################################################################################
function WriteLog {
param (
[switch] $Debug,
[switch] $Verbose,
[String] $Message,
[String] $SetPersistentLogPath,
[ValidateSet('DebugAndVerboseToLog','NoLogging','VerboseToLog')][string[]]$SetPersistentLogToFileLevel
)
if (!$PSScriptRoot) {
Write-Warning 'Can not create log, script has not been saved.'
throw
}
if ($Global:WriteLogDebugToLog -eq $null) {
Write-Debug '$Global:WriteLogDebugToLog is $null.'
if ([string]::IsNullOrEmpty($SetPersistentLogToFileLevel)) {
$Global:WriteLogDebugToLog = 'VerboseToLog'
}
else {
$Global:WriteLogDebugToLog = $SetPersistentLogToFileLevel
}
}
if ($SetPersistentLogToFileLevel -ne $null) {
if ($Global:WriteLogDebugToLog -eq $SetPersistentLogToFileLevel) {
Write-Debug '$Global:WriteLogDebugToLog is the same as $SetPersistentLogToFileLevel, no action required.'
}
else {
$Global:WriteLogDebugToLog = $SetPersistentLogToFileLevel
Write-Debug "Persistent log level previously set."
Write-Debug "Now overriding log level to $Global:WriteLogDebugToLog."
}
}
If ($Global:WriteLogPersistentPath -eq $null) {
Write-Debug '$Global:WriteLogPersistentPath is $null.'
If ([string]::IsNullOrEmpty($SetPersistentLogPath)) {
Write-Debug '$SetPersistentLogPath is $null.'
$Global:WriteLogPersistentPath = ((Get-PSCallStack | Where-Object ScriptName -ne $null)[-1].ScriptName) + '.log'
}
else {
if (Test-Path $SetPersistentLogPath) {
Write-Debug '$SetPersistentLogPath is a valid path.'
if ((Get-Item $SetPersistentLogPath) -is [System.IO.DirectoryInfo]) {
Write-Debug '$SetPersistentLogPath is a directory.'
If ($SetPersistentLogPath -match '\\$') {
Write-Debug '$SetPersistentLogPath has a trailing backslash.'
$Global:WriteLogPersistentPath = $SetPersistentLogPath + (Split-Path ((Get-PSCallStack | Where-Object ScriptName -ne $null)[-1].ScriptName) -Leaf) + '.log'
}
else {
Write-Debug '$SetPersistentLogPath does not have a trailing backslash.'
$Global:WriteLogPersistentPath = $SetPersistentLogPath + '\' + (Split-Path ((Get-PSCallStack | Where-Object ScriptName -ne $null)[-1].ScriptName) -Leaf) + '.log'
}
}
else {
Write-Debug '$SetPersistentLogPath is a path an existing file.'
$Global:WriteLogPersistentPath = $SetPersistentLogPath
}
}
else {
If (Test-Path (Split-Path $SetPersistentLogPath)) {
Write-Debug '$SetPersistentLogPath is a directory path, creating log file on write.'
$Global:WriteLogPersistentPath = $SetPersistentLogPath
}
else {
Write-Debug '$SetPersistentLogPath is not a valid directory.'
}
}
}
}
elseif (!([string]::IsNullOrEmpty($SetPersistentLogPath))) {
if (Test-Path $SetPersistentLogPath) {
Write-Debug '$SetPersistentLogPath is a valid path.'
if ((Get-Item $SetPersistentLogPath) -is [System.IO.DirectoryInfo]) {
Write-Debug '$SetPersistentLogPath is a directory.'
If ($SetPersistentLogPath -match '\\$') {
Write-Debug '$SetPersistentLogPath has a trailing backslash.'
$Global:WriteLogPersistentPath = $SetPersistentLogPath + (Split-Path ((Get-PSCallStack | Where-Object ScriptName -ne $null)[-1].ScriptName) -Leaf) + '.log'
}
else {
Write-Debug '$SetPersistentLogPath does not have a trailing backslash.'
$Global:WriteLogPersistentPath = $SetPersistentLogPath + '\' + (Split-Path ((Get-PSCallStack | Where-Object ScriptName -ne $null)[-1].ScriptName) -Leaf) + '.log'
}
}
else {
Write-Debug '$SetPersistentLogPath is a path an existing file.'
$Global:WriteLogPersistentPath = $SetPersistentLogPath
}
Write-Debug "Persistent log path previously set."
Write-Debug "Now overriding log path to $Global:WriteLogPersistentPath"
}
else {
If (Test-Path (Split-Path $SetPersistentLogPath)) {
Write-Debug '$SetPersistentLogPath is a directory path, creating log file on write.'
$Global:WriteLogPersistentPath = $SetPersistentLogPath
Write-Debug "Persistent log path previously set."
Write-Debug "Now overriding log path to $Global:WriteLogPersistentPath."
}
else {
Write-Debug '$SetPersistentLogPath is not a valid directory.'
}
}
}
if ($Debug) {
if (($Global:WriteLogDebugToLog -match 'DebugAndVerboseToLog')) {
"$(Get-Date -Format 'u') - Debug - $Message" | Out-File -Append $Global:WriteLogPersistentPath
}
if ($DebugPreference -eq 'Continue') {
Write-Debug $Message
}
}
if ($Verbose) {
if (($Global:WriteLogDebugToLog -match 'DebugAndVerboseToLog|VerboseToLog')) {
"$(Get-Date -Format 'u') - Verbose - $Message" | Out-File -Append $Global:WriteLogPersistentPath
}
if ($VerbosePreference -eq 'Continue') {
Write-Verbose $Message
}
}
} #End function WriteLog
#<<< End Of Function List >>>
# Setting up housekeeping #########################################################################
$DebugPreference = 'SilentlyContinue' #SilentlyContinue|Continue
$VerbosePreference = 'SilentlyContinue' #SilentlyContinue|Continue
$LogPath = $env:TEMP #$env:TEMP|$PSScriptRoot
$LogLevel = 'DebugAndVerboseToLog' #'NoLogging'|'VerboseToLog'|'DebugAndVerboseToLog'
$ClickOnceReferenceFilePath = "$PSScriptRoot\Start-ClickOnceApplicationReference.txt"
#<<< End of Setting up housekeeping >>>
# Start of script work ############################################################################
WriteLog -SetPersistentLogPath $LogPath
WriteLog -SetPersistentLogToFileLevel $LogLevel
WriteLog -Verbose -Message "Launching '$UniqueReference', one moment please."
WriteLog -Debug -Message 'Importing reference file.'
if (-not(Test-Path -Path "$ClickOnceReferenceFilePath")) {
throw '$ClickOnceApplicationReferenceFile cannot be found.'
}
$ClickOnceReference = Import-Csv -Delimiter `t -Path "$ClickOnceReferenceFilePath"
#Comment the line below to obfuscate the reference data in the log file.
WriteLog -Debug -Message "$($ClickOnceReference | out-string)"
WriteLog -Debug -Message 'Discovering launch scenario.'
$ClickOnceToLaunch = $ClickOnceReference |
Where-Object {
$_.UniqueReference -eq $UniqueReference
}
if ($ClickOnceToLaunch -eq $null) {
Write-Verbose "UniqueReference '$UniqueReference' not found in reference file."
throw "UniqueReference '$UniqueReference' not found in reference file."
}
if ($ClickOnceToLaunch.UniqueReference.Count -ne 1) {
Write-Verbose "UniqueReference '$UniqueReference' found more than one entry."
throw "UniqueReference '$UniqueReference' found more than one entry."
}
$DetectionExecutable = $ClickOnceToLaunch.DetectionExecutable
$UniformResourceLocator = $ClickOnceToLaunch.UniformResourceLocator
if ($DetectionExecutable -match '\.exe$') {
WriteLog -Debug -Message " DetectionExecutable matched '.exe', stripping extension."
$DetectionExecutable = ($DetectionExecutable -split '.exe')[0]
}
WriteLog -Debug " DetectionExecutable is '$DetectionExecutable'."
WriteLog -Debug " UniformResourceLocator is '$UniformResourceLocator'."
WriteLog -Debug -Message 'Discovering Microsoft .NET Framework support binaries.'
$NetRegistryPath = "HKLM:\SOFTWARE\Microsoft\NET Framework Setup\NDP\v4\Full"
$NetBinariesPath = (Get-ItemProperty -Path "$NetRegistryPath").InstallPath
$ClickOnceSupportExecutable = "$NetBinariesPath\dfsvc.exe"
if (Test-Path -Path "$ClickOnceSupportExecutable") {
WriteLog -Debug -Message " ClickOnce support executable 'dfsvc.exe' found."
} else {
WriteLog -Debug -Message " ClickOnce support executable 'dfsvc.exe' not found."
throw WriteLog -Debug -Message " ClickOnce support executable 'dfsvc.exe' not found."
}
WriteLog -Debug -Message ' Starting ClickOnce support process.'
$ClickOnceSupportProcess = $null
$ClickOnceSupportProcess = Start-Process -FilePath "$ClickOnceSupportExecutable" -PassThru
do {
Start-Sleep -Milliseconds 1500
WriteLog -Debug -Message ' Waiting for ClickOnce support process to start.'
} while ($ClickOnceSupportProcess -eq $null)
WriteLog -Debug -Message "$($ClickOnceSupportProcess | Out-String)"
WriteLog -Debug -Message 'Starting ClickOnce application.'
& rundll32.exe dfshim.dll,ShOpenVerbApplication $UniformResourceLocator
$ClickOnceAppProcess = $null
while ($ClickOnceAppProcess -eq $null) {
Start-Sleep -Milliseconds 1000
WriteLog -Debug -Message ' Wait for application process to start.'
$ClickOnceAppProcess = Get-Process -Name "$DetectionExecutable" -ErrorAction SilentlyContinue |
Where-Object -FilterScript {
$_.Path -like "*$env:USERNAME\AppData\Local\Apps\2.0\*"
}
}
WriteLog -Debug -Message ' Application process detected.'
WriteLog -Debug -Message "$($ClickOnceAppProcess | out-string)"
WriteLog -Debug -Message 'Stopping ClickOnce support process.'
Stop-Process $ClickOnceSupportProcess.id
#<<< End of script work >>>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment