# Croft Plus for desktop - installer for Windows 10/11 (Windows PowerShell 5.1 or PowerShell 7). No administrator rights needed. # # irm /install.ps1 | iex install or update (settings via $env:CROFTPLUSDESK_* below) # & ([scriptblock]::Create((irm /install.ps1))) -Uninstall remove it (your messages and key are kept) # & ([scriptblock]::Create((irm /install.ps1))) -Uninstall -Purge -Yes remove everything, data included # # (An .msi built from tools/desktop/croftplusdesk.wxs does the same install without PowerShell.) # Safe to run again at any time: same inputs give the same result, nothing is restarted unless something changed, nothing is # duplicated, and your data (messages, pairing key) is never touched except by -Purge. # # Settings (parameters, or the same names as $env:CROFTPLUSDESK_VERSION, $env:CROFTPLUSDESK_BASE_URL, $env:CROFTPLUSDESK_FROM_DIR, $env:CROFTPLUSDESK_NO_PATH=1 ...): # -Version V -BaseUrl URL -FromDir DIR (use release files from a folder) -NoPath (do not touch PATH) -NoStart # -NoAutostart (do not register the login task) -RequireSignature -DryRun -Uninstall -Purge -Yes -Status # -KeepRecall (leave Windows Recall alone; by default it is switched off) [CmdletBinding()] param( [string]$Version = $env:CROFTPLUSDESK_VERSION, [string]$BaseUrl = $env:CROFTPLUSDESK_BASE_URL, [string]$FromDir = $env:CROFTPLUSDESK_FROM_DIR, [switch]$NoPath = ($env:CROFTPLUSDESK_NO_PATH -eq '1'), [switch]$NoStart = ($env:CROFTPLUSDESK_NO_START -eq '1'), [switch]$NoAutostart = ($env:CROFTPLUSDESK_NO_AUTOSTART -eq '1'), [switch]$KeepRecall = ($env:CROFTPLUSDESK_KEEP_RECALL -eq '1'), [switch]$RequireSignature = ($env:CROFTPLUSDESK_REQUIRE_SIGNATURE -eq '1'), [switch]$DryRun = ($env:CROFTPLUSDESK_DRY_RUN -eq '1'), [switch]$Uninstall, [switch]$Purge, [switch]$Yes, [switch]$Status ) $PubKeyB64 = 'uvKUp9LZLmhZsWJG/7bCOFtYnrJLmKP65Sp1qVvPHvs=' $DefaultVersion = '1.14.0' $DefaultBaseUrl = 'https://get.croftmessaging.com/desktop' if (-not $BaseUrl -and $DefaultBaseUrl -notlike '@*') { $BaseUrl = $DefaultBaseUrl } $ErrorActionPreference = 'Stop' $ProgressPreference = 'SilentlyContinue' # Windows PowerShell 5.1 downloads are many times slower with the progress bar $TaskName = 'CroftMessaging Desktop' $FirewallName = 'CroftMessaging Desktop (phone link)' $Arch = 'x86_64' function Say($m) { Write-Host $m } function Note($m) { Write-Host " $m" } function Warn($m) { Write-Warning $m } function Fail($m) { throw "Croft Plus installer: $m" } # Ed25519 verify in plain PowerShell, since Windows has no built-in. Takes about a second. # Checks that SHA256SUMS was signed by the release key embedded above. Slow-ish (about a second), runs once per install. function Test-Ed25519 { param([byte[]]$PublicKey, [byte[]]$Message, [byte[]]$Signature) Add-Type -AssemblyName System.Numerics $BI = [System.Numerics.BigInteger] function LE([byte[]]$b) { $BI::new([byte[]]($b + [byte]0)) } function Mod([System.Numerics.BigInteger]$a, [System.Numerics.BigInteger]$m) { $r = $BI::Remainder($a, $m); if ($r.Sign -lt 0) { $r + $m } else { $r } } $p = $BI::Pow(2, 255) - 19 $L = $BI::Pow(2, 252) + $BI::Parse('27742317777372353535851937790883648493') function Inv($a) { $BI::ModPow((Mod $a $p), $p - 2, $p) } $d = Mod ($BI::MinusOne * 121665 * (Inv 121666)) $p $d2 = Mod (2 * $d) $p function Add-Pt($P1, $P2) { $A = Mod (($P1[1] - $P1[0]) * ($P2[1] - $P2[0])) $p $B = Mod (($P1[1] + $P1[0]) * ($P2[1] + $P2[0])) $p $C = Mod ($P1[3] * $d2 * $P2[3]) $p $D = Mod (2 * $P1[2] * $P2[2]) $p $E = $B - $A; $F = $D - $C; $G = $D + $C; $H = $B + $A , @((Mod ($E * $F) $p), (Mod ($G * $H) $p), (Mod ($F * $G) $p), (Mod ($E * $H) $p)) } function Mul-Pt([System.Numerics.BigInteger]$k, $P1) { $R = , @([System.Numerics.BigInteger]0, [System.Numerics.BigInteger]1, [System.Numerics.BigInteger]1, [System.Numerics.BigInteger]0) $R = $R[0]; $Q = $P1 while ($k.Sign -gt 0) { if (-not $k.IsEven) { $R = Add-Pt $R $Q } $Q = Add-Pt $Q $Q $k = $k -shr 1 } , $R } function Decode-Pt([byte[]]$b) { $y = LE $b; $sign = ($y -shr 255) -band 1; $y = $y -band ($BI::Pow(2, 255) - 1) if ($y -ge $p) { return $null } $y2 = Mod ($y * $y) $p $u = Mod ($y2 - 1) $p; $v = Mod ($d * $y2 + 1) $p $x = $BI::ModPow((Mod ($u * (Inv $v)) $p), ($p + 3) / 8, $p) if ((Mod ($x * $x * $v - $u) $p) -ne 0) { $x = Mod ($x * $BI::ModPow(2, ($p - 1) / 4, $p)) $p } if ((Mod ($x * $x * $v - $u) $p) -ne 0) { return $null } if (($x -band 1) -ne $sign) { $x = Mod ($p - $x) $p } if ($x.IsZero -and $sign -eq 1) { return $null } , @($x, $y, [System.Numerics.BigInteger]1, (Mod ($x * $y) $p)) } if ($PublicKey.Length -ne 32 -or $Signature.Length -ne 64) { return $false } $A = Decode-Pt $PublicKey $R = Decode-Pt $Signature[0..31] if ($null -eq $A -or $null -eq $R) { return $false } $S = LE $Signature[32..63] if ($S -ge $L) { return $false } $sha = [System.Security.Cryptography.SHA512]::Create() $h = Mod (LE $sha.ComputeHash([byte[]]($Signature[0..31] + $PublicKey + $Message))) $L $By = Mod (4 * (Inv 5)) $p $B0 = Decode-Pt ([byte[]]((($By).ToByteArray() + (1..32 | ForEach-Object { [byte]0 }))[0..31])) $lhs = Mul-Pt $S $B0 $rhs = Add-Pt $R (Mul-Pt $h $A) # equal as projective points: X1*Z2 == X2*Z1 and Y1*Z2 == Y2*Z1 return ((Mod ($lhs[0] * $rhs[2] - $rhs[0] * $lhs[2]) $p).IsZero -and (Mod ($lhs[1] * $rhs[2] - $rhs[1] * $lhs[2]) $p).IsZero) } function Get-Paths { $root = $env:CROFTPLUSDESK_HOME if (-not $root) { $root = Join-Path $env:LOCALAPPDATA 'CroftMessaging\Desktop' } $programs = [Environment]::GetFolderPath('Programs'); if (-not $programs) { $programs = $root } $startup = [Environment]::GetFolderPath('Startup'); if (-not $startup) { $startup = $root } [pscustomobject]@{ Data = $root Bin = Join-Path $root 'bin' Exe = Join-Path $root 'bin\croftplusdesk.exe' ExeW = Join-Path $root 'bin\croftplusdeskw.exe' StartMenu = Join-Path $programs 'CroftMessaging.lnk' Startup = Join-Path $startup 'CroftMessaging.lnk' } } function Is-Windows { return ($env:OS -eq 'Windows_NT') } function Get-Running($paths) { # only OUR copies (by path), never some other program with the same name Get-Process -Name croftplusdesk, croftplusdeskw -ErrorAction SilentlyContinue | Where-Object { try { $_.Path -like "$($paths.Bin)*" } catch { $false } } } function Stop-Ours($paths) { if (Test-Path $paths.Exe) { try { & $paths.Exe stop 2>&1 | Out-Null } catch { } } $left = Get-Running $paths if ($left) { $left | Stop-Process -Force -ErrorAction SilentlyContinue; Start-Sleep -Milliseconds 400 } } function Get-Release($name, $dest, [switch]$Optional) { try { if ($FromDir) { $src = Join-Path $FromDir $name if (-not (Test-Path $src)) { if ($Optional) { return $false }; Fail "$name is not in $FromDir" } Copy-Item $src $dest -Force } else { [Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12 $u = if ($BaseUrl -like '*{file}*') { $BaseUrl.Replace('{file}', $name) } else { $BaseUrl.TrimEnd('/') + '/' + $name } Invoke-WebRequest -Uri $u -OutFile $dest -UseBasicParsing } return $true } catch { if ($Optional) { return $false } Fail "cannot get $name ($($_.Exception.Message))" } } function Obtain-Release { if (-not $FromDir -and -not $BaseUrl) { Fail 'no download address: pass -BaseUrl URL (or -FromDir DIR)' } $tmp = Join-Path ([IO.Path]::GetTempPath()) ('croftplusdesk-' + [guid]::NewGuid().ToString('N')) New-Item -ItemType Directory -Path $tmp | Out-Null $script:Tmp = $tmp if (-not $Version) { if (Get-Release 'latest' (Join-Path $tmp 'latest') -Optional) { $script:Version = (Get-Content (Join-Path $tmp 'latest') -Raw).Trim() } else { $script:Version = $DefaultVersion } } if ($Version -notmatch '^[0-9A-Za-z._-]+$' -or $Version -like '@*') { Fail "cannot work out which version to install; pass -Version ($Version)" } $archive = "croftplusdesk-$Version-windows-$Arch.zip" Say "Installing Croft Plus desktop $Version (Windows $Arch)" Get-Release 'SHA256SUMS' (Join-Path $tmp 'SHA256SUMS') | Out-Null $haveSig = Get-Release 'SHA256SUMS.sig' (Join-Path $tmp 'SHA256SUMS.sig') -Optional $verdict = 'unchecked' if ($haveSig -and $PubKeyB64 -notlike '@*') { try { $sig = [Convert]::FromBase64String((Get-Content (Join-Path $tmp 'SHA256SUMS.sig') -Raw).Trim()) $pub = [Convert]::FromBase64String($PubKeyB64) $msg = [IO.File]::ReadAllBytes((Join-Path $tmp 'SHA256SUMS')) $verdict = if (Test-Ed25519 -PublicKey $pub -Message $msg -Signature $sig) { 'ok' } else { 'WRONG' } } catch { $verdict = 'unchecked' } } if ($verdict -eq 'WRONG') { Fail 'the release signature is WRONG. Nothing was installed. (Wrong or tampered files; do not continue.)' } if ($verdict -eq 'ok') { Note 'release signature: OK' } elseif ($RequireSignature) { Fail 'could not check the release signature and -RequireSignature was given' } else { Warn 'release signature not checked; relying on the download address being genuine and the SHA-256 list' } $want = $null foreach ($line in (Get-Content (Join-Path $tmp 'SHA256SUMS'))) { $parts = $line -split '\s+', 2 if ($parts.Count -eq 2 -and ($parts[1].TrimStart('*') -eq $archive)) { $want = $parts[0].ToLower() } } if (-not $want) { Fail "no Windows build in release $Version" } Get-Release $archive (Join-Path $tmp $archive) | Out-Null $got = (Get-FileHash -Algorithm SHA256 (Join-Path $tmp $archive)).Hash.ToLower() if ($got -ne $want) { Fail "checksum mismatch for $archive (expected $want, got $got). Nothing was installed." } Note 'checksum: OK' $x = Join-Path $tmp 'x' Expand-Archive -Path (Join-Path $tmp $archive) -DestinationPath $x -Force foreach ($f in 'croftplusdesk.exe', 'croftplusdeskw.exe') { if (-not (Test-Path (Join-Path $x $f))) { Fail "the archive has no $f" } } $script:Unpacked = $x } function Files-Equal($a, $b) { if (-not (Test-Path $a) -or -not (Test-Path $b)) { return $false } return ((Get-FileHash -Algorithm SHA256 $a).Hash -eq (Get-FileHash -Algorithm SHA256 $b).Hash) } function Install-Autostart($paths) { $ok = $false try { $action = New-ScheduledTaskAction -Execute $paths.ExeW -Argument 'run' $trigger = New-ScheduledTaskTrigger -AtLogOn -User $env:USERNAME $settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -StartWhenAvailable -ExecutionTimeLimit ([TimeSpan]::Zero) -RestartCount 3 -RestartInterval (New-TimeSpan -Minutes 1) $principal = New-ScheduledTaskPrincipal -UserId $env:USERNAME -LogonType Interactive -RunLevel Limited Register-ScheduledTask -TaskName $TaskName -Action $action -Trigger $trigger -Settings $settings -Principal $principal -Description 'Croft Plus desktop link' -Force | Out-Null $ok = $true # a Startup-folder shortcut from an earlier fallback would start a second copy if (Test-Path $paths.Startup) { Remove-Item $paths.Startup -Force } } catch { Warn "could not register the login task ($($_.Exception.Message)); using a Startup-folder shortcut instead" } if (-not $ok) { $sh = New-Object -ComObject WScript.Shell $l = $sh.CreateShortcut($paths.Startup); $l.TargetPath = $paths.ExeW; $l.Arguments = 'run'; $l.WorkingDirectory = $paths.Bin; $l.Save() } } function Install-StartMenu($paths) { try { $sh = New-Object -ComObject WScript.Shell $l = $sh.CreateShortcut($paths.StartMenu); $l.TargetPath = $paths.ExeW; $l.Arguments = 'open'; $l.WorkingDirectory = $paths.Bin $l.Description = 'Open Croft Plus'; $l.Save() } catch { Warn "could not create the Start menu shortcut ($($_.Exception.Message))" } } function Set-UserPath($paths, [bool]$Add) { $cur = [Environment]::GetEnvironmentVariable('Path', 'User'); if (-not $cur) { $cur = '' } $items = @($cur -split ';' | Where-Object { $_ }) $has = $items | Where-Object { $_.TrimEnd('\') -ieq $paths.Bin.TrimEnd('\') } if ($Add -and -not $has) { if (-not $DryRun) { [Environment]::SetEnvironmentVariable('Path', (($items + $paths.Bin) -join ';'), 'User') } Note "added $($paths.Bin) to your PATH (open a new terminal)" } elseif (-not $Add -and $has) { if (-not $DryRun) { [Environment]::SetEnvironmentVariable('Path', (($items | Where-Object { $_.TrimEnd('\') -ine $paths.Bin.TrimEnd('\') }) -join ';'), 'User') } } } function Test-Admin { try { return ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) } catch { return $false } } function Install-Firewall($paths) { if (-not (Is-Windows)) { return } $existing = Get-NetFirewallRule -DisplayName $FirewallName -ErrorAction SilentlyContinue if ($existing) { return } if (Test-Admin) { New-NetFirewallRule -DisplayName $FirewallName -Direction Inbound -Program $paths.ExeW -Profile Private -Action Allow -Protocol TCP | Out-Null Note 'firewall: allowed the phone link on private networks' } else { Note 'Windows may ask to allow Croft Plus on your private network the first time your phone connects: choose Allow.' } } # Recall: switched off unless -KeepRecall is given. Writes the DisableAIDataAnalysis policy for this user, and machine-wide when elevated. # Recall takes screenshots of everything on screen, including messages. This writes the documented policy value # WindowsAI\DisableAIDataAnalysis = 1: always for the current user (no admin needed), and machine-wide plus the optional feature # when the script runs elevated. It never fails the install. Undo commands are printed; -Uninstall does NOT change this back. $RecallPolicyKey = 'Software\Policies\Microsoft\Windows\WindowsAI' $RecallUndo = @( "Remove-ItemProperty -Path 'HKCU:\$RecallPolicyKey' -Name DisableAIDataAnalysis", "Remove-ItemProperty -Path 'HKLM:\$RecallPolicyKey' -Name DisableAIDataAnalysis (administrator PowerShell)", "Enable-WindowsOptionalFeature -Online -FeatureName Recall -NoRestart (administrator PowerShell)") function Get-RecallValue([string]$Hive) { try { $v = (Get-ItemProperty -Path "${Hive}:\$RecallPolicyKey" -Name DisableAIDataAnalysis -ErrorAction Stop).DisableAIDataAnalysis; return $v } catch { return $null } } function Set-RecallPolicy([string]$Hive) { $path = "${Hive}:\$RecallPolicyKey" if (-not (Test-Path $path)) { New-Item -Path $path -Force | Out-Null } New-ItemProperty -Path $path -Name DisableAIDataAnalysis -Value 1 -PropertyType DWord -Force | Out-Null } function Disable-Recall { if (-not (Is-Windows)) { return } if ($KeepRecall) { Note 'Recall: left as it is (-KeepRecall).'; return } if ($DryRun) { Say ' [dry-run] would turn Windows Recall / AI data analysis off (policy DisableAIDataAnalysis=1)'; return } Say 'Turning off Windows Recall (it screenshots everything on screen, including your messages):' try { Set-RecallPolicy 'HKCU'; Note ("set HKCU:\$RecallPolicyKey DisableAIDataAnalysis = 1 (this user)") } catch { Warn "could not set the per-user Recall policy ($($_.Exception.Message))" } if (Test-Admin) { try { Set-RecallPolicy 'HKLM'; Note ("set HKLM:\$RecallPolicyKey DisableAIDataAnalysis = 1 (all users)") } catch { Warn "could not set the machine Recall policy ($($_.Exception.Message))" } try { $f = Get-WindowsOptionalFeature -Online -FeatureName Recall -ErrorAction Stop if ($f -and $f.State -ne 'Disabled') { Disable-WindowsOptionalFeature -Online -FeatureName Recall -NoRestart -ErrorAction Stop | Out-Null; Note 'disabled the Windows optional feature "Recall" (a restart may be needed)' } elseif ($f) { Note 'the Windows optional feature "Recall" is already disabled' } } catch { Note 'no "Recall" optional feature on this Windows (nothing to disable there)' } } else { Note 'not running as administrator: only this user was covered. For all users and the optional feature, run this installer once from an administrator PowerShell.' } Note 'To undo, run:' foreach ($u in $RecallUndo) { Note " $u" } Note 'Pass -KeepRecall to skip this step.' } function Test-Recall { if (-not (Is-Windows) -or $KeepRecall -or $DryRun) { return } $u = Get-RecallValue 'HKCU' Say (" Recall check (this user): " + $(if ($u -eq 1) { 'OK, off' } else { 'NOT SET' })) if (Test-Admin) { $m = Get-RecallValue 'HKLM' Say (" Recall check (all users): " + $(if ($m -eq 1) { 'OK, off' } else { 'NOT SET' })) } } function Do-Install { $paths = Get-Paths Obtain-Release Say "Setting up: program $($paths.Bin), data $($paths.Data)" $changed = (-not (Files-Equal (Join-Path $script:Unpacked 'croftplusdesk.exe') $paths.Exe)) -or (-not (Files-Equal (Join-Path $script:Unpacked 'croftplusdeskw.exe') $paths.ExeW)) $wasRunning = [bool](Get-Running $paths) if ($DryRun) { Say " [dry-run] would install into $($paths.Bin) (changed: $changed) and register the login task"; return } New-Item -ItemType Directory -Path $paths.Bin -Force | Out-Null if ($changed) { Stop-Ours $paths foreach ($f in 'croftplusdesk.exe', 'croftplusdeskw.exe') { $dest = Join-Path $paths.Bin $f; $new = "$dest.new" Copy-Item (Join-Path $script:Unpacked $f) $new -Force Move-Item $new $dest -Force } Note "installed $($paths.Exe)" } else { Note "program already up to date: $($paths.Exe)" } $out = & $paths.Exe version 2>&1 if ($LASTEXITCODE -ne 0) { Fail "the installed program does not run on this machine: $out" } $conf = "base_url=$BaseUrl`r`nversion=$($out -replace '^croftplusdesk ','')`r`n" $confPath = Join-Path $paths.Data 'install.conf' if (-not (Test-Path $confPath) -or ((Get-Content $confPath -Raw) -ne $conf)) { Set-Content -Path $confPath -Value $conf -NoNewline -Encoding ASCII } if (-not $NoAutostart -and (Is-Windows)) { Install-Autostart $paths } if (Is-Windows) { Install-StartMenu $paths; Install-Firewall $paths } if (-not $NoPath -and (Is-Windows)) { Set-UserPath $paths $true } try { Disable-Recall } catch { Warn "could not turn Recall off ($($_.Exception.Message))" } if ($NoStart) { Note 'not started (-NoStart)' } elseif ($wasRunning -and -not $changed) { Note 'already running; nothing to restart' } else { & $paths.ExeW start 2>&1 | Out-Null $up = $false for ($i = 0; $i -lt 30; $i++) { & $paths.Exe status 2>&1 | Out-Null; if ($LASTEXITCODE -eq 0) { $up = $true; break }; Start-Sleep -Milliseconds 500 } if (-not $up) { Fail "the program did not start. See: $(Join-Path $paths.Data 'daemon.log'). Run '$($paths.Exe) run' by hand to see the error." } Note 'running' } Say '' Say 'Done.' try { Test-Recall } catch { Warn "Recall check failed ($($_.Exception.Message))" } # the setup window does the rest: linking the phone, nothing to type $opened = $false if (-not $NoStart -and $env:CROFTPLUSDESK_NO_SETUP -ne '1') { Say ' Opening Croft Plus Setup in its own window...' try { Start-Process -FilePath $paths.Exe -ArgumentList 'setup' -WindowStyle Hidden; $opened = $true } catch { Warn "Could not open the setup window ($($_.Exception.Message))" } } if (-not $opened) { Say ' Next: croftplusdesk pair (shows a code to link your phone)' Say ' croftplusdesk open (opens your messages; also in the Start menu as Croft Plus)' } if ($env:Path -notlike "*$($paths.Bin)*") { Say " (Open a new terminal for 'croftplusdesk' to be found, or run $($paths.Exe).)" } Cleanup } function Do-Uninstall { $paths = Get-Paths Say "Removing Croft Plus desktop (program and login task$(if ($Purge) { ', and ALL data' }))" if ($Purge -and -not $Yes) { $a = Read-Host "This permanently deletes the message copy and pairing key in $($paths.Data). Type yes to continue" if ($a -ne 'yes') { Fail 'not purged' } } if ($DryRun) { Say ' [dry-run] nothing changed'; return } if ($Purge -and (Test-Path $paths.Exe)) { try { & $paths.Exe unlink 2>&1 | Out-Null } catch { } } # tell a linked phone while we still can Stop-Ours $paths if (Is-Windows) { try { Unregister-ScheduledTask -TaskName $TaskName -Confirm:$false -ErrorAction SilentlyContinue } catch { } try { Get-NetFirewallRule -DisplayName $FirewallName -ErrorAction SilentlyContinue | Remove-NetFirewallRule -ErrorAction SilentlyContinue } catch { } foreach ($l in $paths.StartMenu, $paths.Startup) { if (Test-Path $l) { Remove-Item $l -Force } } Set-UserPath $paths $false } if (Test-Path $paths.Bin) { Remove-Item $paths.Bin -Recurse -Force } Remove-Item (Join-Path $paths.Data 'install.conf') -Force -ErrorAction SilentlyContinue if ($Purge) { if (Test-Path $paths.Data) { Remove-Item $paths.Data -Recurse -Force }; Say 'Data deleted.' } else { Say "Your messages and pairing key are kept in $($paths.Data) (use -Purge to delete them)." } if ((Is-Windows) -and -not $KeepRecall) { Say 'Windows Recall was turned off at install time and is NOT turned back on by this uninstall. To turn it back on, run:' foreach ($u in $RecallUndo) { Say " $u" } } Say 'Done.' } function Do-Status { $paths = Get-Paths if (Test-Path $paths.Exe) { Say ("program: " + (& $paths.Exe version 2>&1) + " ($($paths.Exe))") } else { Say 'program: not installed' } Say "data: $($paths.Data)" if (Test-Path $paths.Exe) { & $paths.Exe status } } function Cleanup { if ($script:Tmp -and (Test-Path $script:Tmp)) { Remove-Item $script:Tmp -Recurse -Force -ErrorAction SilentlyContinue } } try { if ($Status) { Do-Status } elseif ($Uninstall) { Do-Uninstall } else { Do-Install } } finally { Cleanup }