param( [ValidateSet('Check','Install')][string]$Mode = 'Check', [string]$CurrentVersion, [string]$ExpectedVersion, [string]$InstallDirectory, [string]$WorkDirectory, [int]$ParentProcessId, [string]$ReadyEvent, [switch]$ShowProgress, [switch]$Library ) $ErrorActionPreference = 'Stop' Set-StrictMode -Version Latest $script:progressWindow = $null function Start-UpdateProgress { Add-Type -AssemblyName System.Windows.Forms Add-Type -AssemblyName System.Drawing if (!('KSwitcherUpdateProgress' -as [type])) { Add-Type -ReferencedAssemblies System.Windows.Forms,System.Drawing -TypeDefinition @' using System; using System.Drawing; using System.Threading; using System.Windows.Forms; using System.Drawing.Drawing2D; using System.Runtime.InteropServices; public sealed class UpdateMeter : Control { public int Percent = -1; private int position; private readonly System.Windows.Forms.Timer timer = new System.Windows.Forms.Timer { Interval = 35 }; public UpdateMeter() { DoubleBuffered = true; timer.Tick += delegate { if (Percent < 0) { position = (position + 5) % Math.Max(1,Width); Invalidate(); } }; timer.Start(); } protected override void OnPaint(PaintEventArgs e) { using(var track = new SolidBrush(Color.FromArgb(232,234,241))) e.Graphics.FillRectangle(track,0,0,Width,Height); using(var fill = new SolidBrush(Color.FromArgb(79,70,229))) { if(Percent < 0) e.Graphics.FillRectangle(fill,position-Width/4,0,Width/4,Height); else e.Graphics.FillRectangle(fill,0,0,Width*Percent/100,Height); } } protected override void Dispose(bool disposing) { if(disposing) timer.Dispose(); base.Dispose(disposing); } } public sealed class UpdateSteps : Control { public int Current = -1; public bool Complete; public UpdateSteps() { DoubleBuffered = true; } protected override void OnPaint(PaintEventArgs e) { e.Graphics.SmoothingMode = SmoothingMode.AntiAlias; string[] names = {"Download","Verify","Install","Restart"}; for(int i=0;i<4;i++) { int x = i*(Width-80)/3; Color color = Complete || i<=Current ? Color.FromArgb(79,70,229) : Color.FromArgb(135,141,158); using(var pen = new Pen(Color.FromArgb(231,233,240))) if(i<3) e.Graphics.DrawLine(pen,x+22,8,x+(Width-80)/3-10,8); using(var brush = new SolidBrush(color)) { if(Complete || i=0) detail.Text = text.Substring(separator+4)+" downloaded"; if(title.StartsWith("Downloading")) steps.Current=0; else if(title.StartsWith("Verifying") || title.StartsWith("Preparing")) steps.Current=1; else if(title.StartsWith("Installing") || title.StartsWith("Closing")) steps.Current=2; else if(title.StartsWith("Restarting")) steps.Current=3; progress.Invalidate(); steps.Invalidate(); }); } public void SavePreview(string path) { form.Invoke((Action)delegate { using(var bitmap = new Bitmap(form.Width,form.Height)) { form.DrawToBitmap(bitmap,new Rectangle(0,0,bitmap.Width,bitmap.Height)); bitmap.Save(path,System.Drawing.Imaging.ImageFormat.Png); } }); } public string Status { get { return (string)form.Invoke((Func)delegate { return stage.Text; }); } } public int Percent { get { return (int)form.Invoke((Func)delegate { return progress.Percent; }); } } public void Finish(string title, string text, bool success) { form.Invoke((Action)delegate { closing = true; stage.Text = title; detail.Text = text; progress.Percent = success ? 100 : 0; percentage.Text = success ? "100%" : ""; steps.Complete = success; if(!success) { detail.Visible = false; steps.Visible = false; var result = new TextBox { Text = text, Location = new Point(28,148), Size = new Size(484,126), Multiline = true, ReadOnly = true, BorderStyle = BorderStyle.None, BackColor = Color.White, ScrollBars = ScrollBars.Vertical }; form.Controls.Add(result); result.BringToFront(); progress.Visible = false; } footer.Text = success ? "Your settings and history are preserved." : "Review the details above."; close.Enabled = true; done.Visible = true; form.AcceptButton = done; form.CancelButton = done; progress.Invalidate(); steps.Invalidate(); }); } public void WaitForClose() { thread.Join(); } public void Dispose() { if (form != null && !form.IsDisposed) form.BeginInvoke((Action)delegate { closing = true; form.Close(); }); thread.Join(3000); } } '@ } $script:progressWindow = New-Object KSwitcherUpdateProgress } function Show-UpdateProgress([string]$Stage,[string]$Detail,[int]$Percent = -1) { if ($script:progressWindow) { try { $script:progressWindow.Report($Stage,$Detail,$Percent) } catch { Write-Warning 'Could not refresh update progress.' } } } function Complete-UpdateProgress([string]$Title,[string]$Detail,[bool]$Success) { if ($script:progressWindow) { try { $script:progressWindow.Finish($Title,$Detail,$Success); return $true } catch { Write-Warning 'Could not display the update result.' } } return $false } function Get-StableVersion([string]$Tag) { if ($Tag -notmatch '^(?:v|release-v)?(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$') { return $null } try { return [version]($Tag -replace '^(?:release-)?v','') } catch { return $null } } function Assert-ReleaseUrl([string]$Url) { $uri = [uri]$Url if (!$uri.IsAbsoluteUri -or $uri.Scheme -ne 'https' -or $uri.Host -ne 'gitea.hrynco.com' -or !$uri.IsDefaultPort -or $uri.UserInfo -or $uri.Query -or $uri.Fragment -or !$uri.AbsolutePath.StartsWith('/hrynco/hrynco-kswitcher/releases/download/',[StringComparison]::Ordinal)) { throw 'Release asset URL is outside the KSwitcher repository.' } } function Receive-File([string]$Url,[string]$Path,[long]$Limit,[string]$ProgressLabel = '') { [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 $request = [Net.HttpWebRequest]::Create($Url) $request.AllowAutoRedirect = $false $request.Timeout = 15000 $request.ReadWriteTimeout = 15000 $request.UserAgent = 'KSwitcher-Updater' $response = $null; $inputStream = $null; $outputStream = $null try { $response = $request.GetResponse() if ([int]$response.StatusCode -ne 200 -or $response.ContentLength -gt $Limit) { throw 'Invalid or oversized server response.' } $inputStream = $response.GetResponseStream() $outputStream = [IO.File]::Create($Path) $buffer = New-Object byte[] 65536 $total = 0L $deadline = [DateTime]::UtcNow.AddSeconds(60) $lastProgress = [DateTime]::MinValue while (($count = $inputStream.Read($buffer,0,$buffer.Length)) -gt 0) { $total += $count if ($total -gt $Limit -or [DateTime]::UtcNow -gt $deadline) { throw 'Download exceeded size or time limit.' } $outputStream.Write($buffer,0,$count) if ($ProgressLabel -and ([DateTime]::UtcNow - $lastProgress).TotalMilliseconds -ge 100) { $percent = if ($response.ContentLength -gt 0) { [int][Math]::Min(100,100.0*$total/$response.ContentLength) } else { -1 } $detail = if ($percent -ge 0) { '{0}% - {1:N1} / {2:N1} MB' -f $percent,($total/1MB),($response.ContentLength/1MB) } else { '{0:N1} MB received' -f ($total/1MB) } Show-UpdateProgress $ProgressLabel $detail $percent $lastProgress = [DateTime]::UtcNow } } if ($response.ContentLength -ge 0 -and $total -ne $response.ContentLength) { throw 'The download ended before the complete file was received.' } if ($ProgressLabel) { Show-UpdateProgress $ProgressLabel ('Downloaded {0:N1} MB' -f ($total/1MB)) 100 } } finally { if ($outputStream) { $outputStream.Dispose() } if ($inputStream) { $inputStream.Dispose() } if ($response) { $response.Dispose() } } } function Select-Release($Releases,[version]$Current) { $candidates = @($Releases | Where-Object { !$_.draft -and !$_.prerelease -and (Get-StableVersion $_.tag_name) } | Sort-Object { Get-StableVersion $_.tag_name } -Descending) if (!$candidates.Count -or (Get-StableVersion $candidates[0].tag_name) -le $Current) { return $null } $release = $candidates[0] $zip = @($release.assets | Where-Object name -CEQ 'KSwitcher-win-x64.zip') $sum = @($release.assets | Where-Object name -CEQ 'SHA256SUMS.txt') if ($zip.Count -ne 1 -or $sum.Count -ne 1) { throw 'The newest release lacks its x64 ZIP or SHA256SUMS.txt.' } Assert-ReleaseUrl $zip[0].browser_download_url Assert-ReleaseUrl $sum[0].browser_download_url return @{ version=$release.tag_name; zip=$zip[0].browser_download_url; checksum=$sum[0].browser_download_url } } function Assert-Checksum([string]$Archive,[string]$Checksum) { $matchesFound = @([IO.File]::ReadAllLines($Checksum) | Where-Object { $_ -cmatch '^([a-fA-F0-9]{64})\s+\*?KSwitcher-win-x64\.zip$' }) if ($matchesFound.Count -ne 1) { throw 'Missing or ambiguous archive checksum.' } $expected = ($matchesFound[0] -split '\s+')[0] # Do not depend on PowerShell module autoloading: hosts can inherit a # PowerShell 7 PSModulePath while this helper runs in Windows PowerShell 5.1. $stream = [IO.File]::OpenRead($Archive) $sha = [Security.Cryptography.SHA256]::Create() try { $actual = [BitConverter]::ToString($sha.ComputeHash($stream)).Replace('-','') } finally { $sha.Dispose(); $stream.Dispose() } if ($actual -ne $expected) { throw 'Release checksum verification failed.' } } function Expand-LegacyRelease([string]$Archive,[string]$Destination,[version]$Expected) { Add-Type -AssemblyName System.IO.Compression.FileSystem $zip = [IO.Compression.ZipFile]::OpenRead($Archive) try { $seen = [Collections.Generic.HashSet[string]]::new([StringComparer]::OrdinalIgnoreCase) $total = 0L foreach ($entry in $zip.Entries) { $name = $entry.FullName.Replace('\','/') if ($name.EndsWith('/') -and $name -eq 'dictionaries/') { continue } if ($name -cnotmatch '^(KSwitcher(?:\.(?:Config|Screenshot))?\.exe|LICENSE|json-LICENSE\.txt|README\.md|dictionaries/[a-zA-Z0-9_.-]+)$' -or $name.EndsWith('/.') -or $name.EndsWith('/..') -or $name.EndsWith('.') -or (($entry.ExternalAttributes -shr 16) -band 0xF000) -eq 0xA000 -or !$seen.Add($name)) { throw "Unexpected or unsafe archive entry: $name" } $total += $entry.Length if ($total -gt 100MB -or $zip.Entries.Count -gt 100) { throw 'Release expands beyond allowed limits.' } $destinationFile = Join-Path $Destination $name [IO.Directory]::CreateDirectory([IO.Path]::GetDirectoryName($destinationFile)) | Out-Null [IO.Compression.ZipFileExtensions]::ExtractToFile($entry,$destinationFile,$false) } foreach ($required in @('KSwitcher.exe','LICENSE','json-LICENSE.txt','README.md')) { if (!$seen.Contains($required)) { throw "Incomplete release: $required" } } if ($Expected -gt [version]'0.2.3' -and !$seen.Contains('KSwitcher.Config.exe')) { throw 'Incomplete release: KSwitcher.Config.exe' } } finally { $zip.Dispose() } foreach ($binary in @('KSwitcher.exe','KSwitcher.Config.exe','KSwitcher.Screenshot.exe')) { $exe = Join-Path $Destination $binary if (!(Test-Path -LiteralPath $exe)) { continue } $version = [Diagnostics.FileVersionInfo]::GetVersionInfo($exe) if ($version.ProductName -ne 'KSwitcher' -or (Get-StableVersion $version.FileVersion) -ne $Expected) { throw 'Executable identity/version does not match the release.' } $bytes = [IO.File]::ReadAllBytes($exe) if ($bytes.Length -lt 64 -or $bytes[0] -ne 0x4d -or $bytes[1] -ne 0x5a) { throw 'Invalid release executable.' } $pe = [BitConverter]::ToInt32($bytes,60) if ($pe -lt 64 -or $pe -gt $bytes.Length-6 -or [BitConverter]::ToUInt32($bytes,$pe) -ne 0x4550 -or [BitConverter]::ToUInt16($bytes,$pe+4) -ne 0x8664) { throw 'Release is not a Windows x64 executable.' } } } function Get-Sha256([IO.Stream]$Stream) { $sha=[Security.Cryptography.SHA256]::Create() try { return [BitConverter]::ToString($sha.ComputeHash($Stream)).Replace('-','').ToLowerInvariant() } finally { $sha.Dispose() } } function Assert-PackagePath([string]$Name) { if (!$Name -or $Name.Length -gt 220 -or $Name -match '[\\:<>"|?*]' -or $Name.StartsWith('/')) { throw "Unsafe package path: $Name" } $parts=$Name.Split('/') foreach ($part in $parts) { if (!$part -or $part -in @('.','..') -or $part -match '[. ]$|[\x00-\x1f]' -or $part -match '^(?i:CON|PRN|AUX|NUL|COM[0-9]|LPT[0-9])(?:\.|$)') { throw "Unsafe package path: $Name" } } if ($parts[0] -ieq 'data' -or $parts[0] -like '.kswitcher-*') { throw "Protected user path: $Name" } } function Expand-Release([string]$Archive,[string]$Destination,[version]$Expected) { Add-Type -AssemblyName System.IO.Compression.FileSystem $zip=[IO.Compression.ZipFile]::OpenRead($Archive) try { $manifests=@($zip.Entries | Where-Object FullName -CEQ 'release-files.json') if (!$manifests.Count) { if ($Expected -gt [version]'0.4.2') { throw 'Release manifest is missing.' } $zip.Dispose(); $zip=$null Expand-LegacyRelease $Archive $Destination $Expected return } if ($manifests.Count -ne 1 -or $manifests[0].Length -gt 256KB) { throw 'Invalid release manifest.' } $reader=[IO.StreamReader]::new($manifests[0].Open()) try { $manifest=$reader.ReadToEnd() | ConvertFrom-Json } finally { $reader.Dispose() } if ($manifest.schema -ne 1 -or (Get-StableVersion $manifest.version) -ne $Expected -or @($manifest.files).Count -lt 1 -or @($manifest.files).Count -gt 1000) { throw 'Unsupported or mismatched release manifest.' } $declared=[Collections.Generic.Dictionary[string,object]]::new([StringComparer]::OrdinalIgnoreCase) foreach ($file in $manifest.files) { Assert-PackagePath $file.path if ($file.path -ieq 'release-files.json' -or $declared.ContainsKey($file.path) -or $file.sha256 -notmatch '^[a-fA-F0-9]{64}$' -or $file.size -lt 0 -or $file.size -gt 100MB) { throw 'Invalid manifest file entry.' } $declared.Add($file.path,$file) } if (!$declared.ContainsKey('KSwitcher.exe')) { throw 'Main executable is missing from the manifest.' } $seen=[Collections.Generic.HashSet[string]]::new([StringComparer]::OrdinalIgnoreCase) $total=0L foreach ($entry in $zip.Entries) { $name=$entry.FullName.Replace('\','/') if ($name.EndsWith('/')) { Assert-PackagePath $name.TrimEnd('/'); continue } Assert-PackagePath $name if (!$seen.Add($name) -or (($entry.ExternalAttributes -shr 16) -band 0xF000) -eq 0xA000) { throw 'Duplicate or linked archive entry.' } if ($name -ceq 'release-files.json') { continue } if (!$declared.ContainsKey($name)) { throw "Undeclared package file: $name" } $file=$declared[$name] if ($entry.Length -ne $file.size) { throw "Package file size mismatch: $name" } $total+=$entry.Length if ($total -gt 100MB -or $zip.Entries.Count -gt 1100) { throw 'Release expands beyond allowed limits.' } $stream=$entry.Open() try { $hash=Get-Sha256 $stream } finally { $stream.Dispose() } if ($hash -ne $file.sha256) { throw "Package file checksum mismatch: $name" } } if ($seen.Count -ne $declared.Count+1) { throw 'Archive does not contain every manifest file.' } [IO.Directory]::CreateDirectory($Destination) | Out-Null Assert-PlainDirectory $Destination foreach ($entry in $zip.Entries) { if ($entry.FullName.EndsWith('/')) { continue } $target=Join-Path $Destination $entry.FullName [IO.Directory]::CreateDirectory([IO.Path]::GetDirectoryName($target)) | Out-Null Assert-PlainDirectory ([IO.Path]::GetDirectoryName($target)) [IO.Compression.ZipFileExtensions]::ExtractToFile($entry,$target,$false) } $info=[Diagnostics.FileVersionInfo]::GetVersionInfo((Join-Path $Destination 'KSwitcher.exe')) if ($info.ProductName -ne 'KSwitcher' -or (Get-StableVersion $info.FileVersion) -ne $Expected) { throw 'Main executable identity/version mismatch.' } } finally { if ($zip) { $zip.Dispose() } } } function Assert-PlainDirectory([string]$Directory) { $item = Get-Item -LiteralPath $Directory -Force while ($item) { if ($item.Attributes -band [IO.FileAttributes]::ReparsePoint) { throw 'Update directory must not contain junctions or symbolic links.' } $item = $item.Parent } } function Install-Files([string]$Stage,[string]$Target,[string]$Backup,[Collections.Generic.List[string]]$Changed) { Assert-PlainDirectory $Target foreach ($file in @(Get-ChildItem -LiteralPath $Stage -File -Recurse)) { $relative = $file.FullName.Substring($Stage.Length+1) $destination = Join-Path $Target $relative if ($relative.StartsWith('dictionaries\',[StringComparison]::OrdinalIgnoreCase) -and (Test-Path -LiteralPath $destination)) { continue } [IO.Directory]::CreateDirectory([IO.Path]::GetDirectoryName($destination)) | Out-Null Assert-PlainDirectory ([IO.Path]::GetDirectoryName($destination)) if (Test-Path -LiteralPath $destination) { if ((Get-Item -LiteralPath $destination -Force).Attributes -band [IO.FileAttributes]::ReparsePoint) { throw 'Cannot replace a linked file.' } $backupFile = Join-Path $Backup $relative [IO.Directory]::CreateDirectory([IO.Path]::GetDirectoryName($backupFile)) | Out-Null Move-Item -LiteralPath $destination -Destination $backupFile } $Changed.Add($relative) Move-Item -LiteralPath $file.FullName -Destination $destination } } function Undo-Files([string]$Target,[string]$Backup,[Collections.Generic.List[string]]$Changed) { for ($i=$Changed.Count-1; $i -ge 0; --$i) { $destination = Join-Path $Target $Changed[$i] if (Test-Path -LiteralPath $destination) { Remove-Item -LiteralPath $destination -Force } $original = Join-Path $Backup $Changed[$i] if (Test-Path -LiteralPath $original) { Move-Item -LiteralPath $original -Destination $destination } } } function Write-Result($Value) { $Value | ConvertTo-Json -Compress | Set-Content -LiteralPath (Join-Path $WorkDirectory 'result.json') -Encoding UTF8 } if ($Library) { return } $transaction = $null; $parentProcess = $null; $ready = $null; $started = $null $changed = [Collections.Generic.List[string]]::new() $parentExited = $false; $success = $false; $preserveBackup = $false; $child = $null; $uiFinished = $false try { if ($ShowProgress -or $Mode -eq 'Install') { Start-UpdateProgress } $current = Get-StableVersion $CurrentVersion if (!$current) { throw 'Invalid installed version.' } if ($Mode -eq 'Install') { $parentProcess = Get-Process -Id $ParentProcessId if ([IO.Path]::GetFullPath($parentProcess.Path) -ne [IO.Path]::GetFullPath((Join-Path $InstallDirectory 'KSwitcher.exe'))) { throw 'The running executable does not match the update target.' } $ready = [Threading.EventWaitHandle]::OpenExisting($ReadyEvent) } $metadata = Join-Path $WorkDirectory 'releases.json' Receive-File 'https://gitea.hrynco.com/api/v1/repos/hrynco/hrynco-kswitcher/releases?limit=100' $metadata 2MB $release = Select-Release (Get-Content -LiteralPath $metadata -Raw | ConvertFrom-Json) $current if (!$release) { Write-Result @{state='current'}; exit 0 } if ($Mode -eq 'Check') { Write-Result @{state='available';version=$release.version}; exit 0 } if ($ExpectedVersion -ne $release.version) { throw 'Release changed since checking. Please select Update again.' } Assert-PlainDirectory $InstallDirectory $transaction = Join-Path $InstallDirectory ('.kswitcher-update-'+[guid]::NewGuid().ToString('N')) $stage = Join-Path $transaction 'stage'; $backup = Join-Path $transaction 'backup' [IO.Directory]::CreateDirectory($stage) | Out-Null $archive = Join-Path $WorkDirectory 'release.zip'; $checksum = Join-Path $WorkDirectory 'SHA256SUMS.txt' Show-UpdateProgress 'Downloading update' $release.version 0 Receive-File $release.zip $archive 50MB 'Downloading update' Show-UpdateProgress 'Verifying download' 'Checking the archive SHA-256 checksum' Receive-File $release.checksum $checksum 64KB Assert-Checksum $archive $checksum Show-UpdateProgress 'Preparing installation' 'Validating and unpacking the release files' Expand-Release $archive $stage (Get-StableVersion $release.version) $screenshotPath = [IO.Path]::GetFullPath((Join-Path $InstallDirectory 'KSwitcher.Screenshot.exe')) if (Get-Process 'KSwitcher.Screenshot' -ErrorAction SilentlyContinue | Where-Object { $_.Path -eq $screenshotPath }) { throw 'Close screenshot editors before installing an update.' } Write-Result @{state='prepared';version=$release.version} Show-UpdateProgress 'Closing KSwitcher' 'Waiting for the running application to exit safely' $ready.Set() | Out-Null if (!$parentProcess.WaitForExit(120000)) { throw 'KSwitcher did not exit; the installed files were not changed.' } $parentExited = $true Show-UpdateProgress 'Installing update' 'Replacing application files; keeping your personal data' Install-Files $stage $InstallDirectory $backup $changed $startedName = 'Local\KSwitcher.Update.Started.'+[guid]::NewGuid().ToString('N') $started = [Threading.EventWaitHandle]::new($false,[Threading.EventResetMode]::ManualReset,$startedName) Show-UpdateProgress 'Restarting KSwitcher' 'Waiting for the updated application to confirm startup' $child = Start-Process -FilePath (Join-Path $InstallDirectory 'KSwitcher.exe') -ArgumentList "--update-ready-event=$startedName" -WorkingDirectory $InstallDirectory -WindowStyle Hidden -PassThru if (!$started.WaitOne(30000) -or $child.HasExited) { throw 'The updated application did not confirm startup.' } $success = $true if ($script:progressWindow) { $uiFinished = Complete-UpdateProgress 'Update completed' "KSwitcher $(Get-StableVersion $release.version) is now running." $true } } catch { $message = $_.Exception.Message if ($parentExited) { Show-UpdateProgress 'Restoring previous version' 'The update failed; recovering the previous installation' try { if ($child -and !$child.HasExited) { $child.Kill(); $child.WaitForExit() } Undo-Files $InstallDirectory $backup $changed Start-Process -FilePath (Join-Path $InstallDirectory 'KSwitcher.exe') -WorkingDirectory $InstallDirectory -WindowStyle Hidden $message += "`nThe previous version has been restored and restarted." } catch { $preserveBackup = $true $message += "`nRecovery needs attention: $($_.Exception.Message)`nBackup: $backup" } } Write-Result @{state='error';error=$message} if ($ready -and $ready.WaitOne(0)) { if ($script:progressWindow) { $uiFinished = Complete-UpdateProgress 'Update failed' $message $false } else { Add-Type -AssemblyName System.Windows.Forms [Windows.Forms.MessageBox]::Show($message,'KSwitcher update failed') | Out-Null } } exit 1 } finally { if ($ready) { $ready.Dispose() } if ($started) { $started.Dispose() } if ($parentProcess) { $parentProcess.Dispose() } # Only remove the uniquely created transaction directory directly under the install directory. if ($transaction -and !$preserveBackup -and [IO.Path]::GetDirectoryName($transaction) -eq [IO.Path]::GetFullPath($InstallDirectory).TrimEnd('\') -and (Test-Path -LiteralPath $transaction)) { Remove-Item -LiteralPath $transaction -Recurse -Force -ErrorAction SilentlyContinue } if ($success) { # The script is loaded in memory; its unique temporary working directory is no longer needed. if ((Split-Path -Leaf $WorkDirectory) -like 'KSwitcher-update-*' -and [IO.Path]::GetDirectoryName($WorkDirectory) -eq [IO.Path]::GetTempPath().TrimEnd('\')) { Remove-Item -LiteralPath $WorkDirectory -Recurse -Force -ErrorAction SilentlyContinue } } if ($script:progressWindow) { if ($uiFinished) { $script:progressWindow.WaitForClose() } $script:progressWindow.Dispose(); $script:progressWindow = $null } }