# One-step cindertunnel installer for Windows — the GUI/desktop # counterpart to deploy/cindertunnel-install.sh (that script's own # header comment explains the fetch/verify/install shape this one # shares; read it first if you haven't): # # iwr -useb https://cinderapps.org/cindertunnel/install.ps1 | iex # # Why a separate script rather than teaching the bash one to also run # under WSL/Git-Bash on Windows: this one asks its two questions (where # to install, whether to start at login) with a real native GUI # folder-picker and a native yes/no box, not a terminal prompt — most # Windows users running this via `iwr | iex` from a plain PowerShell # window have no terminal-prompting convention to meet them halfway, # unlike cindertunnel-install.sh's own /dev/tty prompts, which assume a # real terminal is already in play. It also sets up a per-user Startup # shortcut rather than a system service: a desktop GUI agent (this # build now carries a system-tray icon, see cmd/cindertunnel/ # tray_windows.go) is naturally single-user, unlike the systemd/launchd # path, which deliberately runs system-wide for an unattended # server/Pi (cindertunnel-install.sh's own comment explains why that # one is never per-user) — there's no equivalent "survive someone else # logging out" requirement here, and asking for admin elevation just to # install a per-user desktop tool would be a worse default. # # This script's own relay/environment identity — which downloads # server, which cinder relay, which license environment, which local # UI port, which boot-start instance name — is NOT configurable via # environment variables at all, deliberately (same change, same # reasoning, as cindertunnel-install.sh's own copy of this comment): # it used to be (each of BASE_URL/UI_ADDR/SERVER/LICENSE_ENV/INSTANCE # independently overridable via a CINDERTUNNEL_* env var), and that was # the direct cause of a real, repeated live confusion — a value set in # one PowerShell session (`iwr | iex` evaluates in the CURRENT process, # not a child one, so `$env:` assignments outlive the script that made # them for the rest of that session), or persisted outright via # [Environment]::SetEnvironmentVariable(...,'User'/'Machine'), silently # kept winning over this script's own correct, derived defaults on a # LATER, unrelated run, including a live user running the genuinely # correct script and still getting a dev4637 install with zero # explanation. Direct instruction afterward: "My environment variables # shall not mutate your script installation config." This script is # now fully self-contained on that front — the only thing that ever # decides which relay you get is which URL you fetched THIS FILE from # (baked into its own BaseUrl literal below, rewritten identically for # each published copy by deploy.sh's own sed step); nothing in your # session or your machine's persisted environment can change it. # # Two settings remain real, intentional overrides, env vars — pinning # a version and skipping the install-directory picker are a different # category of thing entirely (per-run install mechanics, not "which # environment is this"), and neither one has ever caused this kind of # confusion: # CINDERTUNNEL_VERSION default: manifest.json's own "latest" # CINDERTUNNEL_INSTALL_DIR skips the folder-picker entirely # # Needs PowerShell 5+ (ships with every Windows 10/11) and .NET's own # System.Windows.Forms (also always present) for the folder/yes-no # dialogs — no extra install needed for either. $ErrorActionPreference = 'Stop' function Get-EnvOrDefault($name, $default) { $v = [Environment]::GetEnvironmentVariable($name) if ([string]::IsNullOrEmpty($v)) { return $default } return $v } # Not an env-var override — see this script's own header comment for # why that was removed outright. This literal is the ONLY thing that # differs between the copy published at cinderapps.org (this line, as # committed) and the one at dev4637.cinderapps.org (deploy.sh's own # publish step sed-rewrites this exact literal for that copy); every # other setting below derives from it. $BaseUrl = 'https://cinderapps.org/cindertunnel/downloads' # Direct request: wisp tracking on cindertunnel downloads "and such" — # a page-driven download click gets counted via the landing page's own # JS, but a bare `iwr | iex` run like this one never touches a browser # at all, so it needs its own ping. A short timeout and an empty catch # so a slow/unreachable tracking endpoint can never delay or fail a # real install — $ErrorActionPreference is 'Stop' for this whole # script, so without this try/catch any failure here would halt # everything below it, which tracking must never do. try { $trackBase = $BaseUrl -replace '/cindertunnel/downloads$', '' Invoke-WebRequest -Uri "$trackBase/v1/track?key=/cindertunnel/install-ps1" -TimeoutSec 3 -UseBasicParsing | Out-Null } catch { } $PinnedVersion = Get-EnvOrDefault 'CINDERTUNNEL_VERSION' '' # Every one of these is derived from BaseUrl alone, with no env-var # override of its own — this one script now covers what # cindertunnel-install-dev.ps1 used to be a separate wrapper for (same # change, same reasoning, as cindertunnel-install.sh's own copy of this # comment). $IsDev = $BaseUrl -match '://dev4637\.cinderapps\.org/' $UiAddr = if ($IsDev) { '127.0.0.1:8090' } else { '127.0.0.1:8089' } $Server = if ($IsDev) { 'https://noteshare.dev4637.cinderapps.org' } else { '' } $LicenseEnv = if ($IsDev) { 'dev4637' } else { '' } $Instance = if ($IsDev) { 'dev4637' } else { '' } $InstallDirOverride = Get-EnvOrDefault 'CINDERTUNNEL_INSTALL_DIR' '' # extra_args mirrors cindertunnel-install.sh's own array of the same # name — built once so every place that launches the binary below # (the Startup shortcut and the immediate post-install launch) agrees # on this instance's fixed flags. $extraArgs = @() if ($Server) { $extraArgs += @('-server', $Server) } if ($LicenseEnv) { $extraArgs += @('-license-env', $LicenseEnv) } switch ([System.Environment]::Is64BitOperatingSystem) { $true { $platform = 'windows-amd64' } $false { $platform = 'windows-386' } } Write-Host "==> Fetching $BaseUrl/manifest.json ..." $manifest = Invoke-RestMethod -Uri "$BaseUrl/manifest.json" $version = $PinnedVersion if (-not $version) { $version = $manifest.latest } Write-Host "==> Installing cindertunnel $version for $platform..." $entry = $manifest.versions | Where-Object { $_.version -eq $version } if (-not $entry) { Write-Error "version $version not found in manifest" exit 1 } $relPath = $entry.platforms.$platform if (-not $relPath) { Write-Error "no $platform build for version $version — see $($BaseUrl -replace '/downloads$','')/ for what's actually available." exit 1 } $expectedSha = $entry.sha256.$platform # Where to install: CINDERTUNNEL_INSTALL_DIR skips asking entirely, # same convention as the bash installer. Otherwise a native folder # browser — defaulting to %LocalAppData%\cindertunnel (or # %LocalAppData%\cindertunnel-$Instance for a named instance, same # collision-avoidance reasoning as the bash installer's own # $INSTANCE), writable with no elevation, rather than Program Files # (which would need an admin prompt for what is, per this script's # own header comment, meant to be a no-friction per-user install) — # with Browse/accept-default both one dialog, not a typed prompt. if ($Instance) { $defaultInstallDir = Join-Path $env:LOCALAPPDATA "cindertunnel-$Instance" } else { $defaultInstallDir = Join-Path $env:LOCALAPPDATA 'cindertunnel' } # Direct request: "remember which folder we installed to, so just # auto pick that, next update" — mirrors the bash installer's own # marker file (see cindertunnel-install.sh's comment for the full # reasoning). A successful install writes $installDir here; the next # run reads it back and, if found, confirms it with a yes/no dialog # (direct follow-up — see below) rather than the full folder browser. # CINDERTUNNEL_INSTALL_DIR, when set, still wins outright over this, # same as always. $markerDir itself (where every instance's own # marker file lives) stays shared/fixed regardless of $Instance — only # the FILENAME is suffixed, same as the bash installer's own # $MARKER_FILE. $markerDir = Join-Path $env:LOCALAPPDATA 'cindertunnel' if ($Instance) { $markerFile = Join-Path $markerDir "last-install-dir-$Instance.txt" } else { $markerFile = Join-Path $markerDir 'last-install-dir.txt' } $installDir = $InstallDirOverride if ($installDir) { # Direct report, found live: CINDERTUNNEL_INSTALL_DIR set # PERSISTENTLY ([Environment]::SetEnvironmentVariable(...,'User'/ # 'Machine'), or set once during earlier testing and never # cleared) silently wins outright over both the remember/confirm # dialog below AND the first-run folder browser — by design, the # override is meant to always win — but with nothing printed, # that's indistinguishable from the script having silently # decided to skip asking on its own, which is exactly what it # looked like running this for real. Announce it unconditionally # so this is never a silent surprise again. Write-Host "==> CINDERTUNNEL_INSTALL_DIR is set — installing to $installDir (unset it to be asked again)" } if (-not $installDir -and (Test-Path $markerFile)) { $remembered = (Get-Content -Path $markerFile -Raw -ErrorAction SilentlyContinue) if ($remembered) { $remembered = $remembered.Trim() } if ($remembered) { # Direct follow-up to A081: ask, rather than silently reusing # it — a remembered directory is almost always right (an # update), but a yes/no confirmation costs nothing and catches # the rare case where it isn't (moved the install by hand, # switching users). Defaults to Yes, same reasoning as the # bash installer's own identical confirmation. Add-Type -AssemblyName System.Windows.Forms $useRemembered = [System.Windows.Forms.MessageBox]::Show( "Use previous install folder ($remembered)?", 'cindertunnel', [System.Windows.Forms.MessageBoxButtons]::YesNo, [System.Windows.Forms.MessageBoxIcon]::Question, [System.Windows.Forms.MessageBoxDefaultButton]::Button1 ) if ($useRemembered -eq [System.Windows.Forms.DialogResult]::Yes) { $installDir = $remembered Write-Host "==> Using previously remembered install directory: $installDir" } } } if (-not $installDir) { # Reached on a genuinely first-ever run (or if $markerFile was # deleted by hand), or right after answering "No" above. Add-Type -AssemblyName System.Windows.Forms $dialog = New-Object System.Windows.Forms.FolderBrowserDialog $dialog.Description = "Choose a folder for cindertunnel (Cancel to use the default: $defaultInstallDir)" $dialog.SelectedPath = $defaultInstallDir $dialog.ShowNewFolderButton = $true $result = $dialog.ShowDialog() if ($result -eq [System.Windows.Forms.DialogResult]::OK -and $dialog.SelectedPath) { $installDir = $dialog.SelectedPath } else { $installDir = $defaultInstallDir } } New-Item -ItemType Directory -Force -Path $installDir | Out-Null $tmp = Join-Path $env:TEMP "cindertunnel-install-$([guid]::NewGuid())" New-Item -ItemType Directory -Force -Path $tmp | Out-Null try { $archiveName = Split-Path -Leaf $relPath $archivePath = Join-Path $tmp $archiveName Write-Host "==> Downloading $relPath..." Invoke-WebRequest -Uri "$BaseUrl/$relPath" -OutFile $archivePath if ($expectedSha) { Write-Host "==> Verifying SHA-256..." $actualSha = (Get-FileHash -Path $archivePath -Algorithm SHA256).Hash.ToLower() if ($actualSha -ne $expectedSha) { Write-Error "checksum mismatch — expected $expectedSha, got $actualSha. Not installing." exit 1 } Write-Host " OK ($actualSha)" } else { Write-Warning "manifest has no checksum for this platform/version — installing unverified." } # Every release tarball is .tar.gz (deploy/release-cindertunnel.sh # packages every platform identically, Windows included) — # tar.exe has shipped standard on Windows since Windows 10 1803, # so this needs no extra dependency. tar -xzf $archivePath -C $tmp if ($LASTEXITCODE -ne 0) { throw "tar extraction failed" } Copy-Item -Path (Join-Path $tmp 'cindertunnel.exe') -Destination (Join-Path $installDir 'cindertunnel.exe') -Force } finally { Remove-Item -Recurse -Force $tmp -ErrorAction SilentlyContinue } New-Item -ItemType Directory -Force -Path $markerDir | Out-Null Set-Content -Path $markerFile -Value $installDir Write-Host "==> Installed $version to $installDir\cindertunnel.exe" $exePath = Join-Path $installDir 'cindertunnel.exe' $argLine = "-ui-addr $UiAddr" foreach ($a in $extraArgs) { $argLine += " `"$a`"" } # Start Menu shortcut — always created; this is how a GUI install is # found again later, the equivalent of the bash installer putting the # binary on PATH. $startMenuDir = Join-Path $env:APPDATA 'Microsoft\Windows\Start Menu\Programs' $shell = New-Object -ComObject WScript.Shell $shortcut = $shell.CreateShortcut((Join-Path $startMenuDir 'cindertunnel.lnk')) $shortcut.TargetPath = $exePath $shortcut.Arguments = $argLine $shortcut.WorkingDirectory = $installDir $shortcut.Description = 'cindertunnel' $shortcut.Save() Write-Host "==> Created Start Menu shortcut" # Run-at-login: a per-user Startup-folder shortcut (not a registry Run # key — a real .lnk there is visible in Task Manager's own Startup tab # and removable the same way a human would expect, unlike a hidden # registry value) — asked via a native yes/no box, default No, same # "don't silently decide this" reasoning as the bash installer's own # boot-start prompt. Add-Type -AssemblyName System.Windows.Forms $startAtLogin = [System.Windows.Forms.MessageBox]::Show( 'Start cindertunnel automatically when you log in?', 'cindertunnel', [System.Windows.Forms.MessageBoxButtons]::YesNo, [System.Windows.Forms.MessageBoxIcon]::Question, [System.Windows.Forms.MessageBoxDefaultButton]::Button2 ) if ($startAtLogin -eq [System.Windows.Forms.DialogResult]::Yes) { $startupDir = Join-Path $env:APPDATA 'Microsoft\Windows\Start Menu\Programs\Startup' $startupShortcut = $shell.CreateShortcut((Join-Path $startupDir 'cindertunnel.lnk')) $startupShortcut.TargetPath = $exePath $startupShortcut.Arguments = $argLine $startupShortcut.WorkingDirectory = $installDir $startupShortcut.WindowStyle = 7 # minimized — no console flash on login; the tray icon is the UI $startupShortcut.Save() Write-Host "==> Will start automatically at login" } Write-Host "==> Starting cindertunnel now..." Start-Process -FilePath $exePath -ArgumentList $argLine -WorkingDirectory $installDir -WindowStyle Minimized Write-Host "Running — look for the cindertunnel icon in your system tray, or open http://$UiAddr for the web admin UI. Re-run this script any time to update."