# The SIYNET installer for Windows, served at siy.sh/win: # # irm siy.sh/win | iex # # It puts siy.exe and siyd.exe in %LOCALAPPDATA%\SIYNET\bin, adds that folder to your PATH, and # starts siyd now and whenever you sign in. No admin needed. macOS and Linux: curl -fsSL siy.sh | sh # # Options, as environment variables set before irm | iex, or as parameters when run as a file: # $env:SIY_DRY_RUN = 1 -DryRun check everything, change nothing # $env:SIY_VERSION = '0.4.2' -Version install this version, not the latest # $env:SIY_INSTALL_DIR = '...' -Dir where siy.exe and siyd.exe go # $env:SIY_NO_START = 1 -NoStart don't start siyd # $env:SIY_UNINSTALL = 1 -Uninstall take SIY off this PC # $env:SIY_RELEASE_BASE where releases come from (https://siy.sh/releases) # # Nothing is installed until the release checks out, and every check fails closed: # 1. SHA256SUMS.minisig must be a minisign signature of SHA256SUMS by the release key below, # checked with minisign from PATH, else with a minisign downloaded from the release whose # SHA-256 is pinned below. # 2. The archive must match its line in that signed SHA256SUMS. # # Releases live under $SIY_RELEASE_BASE: latest (the newest version, on one line), # /SHA256SUMS, /SHA256SUMS.minisig, /siy--.zip (siy.exe # and siyd.exe at the root) and tools/minisign-.exe, for the targets # x86_64-pc-windows-msvc and aarch64-pc-windows-msvc. # # It runs in Windows PowerShell 5.1 and PowerShell 7. The file is plain ASCII on purpose: 5.1 # reads a script without a byte order mark in the local code page, and irm decodes by whatever # charset the server sends, so the marks and curly quotes it prints are made from code points. # # Everything is in one function and the last line calls it, so a download cut short runs nothing, # and your session keeps its own settings. Tests: apps/site/test/install.test.ts. function Install-Siynet { # Its options (-DryRun, -Version , -Dir , -NoStart, -Uninstall, -Help, or the # same as --dry-run and so on) are read from $args, so they arrive the same however it's run. # --- RELEASE KEY PLACEHOLDER --- replace with the SIYNET minisign public key (the "RW..." line of # minisign.pub) before publishing. Until then this installer refuses to install anything. $SiyReleaseKey = 'REPLACE_WITH_SIYNET_MINISIGN_PUBLIC_KEY' # --- BOOTSTRAP VERIFIER PINS --- the SHA-256 of $SIY_RELEASE_BASE/tools/minisign-.exe, # one per target, used only when minisign isn't on PATH. Replace each placeholder with the real # digest before publishing. Until then that path refuses. $SiyMinisignSha256X64 = 'REPLACE_WITH_SHA256_OF_minisign-x86_64-pc-windows-msvc.exe' $SiyMinisignSha256Arm64 = 'REPLACE_WITH_SHA256_OF_minisign-aarch64-pc-windows-msvc.exe' # These only last while this function runs. $ErrorActionPreference = 'Stop' $ProgressPreference = 'SilentlyContinue' Set-StrictMode -Version 2.0 # Run as a file, it exits with a status. Through irm | iex, exiting would close your window. $fromFile = [bool]$MyInvocation.MyCommand.ScriptBlock.File # The boards' marks and punctuation, from their code points (see the note at the top). $Check = [string][char]0x2713 $Cross = [string][char]0x2715 $Ring = [string][char]0x25C9 $Mid = [string][char]0x00B7 $Ap = [string][char]0x2019 $LQ = [string][char]0x201C $RQ = [string][char]0x201D $Esc = [string][char]0x1B $VersionPattern = '^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.]+)?\z' $RunKey = 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Run' $rerun = 'irm siy.sh/win | iex' $state = @{ HeaderDone = $false; Version = ''; Tmp = ''; Changed = $false } # -- Output ------------------------------------------------------------------------------------ # The boards' look: a two-space margin, one line per step, a check when done, ! when it needs # you, a cross for an error, quiet detail after a middle dot, commands in bold. Colour only in a # console, never with NO_COLOR: the boards' own colours where the console takes escape codes, # the nearest console colours where it doesn't. $colour = 'none' if (-not $env:NO_COLOR) { $redirected = $true try { $redirected = [Console]::IsOutputRedirected } catch { $redirected = $true } if (-not $redirected) { $colour = 'console' try { if ($Host.UI.SupportsVirtualTerminal) { $colour = 'ansi' } } catch { $colour = 'console' } } } $ansiCodes = @{ ok = '38;2;142;240;208'; warn = '38;2;255;201;131'; err = '38;2;255;138;122'; dim = '38;2;138;143;156' link = '4;38;2;174;188;255'; bold = '1' iris0 = '38;2;142;162;255'; iris2 = '38;2;255;146;194'; iris3 = '38;2;255;201;131'; iris4 = '38;2;142;240;208' } $consoleColours = @{ ok = 'Green'; warn = 'Yellow'; err = 'Red'; dim = 'DarkGray'; link = 'Cyan'; bold = 'White' iris0 = 'Blue'; iris2 = 'Magenta'; iris3 = 'Yellow'; iris4 = 'Green' } function Write-SiySpan([string]$Text, [string]$Style) { if ($Text -eq '') { return } if ($Style -eq '' -or $colour -eq 'none') { Write-Host -NoNewline -Object $Text } elseif ($colour -eq 'ansi') { Write-Host -NoNewline -Object ($Esc + '[' + $ansiCodes[$Style] + 'm' + $Text + $Esc + '[0m') } else { Write-Host -NoNewline -ForegroundColor $consoleColours[$Style] -Object $Text } } # One line from pairs of text and style (ok, warn, err, dim, link, bold, or '' for plain). A # plain function, so every argument simply arrives in $args. function Write-SiyLine { for ($i = 0; $i -lt $args.Count; $i += 2) { $style = '' if ($i + 1 -lt $args.Count) { $style = [string]$args[$i + 1] } Write-SiySpan ([string]$args[$i]) $style } Write-Host '' } function Write-SiyBlank { Write-Host '' } # The speaker mark, one iris colour per letter, then what it says. function Write-SiySpeaker([string]$Rest) { Write-SiySpan ' ' '' Write-SiySpan $Ring 'iris0' Write-SiySpan ' ' '' Write-SiySpan 's' 'iris2' Write-SiySpan 'i' 'iris3' Write-SiySpan 'y' 'iris4' Write-SiySpan " $Rest" '' } # At the margin, and under a mark (column 5, a command in bold unless it says otherwise). function Write-SiySay([string]$Text, [string]$Style = '') { Write-SiyLine ' ' '' $Text $Style } function Write-SiySub([string]$Text, [string]$Style = 'bold') { Write-SiyLine ' ' '' $Text $Style } function Write-SiyMark([string]$Glyph, [string]$GlyphStyle, [string]$Text, [string]$Detail) { if ($Detail) { Write-SiyLine ' ' '' $Glyph $GlyphStyle " $Text " '' "$Mid $Detail" 'dim' } else { Write-SiyLine ' ' '' $Glyph $GlyphStyle " $Text" '' } } function Write-SiyOk([string]$Text, [string]$Detail = '') { Write-SiyMark $Check 'ok' $Text $Detail } function Write-SiyWarn([string]$Text, [string]$Detail = '') { Write-SiyMark '!' 'warn' $Text $Detail } function Write-SiyBad([string]$Text, [string]$Detail = '') { Write-SiyMark $Cross 'err' $Text $Detail } # A quiet line, with a command to run after it when there is one. function Write-SiyNote([string]$Text, [string]$Command = '') { if ($Command) { Write-SiyLine ' ' '' "$Mid $Text" 'dim' ' ' '' $Command 'bold' } else { Write-SiyLine ' ' '' "$Mid $Text" 'dim' } } function Write-SiyHeader { if ($state.HeaderDone) { return } $state.HeaderDone = $true $what = 'installing siyd' if ($state.Version) { $what = "$what $($state.Version)" } Write-SiyBlank Write-SiySpeaker "$what for $platform" if ($dry) { Write-SiySpan ' ' '' Write-SiySpan "$Mid dry run" 'dim' } Write-Host '' } # Stops the run with an exit status; everything above has already said why. function Stop-Siy([int]$Code = 1) { throw ('SIY-STOP:' + $Code) } function Write-SiyStopped { Write-SiyBad 'stopped' 'nothing on this PC changed' Write-SiyBlank } function Stop-SiyTampered([string]$First) { Write-SiyBad $First Write-SiyBad 'refused to install' 'nothing on this PC changed' Write-SiyBlank Write-SiySay 'The download was changed somewhere on its way here.' Write-SiySay 'Try again from another network:' Write-SiySub $rerun Write-SiyLine ' ' '' 'Still failing? Check the key at ' '' 'siy.sh/key' 'link' Write-SiyBlank Stop-Siy 1 } function Stop-SiyNoVerifier([string]$First) { if (-not $First) { $First = "there${Ap}s nothing on this PC I can check the signature with" } Write-SiyBad $First Write-SiyBad 'refused to install' 'nothing on this PC changed' Write-SiyBlank Write-SiySay 'Install minisign, then open a new terminal and run the same line again:' Write-SiySub 'winget install jedisct1.minisign' Write-SiySub $rerun Write-SiyBlank Stop-Siy 1 } function Stop-SiyMisuse([string]$First, [string]$Detail, [string]$Advice, [string]$Command) { Write-SiyBlank Write-SiyBad $First $Detail Write-SiyBlank Write-SiySay $Advice if ($Command) { Write-SiySub $Command } Write-SiyBlank Stop-Siy 2 } function Write-SiyHelp { Write-SiyBlank Write-SiySpeaker 'This installs siy and siyd, after checking their signature.' Write-Host '' Write-SiyBlank Write-SiySay 'irm siy.sh/win | iex' 'bold' Write-SiyBlank Write-SiySay 'Set any of these first, or pass the parameter when you run the file:' Write-SiySay '$env:SIY_DRY_RUN = 1 -DryRun check everything, change nothing' Write-SiySay '$env:SIY_VERSION = ''0.4.2'' -Version install this version, not the latest' Write-SiySay '$env:SIY_INSTALL_DIR = ''...'' -Dir where siy and siyd go' Write-SiySay ('$env:SIY_NO_START = 1 -NoStart don' + $Ap + 't start siyd') Write-SiySay '$env:SIY_UNINSTALL = 1 -Uninstall take siy and siyd off this PC' Write-SiyBlank Write-SiySay 'SIY_RELEASE_BASE is where releases come from. NO_COLOR turns colour off.' 'dim' Write-SiyBlank } # -- Downloads --------------------------------------------------------------------------------- function Get-SiyReason([System.Exception]$Problem) { $inner = $Problem while ($null -ne $inner.InnerException) { $inner = $inner.InnerException } return $inner.Message } function Test-SiyTlsProblem([System.Exception]$Problem) { $inner = $Problem while ($null -ne $inner) { if ($inner -is [System.Security.Authentication.AuthenticationException]) { return $true } if ($inner -is [System.Net.WebException] -and ([string]$inner.Status) -in @('TrustFailure', 'SecureChannelFailure')) { return $true } $inner = $inner.InnerException } return $false } # Downloads a file. Kind is '' when the whole file arrived; otherwise missing (404), http # (another error), lost (cut off part way), tls or unreachable, with what's known for the message. function Get-SiyFile([string]$Url, [string]$Path) { $result = @{ Kind = ''; Code = 0; Got = [long]0; Total = [long]-1; Detail = '' } if (Test-Path -LiteralPath $Path) { Remove-Item -LiteralPath $Path -Force } $response = $null try { try { $response = $client.GetAsync($Url, [System.Net.Http.HttpCompletionOption]::ResponseHeadersRead).GetAwaiter().GetResult() } catch { $result.Detail = Get-SiyReason $_.Exception if (Test-SiyTlsProblem $_.Exception) { $result.Kind = 'tls' } else { $result.Kind = 'unreachable' } return $result } $result.Code = [int]$response.StatusCode if (-not $response.IsSuccessStatusCode) { if ($result.Code -eq 404 -or $result.Code -eq 410) { $result.Kind = 'missing' } else { $result.Kind = 'http' } return $result } if ($null -ne $response.Content.Headers.ContentLength) { $result.Total = [long]$response.Content.Headers.ContentLength } $stream = $null $file = $null try { $stream = $response.Content.ReadAsStreamAsync().GetAwaiter().GetResult() $file = [System.IO.File]::Create($Path) $buffer = New-Object byte[] 81920 while ($true) { # A connection that goes quiet for a minute counts as lost. $task = $stream.ReadAsync($buffer, 0, $buffer.Length) if (-not $task.Wait([TimeSpan]::FromSeconds(60))) { throw 'the download stalled' } $read = $task.Result if ($read -le 0) { break } $file.Write($buffer, 0, $read) $result.Got += $read } } catch { $result.Kind = 'lost' $result.Detail = Get-SiyReason $_.Exception return $result } finally { if ($null -ne $file) { $file.Dispose() } if ($null -ne $stream) { $stream.Dispose() } } if ($result.Total -ge 0 -and $result.Got -lt $result.Total) { $result.Kind = 'lost' } return $result } finally { if ($null -ne $response) { $response.Dispose() } } } # Says why a download failed, and stops. $What is what was being downloaded. function Stop-SiyDownload([hashtable]$Result, [string]$What) { Write-SiyHeader switch ($Result.Kind) { 'lost' { if ($Result.Total -gt 0) { $percent = [int][Math]::Min(99, [Math]::Floor($Result.Got * 100 / $Result.Total)) Write-SiyBad 'lost the network while downloading' "$percent%" } else { Write-SiyBad 'lost the network while downloading' } Write-SiyStopped Write-SiySay 'Check your Wi-Fi or cable, then run the same line again:' } 'unreachable' { Write-SiyBad "couldn${Ap}t reach $siteHost" Write-SiyStopped Write-SiySay 'Check your Wi-Fi or cable, then run the same line again:' } 'missing' { Write-SiyBad "there${Ap}s no siyd $($state.Version) for $platform yet" Write-SiyStopped if ($want -ne 'latest') { if ($optVersion) { Write-SiySay 'Run it again without -Version for the latest.' } else { Write-SiySay 'Install the latest version instead:' Write-SiySub ('$env:SIY_VERSION = $null; ' + $rerun) } Write-SiyBlank Stop-Siy 1 } Write-SiySay "It isn${Ap}t out for this PC yet. Run the same line again later:" } 'tls' { Write-SiyBad "couldn${Ap}t make a secure connection to $siteHost" Write-SiyStopped Write-SiySay 'A network that looks inside secure connections can do this. Try another network:' } 'http' { Write-SiyBad "$siteHost couldn${Ap}t send $What" "HTTP $($Result.Code)" Write-SiyStopped Write-SiySay "That${Ap}s on $siteHost${Ap}s side. Wait a minute, then run the same line again:" } default { Write-SiyBad "couldn${Ap}t download $What" $Result.Detail Write-SiyStopped Write-SiySay 'Run the same line again:' } } Write-SiySub $rerun Write-SiyBlank Stop-Siy 1 } function Receive-SiyFile([string]$Url, [string]$Path, [string]$What) { $result = Get-SiyFile $Url $Path if ($result.Kind) { Stop-SiyDownload $result $What } } function Get-SiySha256([string]$Path) { return (Get-FileHash -LiteralPath $Path -Algorithm SHA256).Hash.ToLowerInvariant() } # Runs a program quietly and returns its exit status, or -1 when it couldn't run at all. Its # error output is dropped rather than turned into PowerShell errors. LASTEXITCODE starts at -1, # so a program that never ran can't pass for one that said yes. function Invoke-SiyNative([string]$File, [string[]]$Arguments) { if (-not (Test-Path -LiteralPath $File -PathType Leaf)) { return -1 } $ErrorActionPreference = 'Continue' $global:LASTEXITCODE = -1 try { & $File @Arguments *> $null } catch { return -1 } if ($null -eq $global:LASTEXITCODE) { return -1 } return [int]$global:LASTEXITCODE } # -- Checking ---------------------------------------------------------------------------------- function Get-SiyVerifier { $pin = $SiyMinisignSha256X64 if ($target -eq 'aarch64-pc-windows-msvc') { $pin = $SiyMinisignSha256Arm64 } $pin = $pin.ToLowerInvariant() # A placeholder pin isn't a pin: refuse rather than run something unchecked. if ($pin -notmatch '^[0-9a-f]{64}\z') { Stop-SiyNoVerifier '' } $path = Join-Path $state.Tmp 'minisign.exe' $result = Get-SiyFile "$base/tools/minisign-$target.exe" $path if ($result.Kind -eq 'missing') { Stop-SiyNoVerifier '' } if ($result.Kind) { Stop-SiyDownload $result 'the signature checker' } if ((Get-SiySha256 $path) -ne $pin) { Stop-SiyNoVerifier "the signature checker I downloaded isn${Ap}t the one I trust" } return $path } function Test-SiySignature([string]$Sums, [string]$Signature) { $arguments = @('-V', '-q', '-P', $SiyReleaseKey, '-m', $Sums, '-x', $Signature) $minisign = Get-Command -Name 'minisign' -CommandType Application -ErrorAction SilentlyContinue | Select-Object -First 1 if ($null -ne $minisign) { if ((Invoke-SiyNative $minisign.Path $arguments) -eq 0) { return } Stop-SiyTampered "the signature doesn${Ap}t match the SIYNET release key" } $verifier = Get-SiyVerifier $status = Invoke-SiyNative $verifier $arguments if ($status -eq 0) { return } if ($status -eq -1) { Stop-SiyNoVerifier "I couldn${Ap}t run the signature checker I downloaded" } Stop-SiyTampered "the signature doesn${Ap}t match the SIYNET release key" } # The archive's hash in the signed SHA256SUMS (" " or " *"). function Get-SiyExpected([string]$Sums, [string]$Name) { $found = New-Object System.Collections.Generic.List[string] foreach ($line in [System.IO.File]::ReadAllLines($Sums)) { $fields = @($line.Trim() -split '\s+') if ($fields.Count -ne 2) { continue } if ($fields[1].TrimStart('*') -ceq $Name) { $found.Add($fields[0].ToLowerInvariant()) } } $unique = @($found | Select-Object -Unique) if ($unique.Count -eq 0) { # Signed, and this PC's build isn't in it. Stop-SiyDownload @{ Kind = 'missing'; Code = 404; Got = [long]0; Total = [long]-1; Detail = '' } "siyd $($state.Version)" } if ($unique.Count -ne 1 -or $unique[0] -notmatch '^[0-9a-f]{64}\z') { Stop-SiyTampered "the download doesn${Ap}t match its signed checksum" } return $unique[0] } # -- Installing -------------------------------------------------------------------------------- function Get-SiyUserPathParts { $key = [Microsoft.Win32.Registry]::CurrentUser.OpenSubKey('Environment', $false) if ($null -eq $key) { return } $value = '' try { # Unexpanded, so entries like %USERPROFILE%\bin stay as they are when it's written back. $value = [string]$key.GetValue('Path', '', [Microsoft.Win32.RegistryValueOptions]::DoNotExpandEnvironmentNames) } finally { $key.Close() } foreach ($part in ($value -split ';')) { if ($part -ne '') { $part } } } function Test-SiySamePath([string]$First, [string]$Second) { $one = [Environment]::ExpandEnvironmentVariables($First).TrimEnd('\') $two = [Environment]::ExpandEnvironmentVariables($Second).TrimEnd('\') return [string]::Equals($one, $two, [System.StringComparison]::OrdinalIgnoreCase) } function Test-SiyUserPath([string]$Folder) { foreach ($part in @(Get-SiyUserPathParts)) { if (Test-SiySamePath $part $Folder) { return $true } } return $false } function Set-SiyUserPath([string[]]$Parts) { $key = [Microsoft.Win32.Registry]::CurrentUser.CreateSubKey('Environment') try { $kind = [Microsoft.Win32.RegistryValueKind]::ExpandString if (@($key.GetValueNames()) -contains 'Path') { $kind = $key.GetValueKind('Path') } $key.SetValue('Path', ($Parts -join ';'), $kind) } finally { $key.Close() } # Setting a user variable through .NET tells Explorer and new terminals that PATH changed. [Environment]::SetEnvironmentVariable('SIYNET_INSTALL_REFRESH', '1', 'User') [Environment]::SetEnvironmentVariable('SIYNET_INSTALL_REFRESH', [NullString]::Value, 'User') } function Add-SiyUserPath([string]$Folder) { if (Test-SiyUserPath $Folder) { return $false } Set-SiyUserPath (@(Get-SiyUserPathParts) + $Folder) return $true } function Remove-SiyUserPath([string]$Folder) { $keep = @(Get-SiyUserPathParts | Where-Object { -not (Test-SiySamePath $_ $Folder) }) Set-SiyUserPath $keep } # siyd processes started from the install folder. function Get-SiydProcess { $prefix = $installDir.TrimEnd('\') + '\' foreach ($candidate in @(Get-Process -Name 'siyd' -ErrorAction SilentlyContinue)) { $path = $null try { $path = $candidate.Path } catch { $path = $null } if ($path -and $path.StartsWith($prefix, [System.StringComparison]::OrdinalIgnoreCase)) { $candidate } } } function Install-SiyFiles([string]$From) { $null = [System.IO.Directory]::CreateDirectory($installDir) foreach ($name in @('siy.exe', 'siyd.exe')) { Copy-Item -LiteralPath (Join-Path $From $name) -Destination (Join-Path $installDir "$name.new") -Force } $state.Changed = $true foreach ($name in @('siy.exe', 'siyd.exe')) { $final = Join-Path $installDir $name if (Test-Path -LiteralPath $final) { # A running siyd.exe can be renamed but not replaced, so the old copy moves aside first. $aside = Join-Path $installDir ($name + '.' + [guid]::NewGuid().ToString('N').Substring(0, 8) + '.old') Move-Item -LiteralPath $final -Destination $aside -Force } Move-Item -LiteralPath (Join-Path $installDir "$name.new") -Destination $final -Force } # Old copies go once nothing is running them; any still in use go next time. foreach ($old in @(Get-ChildItem -LiteralPath $installDir -Filter '*.old' -File -ErrorAction SilentlyContinue)) { Remove-Item -LiteralPath $old.FullName -Force -ErrorAction SilentlyContinue } } # Starts siyd now, after stopping one started from here before, and says whether it stayed up. function Start-Siyd([string]$Siyd) { foreach ($old in @(Get-SiydProcess)) { Stop-Process -Id $old.Id -Force -ErrorAction SilentlyContinue $null = $old.WaitForExit(5000) } $process = Start-Process -FilePath $Siyd -WorkingDirectory $installDir -WindowStyle Hidden -PassThru Start-Sleep -Milliseconds 1000 return (-not $process.HasExited) } function Write-SiyNext { Write-SiyBlank Write-SiySay 'Join a SIYNET' 'bold' Write-SiySub 'on a device you already have, open Add a device' '' Write-SiyLine ' then type its code here ' '' 'siy join ' 'bold' Write-SiyBlank Write-SiyLine ' Or start a new one: this PC is ready. ' '' 'siy status' 'bold' Write-SiyBlank } function Invoke-SiyInstall { if (-not $target) { Write-SiyBlank Write-SiyBad "siyd doesn${Ap}t run on $platform yet" Write-SiyBlank Write-SiySay 'It runs on macOS, Linux, Raspberry Pi and 64-bit Windows.' Write-SiyBlank Stop-Siy 1 } if (-not $SiyReleaseKey -or $SiyReleaseKey -like 'REPLACE_*') { Write-SiyHeader Write-SiyBad "this copy of the installer doesn${Ap}t have the SIYNET release key" Write-SiyBad 'refused to install' 'nothing on this PC changed' Write-SiyBlank Write-SiySay "Without the key I can${Ap}t check the signature, so I won${Ap}t install anything." Write-SiySay 'Use the installer from siy.sh:' Write-SiySub $rerun Write-SiyBlank Stop-Siy 1 } $tmp = Join-Path ([System.IO.Path]::GetTempPath()) ('siy-install-' + [guid]::NewGuid().ToString('N')) $null = [System.IO.Directory]::CreateDirectory($tmp) $state.Tmp = $tmp Add-Type -AssemblyName System.Net.Http if ($PSVersionTable.PSVersion.Major -lt 6) { # Windows PowerShell may not offer TLS 1.2 by itself. [Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12 } $client = New-Object System.Net.Http.HttpClient try { $client.Timeout = [TimeSpan]::FromMinutes(2) $client.DefaultRequestHeaders.UserAgent.ParseAdd('siynet-installer/1.0') if ($want -eq 'latest') { $latest = Join-Path $tmp 'latest' $result = Get-SiyFile "$base/latest" $latest # No "latest" at all is the server's problem, not a missing build for this PC. if ($result.Kind -eq 'missing') { $result.Kind = 'http' } if ($result.Kind) { Stop-SiyDownload $result 'the latest version number' } $line = [string](@([System.IO.File]::ReadAllLines($latest)) | Select-Object -First 1) $line = $line.Trim() if ($line -cnotmatch $VersionPattern) { Write-SiyHeader Write-SiyBad "$siteHost sent a version I can${Ap}t read" Write-SiyStopped Write-SiySay 'A network that asks you to sign in first can do this. Sign in, or try another network:' Write-SiySub $rerun Write-SiyBlank Stop-Siy 1 } $state.Version = $line } $v = $state.Version Write-SiyHeader $sums = Join-Path $tmp 'SHA256SUMS' $signature = Join-Path $tmp 'SHA256SUMS.minisig' Receive-SiyFile "$base/$v/SHA256SUMS" $sums 'the signed checksums' Receive-SiyFile "$base/$v/SHA256SUMS.minisig" $signature 'the signature' Test-SiySignature $sums $signature $archive = "siy-$v-$target.zip" $expected = Get-SiyExpected $sums $archive $zip = Join-Path $tmp $archive Receive-SiyFile "$base/$v/$archive" $zip "siyd $v" if ((Get-SiySha256 $zip) -ne $expected) { Stop-SiyTampered "the download doesn${Ap}t match its signed checksum" } Write-SiyOk 'checked the signature' 'SIYNET release key' } finally { $client.Dispose() } $unpacked = Join-Path $tmp 'x' $unpackedOk = $true try { Expand-Archive -LiteralPath $zip -DestinationPath $unpacked -Force } catch { $unpackedOk = $false } foreach ($name in @('siy.exe', 'siyd.exe')) { if (-not (Test-Path -LiteralPath (Join-Path $unpacked $name) -PathType Leaf)) { $unpackedOk = $false } } if (-not $unpackedOk) { Write-SiyBad "the download doesn${Ap}t have siy and siyd in it" Write-SiyBad 'refused to install' 'nothing on this PC changed' Write-SiyBlank Write-SiySay "That${Ap}s a problem with this release, not with this PC. Run the same line again later:" Write-SiySub $rerun Write-SiyBlank Stop-Siy 1 } $onUserPath = Test-SiyUserPath $installDir if ($dry) { Write-SiyNote "would put siy and siyd in $installDir" if (-not $onUserPath) { Write-SiyNote 'would add it to your PATH' } if (-not $leaveStopped) { Write-SiyNote 'would start siyd now, and when you sign in' } Write-SiyBlank Write-SiySay 'Nothing on this PC changed.' Write-SiyBlank return } try { Install-SiyFiles $unpacked } catch { Write-SiyBad "couldn${Ap}t put siy and siyd in $installDir" (Get-SiyReason $_.Exception) if ($state.Changed) { Write-SiyBad 'stopped part way' 'run the same line again to finish' Write-SiyBlank } else { Write-SiyStopped } Write-SiySay 'Close anything using those files, or choose another folder, then run the same line again:' Write-SiySub $rerun Write-SiyBlank Stop-Siy 1 } Write-SiyOk 'installed siy and siyd' $installDir $pathProblem = '' try { if (Add-SiyUserPath $installDir) { Write-SiyOk 'added it to your PATH' 'new terminals will find siy' } } catch { $pathProblem = Get-SiyReason $_.Exception } # This window too, so siy works straight away. $here = @(([string]$env:Path) -split ';' | Where-Object { $_ -ne '' -and (Test-SiySamePath $_ $installDir) }) if ($here.Count -eq 0) { $env:Path = $installDir + ';' + $env:Path } $siyd = Join-Path $installDir 'siyd.exe' $startProblem = '' if ($leaveStopped) { Write-SiyNote 'start siyd by hand with' 'siyd' } else { $atSignIn = $true try { Set-ItemProperty -LiteralPath $RunKey -Name 'siyd' -Value ('"' + $siyd + '"') } catch { $atSignIn = $false } $running = $false try { $running = Start-Siyd $siyd } catch { $running = $false } if ($running -and $atSignIn) { Write-SiyOk 'siyd is running' 'it starts when you sign in' } elseif ($running) { Write-SiyOk 'siyd is running' $startProblem = "I couldn${Ap}t set siyd to start when you sign in" } else { $startProblem = "siyd didn${Ap}t stay running" } } # Warnings go last, each above the line that fixes it. if ($pathProblem) { Write-SiyWarn "I couldn${Ap}t add $installDir to your PATH" $pathProblem Write-SiySub ('[Environment]::SetEnvironmentVariable(''Path'', ''' + $installDir + ';'' + [Environment]::GetEnvironmentVariable(''Path'', ''User''), ''User'')') } if ($startProblem) { Write-SiyWarn $startProblem "it${Ap}s installed; run it here to see why" Write-SiySub 'siyd' } Write-SiyNext } function Invoke-SiyUninstall { $siyExe = Join-Path $installDir 'siy.exe' $siydExe = Join-Path $installDir 'siyd.exe' # A siy that knows `uninstall` does it properly: it asks first, and takes this PC out of the # SIYNET. if ((Test-Path -LiteralPath $siyExe -PathType Leaf) -and (Invoke-SiyNative $siyExe @('help', 'uninstall')) -eq 0) { $arguments = @('uninstall') if ($dry) { $arguments += '--dry-run' } # In this console, so it can ask, and you can answer. $handover = Start-Process -FilePath $siyExe -ArgumentList $arguments -NoNewWindow -Wait -PassThru if ($null -ne $handover.ExitCode -and $handover.ExitCode -ne 0) { Stop-Siy $handover.ExitCode } return } $data = [string]$env:SIYNET_HOME if (-not $data) { $roaming = [string]$env:APPDATA if (-not $roaming) { $roaming = Join-Path $HOME 'AppData\Roaming' } $data = Join-Path $roaming 'SIYNET' } $atSignIn = $false try { $atSignIn = $null -ne (Get-ItemProperty -LiteralPath $RunKey -Name 'siyd' -ErrorAction Stop) } catch { $atSignIn = $false } $running = @(Get-SiydProcess) $files = @(@($siyExe, $siydExe) | Where-Object { Test-Path -LiteralPath $_ }) $onUserPath = Test-SiyUserPath $installDir Write-SiyBlank Write-SiySpeaker 'This removes SIY from this PC.' if ($dry) { Write-SiySpan ' ' '' Write-SiySpan "$Mid dry run" 'dim' } Write-Host '' Write-SiyBlank if ($files.Count -eq 0 -and -not $atSignIn -and $running.Count -eq 0 -and -not $onUserPath) { Write-SiyOk "there${Ap}s nothing to remove" "siy and siyd aren${Ap}t in $installDir" Write-SiyBlank return } Write-SiySay 'It will:' if ($atSignIn -or $running.Count -gt 0) { Write-SiySub 'stop siyd, and stop it starting when you sign in' '' } Write-SiySub "remove siy and siyd from $installDir" '' if ($onUserPath) { Write-SiySub 'take that folder off your PATH' '' } if (Test-Path -LiteralPath $data) { Write-SiySay "Your data stays in $data." 'dim' Write-SiySay 'This PC stays in your SIYNET until you remove it in Devices on another device.' 'dim' } Write-SiyBlank if ($dry) { Write-SiySay 'Nothing on this PC changed.' Write-SiyBlank return } if ($atSignIn -or $running.Count -gt 0) { Remove-ItemProperty -LiteralPath $RunKey -Name 'siyd' -ErrorAction SilentlyContinue foreach ($process in $running) { Stop-Process -Id $process.Id -Force -ErrorAction SilentlyContinue $null = $process.WaitForExit(5000) } Write-SiyOk 'stopped siyd' } foreach ($leftover in @(Get-ChildItem -LiteralPath $installDir -File -ErrorAction SilentlyContinue)) { if ($leftover.Name -in @('siy.exe', 'siyd.exe') -or $leftover.Name -like '*.old' -or $leftover.Name -like '*.new') { Remove-Item -LiteralPath $leftover.FullName -Force -ErrorAction SilentlyContinue } } if (@($siyExe, $siydExe) | Where-Object { Test-Path -LiteralPath $_ }) { Write-SiyBad "couldn${Ap}t remove siy and siyd from $installDir" Write-SiyBlank Write-SiySay 'Close anything using them, then run the same line again.' Write-SiyBlank Stop-Siy 1 } if (@(Get-ChildItem -LiteralPath $installDir -Force -ErrorAction SilentlyContinue).Count -eq 0) { Remove-Item -LiteralPath $installDir -Force -ErrorAction SilentlyContinue } if ($onUserPath) { try { Remove-SiyUserPath $installDir } catch { Write-SiyWarn "I couldn${Ap}t take $installDir off your PATH" } } Write-SiyOk 'removed siy and siyd' $installDir Write-SiyBlank Write-SiySay 'SIY is gone from this PC. To bring it back:' Write-SiySub $rerun Write-SiyBlank } # -- Running ----------------------------------------------------------------------------------- function Test-SiyOn([string]$Value) { return $Value -in @('1', 'true', 'yes', 'on') } $optDry = $false $optNoStart = $false $optUninstall = $false $optHelp = $false $optVersion = '' $optDir = '' $misuse = '' $words = @($args) for ($i = 0; $i -lt $words.Count; $i++) { $word = [string]$words[$i] $value = $null if ($word -match '^(--?[A-Za-z][A-Za-z-]*)[:=](.*)$') { $word = $Matches[1] $value = $Matches[2] } if ($word -in @('-DryRun', '--dry-run')) { $optDry = $true } elseif ($word -in @('-NoStart', '--no-start')) { $optNoStart = $true } elseif ($word -in @('-Uninstall', '--uninstall')) { $optUninstall = $true } elseif ($word -in @('-Help', '--help', '-h', '-?')) { $optHelp = $true } elseif ($word -in @('-Version', '--version', '-Dir', '--dir')) { if ($null -eq $value) { $i++ $value = '' if ($i -lt $words.Count) { $value = [string]$words[$i] } } if ($word -in @('-Version', '--version')) { $optVersion = $value if (-not $value) { $misuse = '-Version needs a version, like 0.4.2' } } else { $optDir = $value if (-not $value) { $misuse = '-Dir needs a folder' } } } else { $misuse = "I don${Ap}t know the option $word" } } $dry = $optDry -or (Test-SiyOn $env:SIY_DRY_RUN) $leaveStopped = $optNoStart -or (Test-SiyOn $env:SIY_NO_START) $removing = $optUninstall -or (Test-SiyOn $env:SIY_UNINSTALL) $want = $optVersion if (-not $want) { $want = [string]$env:SIY_VERSION } if (-not $want) { $want = 'latest' } $platform = 'Windows' $target = '' $installDir = '' $base = '' $siteHost = '' $code = 0 try { $onWindows = $true if ($PSVersionTable.PSVersion.Major -ge 6) { $onWindows = [bool]$IsWindows } if ($optHelp) { Write-SiyHelp } elseif ($misuse) { Stop-SiyMisuse $misuse '' 'Run the file with -Help to see the options.' '' } elseif (-not $onWindows) { Write-SiyBlank Write-SiyBad 'this line is for Windows' Write-SiyBlank Write-SiySay 'On macOS and Linux, run this in a terminal:' Write-SiySub 'curl -fsSL siy.sh | sh' Write-SiyBlank Stop-Siy 1 } else { # The processor Windows runs on, even when this PowerShell is emulated. $arch = '' try { $arch = [string][System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture } catch { $arch = '' } if (-not $arch) { $arch = [string]$env:PROCESSOR_ARCHITEW6432 } if (-not $arch) { $arch = [string]$env:PROCESSOR_ARCHITECTURE } $archLabel = $arch.ToLowerInvariant() if ($arch -in @('X64', 'AMD64')) { $target = 'x86_64-pc-windows-msvc' $archLabel = 'x64' } elseif ($arch -in @('Arm64', 'ARM64')) { $target = 'aarch64-pc-windows-msvc' $archLabel = 'arm64' } $platform = "Windows ($archLabel)" if ($want -ne 'latest') { $want = $want -replace '^v', '' if ($want -cnotmatch $VersionPattern) { if ($optVersion) { Stop-SiyMisuse "$LQ$want$RQ isn${Ap}t a version" 'try one like 0.4.2' 'Give -Version one, or leave it out for the latest.' '' } Stop-SiyMisuse "$LQ$want$RQ isn${Ap}t a version" 'try one like 0.4.2' 'Set SIY_VERSION to one, or clear it for the latest:' '$env:SIY_VERSION = $null' } $state.Version = $want } $base = [string]$env:SIY_RELEASE_BASE if (-not $base) { $base = 'https://siy.sh/releases' } $base = $base.TrimEnd('/') $baseUri = $null if (-not [Uri]::TryCreate($base, [UriKind]::Absolute, [ref]$baseUri) -or $baseUri.Scheme -notin @('https', 'http')) { Stop-SiyMisuse 'SIY_RELEASE_BASE has to be a web address' 'like https://siy.sh/releases' 'Clear it to install from siy.sh:' '$env:SIY_RELEASE_BASE = $null' } $siteHost = $baseUri.Host $installDir = $optDir if (-not $installDir) { $installDir = [string]$env:SIY_INSTALL_DIR } if (-not $installDir) { $local = [string]$env:LOCALAPPDATA if (-not $local) { $local = Join-Path $HOME 'AppData\Local' } $installDir = Join-Path $local 'SIYNET\bin' } $installDir = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($installDir).TrimEnd('\') if ($removing) { Invoke-SiyUninstall } else { Invoke-SiyInstall } } } catch { if ($_.Exception.Message -match '^SIY-STOP:(\d+)$') { $code = [int]$Matches[1] } else { # Something nobody planned for: say what, and what still holds. Write-SiyBlank Write-SiyBad "something went wrong that I didn${Ap}t expect" (Get-SiyReason $_.Exception) if ($state.Changed) { Write-SiyBad 'stopped part way' 'run the same line again to finish' } else { Write-SiyBad 'stopped' 'nothing on this PC changed' } Write-SiyBlank Write-SiySay 'Run the same line again:' Write-SiySub $rerun Write-SiyBlank $code = 1 } } finally { if ($state.Tmp -and (Test-Path -LiteralPath $state.Tmp)) { Remove-Item -LiteralPath $state.Tmp -Recurse -Force -ErrorAction SilentlyContinue } } if ($fromFile) { exit $code } $global:LASTEXITCODE = $code } Install-Siynet @args