Skip to content

Commit 31f9656

Browse files
authored
Update install.ps1
1 parent b8fa15d commit 31f9656

1 file changed

Lines changed: 126 additions & 89 deletions

File tree

install.ps1

Lines changed: 126 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,25 @@
55
param(
66
[string]$InstallRoot = (Join-Path $env:LOCALAPPDATA 'TailscaleControl'),
77
[string]$SourceDirectory,
8-
[string]$ReleaseTag = 'latest',
8+
[string]$ReleaseTag,
99
[string]$ReleaseAssetBase,
1010
[switch]$PreferRemote,
11-
[switch]$SkipHashCheck,
1211
[switch]$NoLaunch
1312
)
1413

1514
Set-StrictMode -Version Latest
1615
$ErrorActionPreference = 'Stop'
16+
$ProgressPreference = 'SilentlyContinue'
17+
18+
$DefaultReleaseTag = 'v1.0.0'
19+
20+
if ([string]::IsNullOrWhiteSpace($DefaultReleaseTag) -or $DefaultReleaseTag -eq '__RELEASE_TAG__') {
21+
throw 'Default release tag was not filled. Run the GitHub Action before publishing install.ps1.'
22+
}
23+
24+
if ([string]::IsNullOrWhiteSpace([string]$ReleaseTag)) {
25+
$ReleaseTag = $DefaultReleaseTag
26+
}
1727

1828
$script:AppName = 'Tailscale Control'
1929
$script:AppVersion = '1.0.0'
@@ -28,28 +38,32 @@ $script:InstalledIconPath = Join-Path $script:InstalledIconsDir 'tailscale-contr
2838
$script:LauncherVbsPath = Join-Path $InstallRoot 'TailscaleControlLauncher.vbs'
2939
$script:StartMenuShortcutPath = Join-Path ([Environment]::GetFolderPath('Programs')) 'Tailscale Control.lnk'
3040
$script:GitHubReleasesBase = 'https://github.com/luizbizzio/tailscale-control/releases'
31-
$script:ExpectedScriptSha256 = '97ea5df2ef1a2089d1e50804112f53597e861b95e69a5d12ff6a613e2ecc71c2'
3241
$script:IconAssets = @(
3342
[pscustomobject]@{
3443
FileName = 'tailscale-control.ico'
3544
RelativePath = 'assets/icons/tailscale-control.ico'
36-
ExpectedSha256 = '2c2c83a52aafd6def4c074fd3127d7de9f414453e8b92db30b4d1d11c1ea0e3a'
3745
},
3846
[pscustomobject]@{
3947
FileName = 'tailscale.ico'
4048
RelativePath = 'assets/icons/tailscale.ico'
41-
ExpectedSha256 = '1eff7d0ee72515e6bfd9a3301900c23a288b389e202c55a51fdd735243c411f0'
4249
},
4350
[pscustomobject]@{
4451
FileName = 'tailscale-mtu.ico'
4552
RelativePath = 'assets/icons/tailscale-mtu.ico'
46-
ExpectedSha256 = '01e024c88d947c4f40e9659498a2ae6ed10ed970a9130806000e2ffd17fcf0b5'
4753
}
4854
)
4955

5056
function Write-Step {
51-
param([string]$Message)
52-
Write-Host ('[{0}] {1}' -f (Get-Date).ToString('HH:mm:ss'), $Message)
57+
param(
58+
[string]$Message,
59+
[ConsoleColor]$Color = [ConsoleColor]::Cyan
60+
)
61+
62+
try {
63+
Write-Host "[$((Get-Date).ToString('HH:mm:ss'))] $Message" -ForegroundColor $Color
64+
} catch {
65+
Write-Output $Message
66+
}
5367
}
5468

5569
function Initialize-Directory {
@@ -59,89 +73,95 @@ function Initialize-Directory {
5973
}
6074
}
6175

