Last active
November 5, 2024 15:44
-
-
Save rileyz/46fdd88b7360b25cef72d063e7798a43 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<# | |
.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