#Requires -Version 5.1 <# wandlet installer. irm https://wandlet.app/install.ps1 | iex Reads the same manifest the app's own updater uses, so this script never carries a version number of its own. The app installs per-user, so no administrator rights are needed. No signature check here on purpose: this script and any key it could fetch come from the same host, so verifying one against the other would prove nothing. HTTPS to wandlet.app is the trust boundary. Every UPDATE after this install is minisign-verified by the app, against a key pinned at compile time -- see docs/releasing.md. #> function Install-Wandlet { # Scoped to this function on purpose, not top-level: this script runs via # `irm ... | iex`, which executes in the CALLER's scope -- the exact same # reason `exit` is never called below. A top-level assignment here would # leak into the user's PowerShell session and outlive the install, # silently turning any later non-terminating error in that window into a # terminating one. $ErrorActionPreference = 'Stop' $manifestUrl = 'https://wandlet.app/updates/latest.json' $appExe = Join-Path $env:LOCALAPPDATA 'wandlet\wandlet.exe' # --- preconditions ---------------------------------------------------- # $IsWindows only exists in PowerShell 6+; on 5.1 it is $null, so the # $env:OS half of this test is what answers on Windows PowerShell. if (-not $IsWindows -and $env:OS -ne 'Windows_NT') { throw 'this installer is for Windows only.' } if (-not [Environment]::Is64BitOperatingSystem) { throw 'wandlet needs 64-bit Windows.' } # Windows PowerShell 5.1 does not always negotiate TLS 1.2 by default. try { [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 } catch { } # --- which version is current? ---------------------------------------- Write-Host ' checking for the latest version...' try { $manifest = Invoke-RestMethod -Uri $manifestUrl -TimeoutSec 30 } catch { throw "could not reach $manifestUrl -- $($_.Exception.Message)" } $version = $manifest.version $url = $manifest.platforms.'windows-x86_64'.url if (-not $version -or -not $url) { throw 'the update manifest has no windows-x86_64 build.' } if ($url -notlike 'https://*') { throw "the manifest's download url is not https: $url" } Write-Host " found v$version" $tmp = Join-Path $env:TEMP "wandlet-$version-setup.exe" $keepInstaller = $false $stoppedRunning = $false try { # --- download ------------------------------------------------------ Write-Host ' downloading...' # Progress rendering makes Invoke-WebRequest many times slower on 5.1. $oldProgress = $ProgressPreference $ProgressPreference = 'SilentlyContinue' try { Invoke-WebRequest -Uri $url -OutFile $tmp -TimeoutSec 300 } catch { throw "download failed -- $($_.Exception.Message)" } finally { $ProgressPreference = $oldProgress } # The download worked. From here on, ANY failure must leave the # installer behind -- it is the only thing the user can retry with. $keepInstaller = $true # --- close a running copy ------------------------------------------- # Windows locks a running .exe, so installing over it fails. Say this # out loud: wandlet sits in the tray and owns a global keyboard # hook, so closing it without a word would be a surprise. $running = Get-Process -Name 'wandlet' -ErrorAction SilentlyContinue if ($running) { Write-Host ' wandlet is running -- closing it, and starting it again after the install' $running | Stop-Process -Force Start-Sleep -Milliseconds 800 $stoppedRunning = $true } # --- install --------------------------------------------------------- Write-Host ' installing...' $proc = Start-Process -FilePath $tmp -ArgumentList '/S' -Wait -PassThru # ExitCode can be $null (e.g. when the launched process re-launches # itself elevated) -- and `$null -ne 0` is $true, so without the # explicit null check a successful install would be reported as a # failure here. if ($null -ne $proc.ExitCode -and $proc.ExitCode -ne 0) { throw "the installer exited with code $($proc.ExitCode)." } # Installed cleanly, so the temp copy is redundant now. $keepInstaller = $false } catch { # Say what state the machine was left in. Losing the installer AND the # running app with no explanation is the worst outcome here. $detail = $_.Exception.Message if ($keepInstaller) { $detail += "`n The installer was kept at: $tmp`n Run it by hand to see what it says." } if ($stoppedRunning) { $detail += "`n wandlet was closed for this install and is NOT running now." } throw $detail } finally { if (-not $keepInstaller -and (Test-Path $tmp)) { Remove-Item $tmp -Force -ErrorAction SilentlyContinue } } # --- start it ----------------------------------------------------------- if (-not (Test-Path $appExe)) { throw "the install finished but $appExe is missing." } try { Start-Process -FilePath $appExe } catch { throw "installed v$version, but it would not start -- $($_.Exception.Message). Open it from the Start menu." } return $version } Write-Host '' Write-Host ' wandlet installer' -ForegroundColor Cyan Write-Host '' try { # NOTE: never call `exit` in this script. It runs via `irm ... | iex`, which # executes in the CALLER's scope -- `exit` would close the user's PowerShell # window instead of just ending the install. $installed = Install-Wandlet Write-Host '' Write-Host " installed wandlet v$installed" -ForegroundColor Green Write-Host ' It lives in the system tray. Select text anywhere, then double-tap Shift.' Write-Host '' } catch { Write-Host '' Write-Host " install failed: $($_.Exception.Message)" -ForegroundColor Red Write-Host '' }