62-
function Get-FileSha256Hex {
63-
param([string]$Path)
64-
return ([string](Get-FileHash -Algorithm SHA256 -LiteralPath $Path).Hash).ToLowerInvariant()
65-
}
66-
67-
function Assert-Hash {
76+
function Assert-IcoFile {
6877
param(
6978
[string]$Path,
70-
[string]$Expected,
7179
[string]$Label
7280
)
73-
if ($SkipHashCheck) {
74-
Write-Step ('Skipping SHA-256 verification for ' + $Label + '.')
75-
return
76-
}
77-
if ([string]::IsNullOrWhiteSpace([string]$Expected)) {
78-
throw ('Missing expected SHA-256 for ' + $Label + '.')
81+
82+
$bytes = [System.IO.File]::ReadAllBytes($Path)
83+
if ($bytes.Length -lt 6) {
84+
throw ($Label + ' is too small to be a valid icon file.')
7985
}
80-
$actual = Get-FileSha256Hex -Path $Path
81-
if ($actual -ne $Expected.ToLowerInvariant()) {
82-
throw ($Label + ' SHA-256 mismatch. Expected ' + $Expected + ' but got ' + $actual + '.')
86+
if ($bytes[0] -ne 0 -or $bytes[1] -ne 0 -or $bytes[2] -ne 1 -or $bytes[3] -ne 0) {
87+
throw ($Label + ' does not look like a valid .ico file.')
8388
}
8489
}
8590

86-
function Assert-IcoFile {
91+
function Assert-PowerShellScript {
8792
param(
8893
[string]$Path,
8994
[string]$Label
9095
)
91-
$bytes = [System.IO.File]::ReadAllBytes($Path)
92-
if ($bytes.Length -lt 6) {
93-
throw ($Label + ' is too small to be a valid icon file.')
96+
97+
$raw = Get-Content -LiteralPath $Path -Raw -Encoding UTF8
98+
if ($raw -match '(?i)<html' -or $raw -match '(?i)<!DOCTYPE html') {
99+
throw ($Label + ' looks like HTML instead of PowerShell.')
94100
}
95-
if ($bytes[0] -ne 0 -or $bytes[1] -ne 0 -or $bytes[2] -ne 1 -or $bytes[3] -ne 0) {
96-
throw ($Label + ' does not look like a valid .ico file.')
101+
if ($raw -notmatch '\$script:AppName\s*=\s*[''"]Tailscale Control[''"]') {
102+
throw ($Label + ' did not pass the content sanity check.')
97103
}
98104
}
99105

106+
function Resolve-ReleaseTag {
107+
param([string]$Tag)
108+
109+
$tagValue = [string]$Tag
110+
if ([string]::IsNullOrWhiteSpace($tagValue)) {
111+
return $DefaultReleaseTag
112+
}
113+
$tagValue = $tagValue.Trim()
114+
if ($tagValue -eq 'latest') {
115+
return 'latest'
116+
}
117+
if ($tagValue -match '^\d+(\.\d+){1,3}([\-+].*)?$') {
118+
return ('v' + $tagValue)
119+
}
120+
return $tagValue
121+
}
122+
100123
function Resolve-ReleaseAssetBase {
101124
param(
102125
[string]$Tag,
103126
[string]$ExplicitBase
104127
)
128+
105129
if (-not [string]::IsNullOrWhiteSpace([string]$ExplicitBase)) {
106130
return ([string]$ExplicitBase).TrimEnd('/')
107131
}
108-
$tagValue = [string]$Tag
109-
if ([string]::IsNullOrWhiteSpace($tagValue)) { $tagValue = 'latest' }
132+
133+
$tagValue = Resolve-ReleaseTag -Tag $Tag
110134
if ($tagValue -eq 'latest') {
111-
return ($script:GitHubReleasesBase + '/latest/download')
135+
return 'https://github.com/luizbizzio/tailscale-control/releases/latest/download'
112136
}
113-
return ($script:GitHubReleasesBase + '/download/' + [uri]::EscapeDataString($tagValue))
137+
return ('https://github.com/luizbizzio/tailscale-control/releases/download/' + [uri]::EscapeDataString($tagValue))
114138
}
115139

140+
$script:ResolvedReleaseTag = Resolve-ReleaseTag -Tag $ReleaseTag
116141
$script:ResolvedReleaseAssetBase = Resolve-ReleaseAssetBase -Tag $ReleaseTag -ExplicitBase $ReleaseAssetBase
117142
$script:RemoteScriptUrl = ($script:ResolvedReleaseAssetBase.TrimEnd('/') + '/tailscale-control.ps1')
118143

119144
function Invoke-Download {
120145
param(
121146
[string]$Url,
122147
[string]$OutFile,
123-
[string]$ExpectedSha256,
124148
[string]$Label,
125149
[switch]$Icon
126150
)
151+
127152
Write-Step ('Downloading ' + $Label + '...')
128-
Invoke-WebRequest -Uri $Url -OutFile $OutFile -UseBasicParsing
153+
Invoke-WebRequest -Uri $Url -OutFile $OutFile -UseBasicParsing -ErrorAction Stop
154+
if (-not (Test-Path -LiteralPath $OutFile)) {
155+
throw ($Label + ' was not downloaded.')
156+
}
129157
if ((Get-Item -LiteralPath $OutFile).Length -le 0) {
130158
throw ($Label + ' download returned an empty file.')
131159
}
132160
if ($Icon) {
133161
Assert-IcoFile -Path $OutFile -Label $Label
162+
} elseif ($Label -eq 'script') {
163+
Assert-PowerShellScript -Path $OutFile -Label $Label
134164
}
135-
elseif ($Label -eq 'script') {
136-
$raw = Get-Content -LiteralPath $OutFile -Raw -Encoding UTF8
137-
if ($raw -match '(?i)<html' -or $raw -match '(?i)<!DOCTYPE html') {
138-
throw 'Downloaded script looks like HTML instead of PowerShell.'
139-
}
140-
if ($raw -notmatch '\$script:AppName\s*=\s*[''\"]Tailscale Control[''\"]') {
141-
throw 'Downloaded script did not pass the content sanity check.'
142-
}
143-
}
144-
Assert-Hash -Path $OutFile -Expected $ExpectedSha256 -Label $Label
145165
}
146166

147167
function Resolve-SourceDirectory {
@@ -159,6 +179,7 @@ function Resolve-LocalIconPath {
159179
[string]$BaseDirectory,
160180
[string]$FileName
161181
)
182+
162183
if ([string]::IsNullOrWhiteSpace([string]$BaseDirectory)) { return $null }
163184
$assetPath = Join-Path (Join-Path $BaseDirectory 'assets\icons') $FileName
164185
if (Test-Path -LiteralPath $assetPath) { return $assetPath }
@@ -169,17 +190,19 @@ function Resolve-LocalIconPath {
169190

170191
function Get-AppVersionFromFile {
171192
param([string]$Path)
193+
172194
try {
173195
$raw = Get-Content -LiteralPath $Path -Raw -Encoding UTF8
174-
if ($raw -match '\$script:AppVersion\s*=\s*[''\"]([^''\"]+)[''\"]') { return [string]$Matches[1] }
175-
if ($raw -match '\$AppVersion\s*=\s*[''\"]([^''\"]+)[''\"]') { return [string]$Matches[1] }
196+
if ($raw -match '\$script:AppVersion\s*=\s*[''"]([^''"]+)[''"]') { return [string]$Matches[1] }
197+
if ($raw -match '\$AppVersion\s*=\s*[''"]([^''"]+)[''"]') { return [string]$Matches[1] }
176198
}
177199
catch { }
178200
return 'unknown'
179201
}
180202

181203
function Write-AppLauncherVbs {
182204
param([string]$ScriptPath)
205+
183206
$content = @"
184207
Set oShell = CreateObject("WScript.Shell")
185208
cmd = Chr(34) & "$($script:PowerShellExe)" & Chr(34) & " -NoProfile -ExecutionPolicy Bypass -WindowStyle Hidden -File " & Chr(34) & "$ScriptPath" & Chr(34)
@@ -209,6 +232,7 @@ function Install-FileAtomically {
209232
[string]$SourcePath,
210233
[string]$DestinationPath
211234
)
235+
212236
$destinationDirectory = Split-Path -Parent $DestinationPath
213237
Initialize-Directory -Path $destinationDirectory
214238
$tmpPath = $DestinationPath + '.tmp'
@@ -222,6 +246,11 @@ function Install-FileAtomically {
222246
Move-Item -LiteralPath $tmpPath -Destination $DestinationPath -Force
223247
}
224248

249+
try {
250+
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 -bor [Net.SecurityProtocolType]::Tls11 -bor [Net.SecurityProtocolType]::Tls
251+
} catch {
252+
}
253+
225254
$resolvedSourceDir = Resolve-SourceDirectory
226255
$localScriptPath = if ($resolvedSourceDir) { Join-Path $resolvedSourceDir 'tailscale-control.ps1' } else { $null }
227256
$useLocalScript = $false
@@ -237,51 +266,59 @@ $tempIconsDir = Join-Path $tempRoot 'assets\icons'
237266
Initialize-Directory -Path $tempIconsDir
238267
$tempScriptPath = Join-Path $tempRoot 'tailscale-control.ps1'
239268

240-
if ($useLocalScript) {
241-
Write-Step ('Using local script from ' + $localScriptPath)
242-
Copy-Item -LiteralPath $localScriptPath -Destination $tempScriptPath -Force
243-
}
244-
else {
245-
Invoke-Download -Url $script:RemoteScriptUrl -OutFile $tempScriptPath -ExpectedSha256 $script:ExpectedScriptSha256 -Label 'script'
246-
}
269+
try {
270+
Write-Step ('Selected release: ' + $script:ResolvedReleaseTag)
271+
Write-Step ('Release asset base: ' + $script:ResolvedReleaseAssetBase)
247272

248-
foreach ($asset in $script:IconAssets) {
249-
$tempIconPath = Join-Path $tempIconsDir $asset.FileName
250-
$localIconPath = Resolve-LocalIconPath -BaseDirectory $resolvedSourceDir -FileName $asset.FileName
251-
if (-not $PreferRemote -and -not [string]::IsNullOrWhiteSpace([string]$localIconPath)) {
252-
Write-Step ('Using local icon from ' + $localIconPath)
253-
Copy-Item -LiteralPath $localIconPath -Destination $tempIconPath -Force
254-
Assert-IcoFile -Path $tempIconPath -Label $asset.FileName
273+
if ($useLocalScript) {
274+
Write-Step ('Using local script from ' + $localScriptPath)
275+
Copy-Item -LiteralPath $localScriptPath -Destination $tempScriptPath -Force
276+
Assert-PowerShellScript -Path $tempScriptPath -Label 'script'
255277
}
256278
else {
257-
$iconUrl = ($script:ResolvedReleaseAssetBase.TrimEnd('/') + '/' + $asset.FileName)
258-
Invoke-Download -Url $iconUrl -OutFile $tempIconPath -ExpectedSha256 $asset.ExpectedSha256 -Label $asset.FileName -Icon
279+
Invoke-Download -Url $script:RemoteScriptUrl -OutFile $tempScriptPath -Label 'script'
259280
}
260-
}
261281

262-
Install-FileAtomically -SourcePath $tempScriptPath -DestinationPath $script:InstalledScriptPath
263-
foreach ($asset in $script:IconAssets) {
264-
$sourceIcon = Join-Path $tempIconsDir $asset.FileName
265-
$destinationIcon = Join-Path $script:InstalledIconsDir $asset.FileName
266-
Install-FileAtomically -SourcePath $sourceIcon -DestinationPath $destinationIcon
267-
}
268-
Write-AppLauncherVbs -ScriptPath $script:InstalledScriptPath
269-
Initialize-StartMenuShortcut
282+
foreach ($asset in $script:IconAssets) {
283+
$tempIconPath = Join-Path $tempIconsDir $asset.FileName
284+
$localIconPath = Resolve-LocalIconPath -BaseDirectory $resolvedSourceDir -FileName $asset.FileName
285+
if (-not $PreferRemote -and -not [string]::IsNullOrWhiteSpace([string]$localIconPath)) {
286+
Write-Step ('Using local icon from ' + $localIconPath)
287+
Copy-Item -LiteralPath $localIconPath -Destination $tempIconPath -Force
288+
Assert-IcoFile -Path $tempIconPath -Label $asset.FileName
289+
}
290+
else {
291+
$iconUrl = ($script:ResolvedReleaseAssetBase.TrimEnd('/') + '/' + $asset.FileName)
292+
Invoke-Download -Url $iconUrl -OutFile $tempIconPath -Label $asset.FileName -Icon
293+
}
294+
}
270295

271-
try {
272-
Remove-Item -LiteralPath $tempRoot -Recurse -Force -ErrorAction SilentlyContinue
273-
}
274-
catch { }
275-
276-
$version = Get-AppVersionFromFile -Path $script:InstalledScriptPath
277-
if ([string]::IsNullOrWhiteSpace([string]$version)) { $version = [string]$script:AppVersion }
278-
Write-Step ('Install/update completed. Version ' + $version + '.')
279-
Write-Step ('Script path: ' + $script:InstalledScriptPath)
280-
Write-Step ('Icons path: ' + $script:InstalledIconsDir)
281-
Write-Step ('Remote asset base: ' + $script:ResolvedReleaseAssetBase)
282-
Write-Step ('Launcher path: ' + $script:LauncherVbsPath)
283-
284-
if (-not $NoLaunch) {
285-
Write-Step 'Launching Tailscale Control without console...'
286-
Start-Process -FilePath $script:WScriptExe -ArgumentList @($script:LauncherVbsPath)
296+
Install-FileAtomically -SourcePath $tempScriptPath -DestinationPath $script:InstalledScriptPath
297+
foreach ($asset in $script:IconAssets) {
298+
$sourceIcon = Join-Path $tempIconsDir $asset.FileName
299+
$destinationIcon = Join-Path $script:InstalledIconsDir $asset.FileName
300+
Install-FileAtomically -SourcePath $sourceIcon -DestinationPath $destinationIcon
301+
}
302+
Write-AppLauncherVbs -ScriptPath $script:InstalledScriptPath
303+
Initialize-StartMenuShortcut
304+
305+
$version = Get-AppVersionFromFile -Path $script:InstalledScriptPath
306+
if ([string]::IsNullOrWhiteSpace([string]$version)) { $version = [string]$script:AppVersion }
307+
Write-Step ('Install/update completed. Version ' + $version + '.') Green
308+
Write-Step ('Script path: ' + $script:InstalledScriptPath)
309+
Write-Step ('Icons path: ' + $script:InstalledIconsDir)
310+
Write-Step ('Remote asset base: ' + $script:ResolvedReleaseAssetBase)
311+
Write-Step ('Launcher path: ' + $script:LauncherVbsPath)
312+
313+
if (-not $NoLaunch) {
314+
Write-Step 'Launching Tailscale Control without console...' Green
315+
Start-Process -FilePath $script:WScriptExe -ArgumentList ('"' + $script:LauncherVbsPath + '"')
316+
}
317+
} finally {
318+
try {
319+
if (Test-Path -LiteralPath $tempRoot) {
320+
Remove-Item -LiteralPath $tempRoot -Recurse -Force -ErrorAction SilentlyContinue
321+
}
322+
}
323+
catch { }
287324
}

0 commit comments

Comments
 (0)