mirror of
https://github.com/l0ng-ai/tty7.git
synced 2026-09-21 16:02:20 +00:00
* fix(windows): bundle Microsoft's ConPTY so panes can answer color queries
The in-box conhost swallows a pane process's OSC 11 background query: it
never reaches tty7's emulator and no reply is ever written back, so
applications that choose a light or dark UI from the terminal background
render a dark UI under a light theme.
tty7 already answers OSC 10/11/12 from the live theme, so nothing was
missing but a pseudoconsole that forwards the question. Microsoft ships one
as a redistributable, and portable-pty already prefers a sideloaded
conpty.dll over kernel32's, so this is packaging rather than code: the pair
goes beside tty7-app.exe, where the DLL search path finds it.
Measured on Windows 11 26200, same binary, only the pair added beside it:
in-box conhost: the terminal side never sees the query; the client times
out with no reply
bundled ConPTY: the terminal side sees ESC]11;?BEL and a real pane reads
back rgb:efef/f1f1/f5f5 under catppuccin_latte, which is
the preset's exact background
The two files are one supported unit, so the release verifier fails a
package that carries only one, a mismatched pair, or the MIT notice-less
DLL. They also join PORTABLE_MANAGED_ROOTS, without which the updater would
reject every portable archive that contains them; they are deliberately not
required by verify_portable_payload, since tty7 runs without them and a
packaging slip should fail the release rather than a user's update.
build.rs stages the pair beside cargo's output so a development build does
not quietly run on the in-box host, and the daemon logs which pseudoconsole
it got.
Closes #345
* fix(windows): restage the bundled ConPTY when it goes missing
Watching only the vendored sources meant a staged copy that left the target
directory stayed gone: the build script was cached, so it never ran again to
put it back, and the build silently fell back to the in-box conhost. Cargo
treats a rerun-if-changed path that does not exist as changed, so naming the
destinations makes the staging self-healing.
Found by deleting target/debug/conpty.dll and watching the next build not
bring it back.
---------
Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com>
197 lines
9.1 KiB
PowerShell
197 lines
9.1 KiB
PowerShell
# Verifies that the Windows release artifacts carry everything the in-app
|
|
# updater requires, immediately after bundle-windows.ps1 produces them.
|
|
#
|
|
# The updater refuses to install a package it cannot recognise, and it does so
|
|
# on the user's machine, after the download, after the GUI has exited. Every
|
|
# fact it checks there is checked here instead, so a packaging mistake fails
|
|
# the release build rather than every user's next update.
|
|
#
|
|
# Mirrors, in order:
|
|
# core::update::windows_update_layout_for — the install marker
|
|
# core::update::package_for_current_install — tty7-updater.exe beside the app
|
|
# tty7-updater `windows::verify_portable_payload` — portable layout + versions
|
|
# tty7-updater `windows::extract_portable_archive` — ZIP entry rules
|
|
# tty7-updater `windows::verify_file_version` — setup.exe PE version
|
|
#
|
|
# Usage: verify-windows-package.ps1 <arch> [version]
|
|
# `version` defaults to the version in Cargo.toml, which is what the bundle
|
|
# script stamped into the artifact names.
|
|
$ErrorActionPreference = 'Stop'
|
|
|
|
$Arch = $args[0]
|
|
if (-not $Arch) { throw "usage: verify-windows-package.ps1 <arch> [version]" }
|
|
$Version = $args[1]
|
|
if (-not $Version) {
|
|
$Version = (Select-String -Path Cargo.toml -Pattern '^version\s*=\s*"([^"]+)"').Matches[0].Groups[1].Value
|
|
}
|
|
# The PE fixed-version resource carries only numeric components, so the updater
|
|
# compares the release version's numeric core against it. Keep the same split.
|
|
$VersionCore = ($Version -split '[-+]', 2)[0]
|
|
|
|
$Name = "tty7-$Version-windows-$Arch"
|
|
$Zip = "dist/$Name.zip"
|
|
$Setup = "dist/$Name-setup.exe"
|
|
$Stage = "dist/$Name"
|
|
|
|
$failures = New-Object System.Collections.Generic.List[string]
|
|
function Fail([string]$message) { $failures.Add($message) }
|
|
|
|
function Get-ProductVersion([string]$path) {
|
|
# The same string the updater reads back with VerQueryValueW
|
|
# (\StringFileInfo\<lang><cp>\ProductVersion).
|
|
(Get-Item -LiteralPath $path).VersionInfo.ProductVersion
|
|
}
|
|
|
|
function Assert-BinaryVersion([string]$path, [string]$label) {
|
|
if (-not (Test-Path -LiteralPath $path)) { Fail "$label is missing: $path"; return }
|
|
$actual = Get-ProductVersion $path
|
|
if ($actual -ne $Version) {
|
|
Fail "$label reports ProductVersion '$actual', expected '$Version'"
|
|
}
|
|
}
|
|
|
|
# Microsoft ships conpty.dll and OpenConsole.exe as one supported unit, and the
|
|
# app degrades quietly without them: `portable-pty` falls back to the in-box
|
|
# conhost, which swallows the OSC 11 background query all over again (#345).
|
|
# Neither the missing case nor the mismatched case is visible at runtime, so
|
|
# both fail the release here instead.
|
|
function Assert-ConptyPair([string]$directory, [string]$label) {
|
|
$versions = @{}
|
|
foreach ($file in 'conpty.dll', 'OpenConsole.exe') {
|
|
$path = Join-Path $directory $file
|
|
if (-not (Test-Path -LiteralPath $path -PathType Leaf)) {
|
|
Fail "$label is missing the bundled $file"
|
|
return
|
|
}
|
|
$versions[$file] = (Get-Item -LiteralPath $path).VersionInfo.FileVersion
|
|
}
|
|
if ($versions['conpty.dll'] -ne $versions['OpenConsole.exe']) {
|
|
Fail ("$label carries a mismatched ConPTY pair: conpty.dll is " +
|
|
"$($versions['conpty.dll']), OpenConsole.exe is $($versions['OpenConsole.exe'])")
|
|
}
|
|
if (-not (Test-Path -LiteralPath (Join-Path $directory 'LICENSE-ConPTY.txt') -PathType Leaf)) {
|
|
Fail "$label ships the bundled ConPTY without its MIT notice"
|
|
}
|
|
}
|
|
|
|
# ---- Portable ZIP --------------------------------------------------------
|
|
# Update rules live in the updater's extractor; the ones that can be broken by
|
|
# packaging alone are re-stated here.
|
|
if (-not (Test-Path -LiteralPath $Zip)) {
|
|
Fail "the portable archive is missing: $Zip"
|
|
} else {
|
|
Add-Type -AssemblyName System.IO.Compression.FileSystem
|
|
$archive = [System.IO.Compression.ZipFile]::OpenRead((Resolve-Path $Zip).Path)
|
|
try {
|
|
$entries = @($archive.Entries | ForEach-Object { $_.FullName })
|
|
} finally {
|
|
$archive.Dispose()
|
|
}
|
|
|
|
# `extract_portable_archive` rejects a backslash outright: the ZIP spec
|
|
# names '/' as the separator, and a mixed archive is one the updater will
|
|
# not unpack. PowerShell's archive writer has emitted both over the years.
|
|
$backslashed = @($entries | Where-Object { $_.Contains('\') })
|
|
if ($backslashed.Count -gt 0) {
|
|
Fail ("the portable archive uses backslash separators the updater rejects: " +
|
|
($backslashed -join ', '))
|
|
}
|
|
|
|
# `validate_portable_relative_path` allows only these top-level names.
|
|
$managed = @(
|
|
'tty7-app.exe', 'tty7.exe', 'tty7-updater.exe', '.tty7-portable',
|
|
'completions', 'server', 'LICENSE.txt', 'README.md',
|
|
'conpty.dll', 'OpenConsole.exe', 'LICENSE-ConPTY.txt'
|
|
)
|
|
$roots = @($entries |
|
|
ForEach-Object { ($_ -split '[\\/]', 2)[0] } |
|
|
Sort-Object -Unique)
|
|
foreach ($root in $roots) {
|
|
if ($managed -notcontains $root) {
|
|
Fail "the portable archive has a top-level entry the updater rejects: $root"
|
|
}
|
|
}
|
|
|
|
# The Inno marker and the portable marker are mutually exclusive: whichever
|
|
# one is present decides how the updater replaces this installation.
|
|
if ($entries -contains '.tty7-inno-install') {
|
|
Fail "the portable archive carries the Inno install marker"
|
|
}
|
|
|
|
$unzipped = Join-Path ([System.IO.Path]::GetTempPath()) "tty7-verify-portable-$([guid]::NewGuid())"
|
|
New-Item -ItemType Directory -Force -Path $unzipped | Out-Null
|
|
try {
|
|
[System.IO.Compression.ZipFile]::ExtractToDirectory(
|
|
(Resolve-Path $Zip).Path, $unzipped)
|
|
|
|
# `verify_portable_payload`: every required member, then the marker
|
|
# content, then the complete version of both executables.
|
|
foreach ($required in @('tty7-app.exe', 'tty7.exe', 'tty7-updater.exe',
|
|
'.tty7-portable', 'LICENSE.txt', 'README.md')) {
|
|
if (-not (Test-Path -LiteralPath (Join-Path $unzipped $required) -PathType Leaf)) {
|
|
Fail "the portable archive is missing the required file $required"
|
|
}
|
|
}
|
|
if (-not (Test-Path -LiteralPath (Join-Path $unzipped 'completions') -PathType Container)) {
|
|
Fail "the portable archive is missing the required directory completions"
|
|
}
|
|
|
|
$markerPath = Join-Path $unzipped '.tty7-portable'
|
|
if (Test-Path -LiteralPath $markerPath) {
|
|
$marker = [System.IO.File]::ReadAllBytes($markerPath)
|
|
$expected = [System.Text.Encoding]::ASCII.GetBytes('portable-v1')
|
|
if (@(Compare-Object $marker $expected -SyncWindow 0).Count -ne 0) {
|
|
Fail "the portable marker does not contain exactly 'portable-v1'"
|
|
}
|
|
}
|
|
|
|
Assert-BinaryVersion (Join-Path $unzipped 'tty7-app.exe') 'the portable tty7-app.exe'
|
|
Assert-BinaryVersion (Join-Path $unzipped 'tty7-updater.exe') 'the portable tty7-updater.exe'
|
|
Assert-ConptyPair $unzipped 'the portable archive'
|
|
} finally {
|
|
Remove-Item -Recurse -Force $unzipped -ErrorAction SilentlyContinue
|
|
}
|
|
}
|
|
|
|
# ---- Inno payload --------------------------------------------------------
|
|
# ISCC compiled the installer from this staging directory, so what it holds is
|
|
# what lands in {app}. Reading the compiled setup.exe back would need
|
|
# innoextract, which the runners do not carry.
|
|
if (-not (Test-Path -LiteralPath $Stage -PathType Container)) {
|
|
Fail "the Inno staging directory is missing: $Stage"
|
|
} else {
|
|
if (-not (Test-Path -LiteralPath (Join-Path $Stage '.tty7-inno-install') -PathType Leaf)) {
|
|
Fail "the Inno payload is missing the .tty7-inno-install marker; installed copies would never be offered an in-app update"
|
|
}
|
|
if (Test-Path -LiteralPath (Join-Path $Stage '.tty7-portable')) {
|
|
Fail "the Inno payload carries the portable marker, which would misroute the updater"
|
|
}
|
|
if (-not (Test-Path -LiteralPath (Join-Path $Stage 'tty7-updater.exe') -PathType Leaf)) {
|
|
Fail "the Inno payload is missing tty7-updater.exe"
|
|
}
|
|
Assert-BinaryVersion (Join-Path $Stage 'tty7-app.exe') 'the installed tty7-app.exe'
|
|
Assert-BinaryVersion (Join-Path $Stage 'tty7-updater.exe') 'the installed tty7-updater.exe'
|
|
Assert-ConptyPair $Stage 'the Inno payload'
|
|
}
|
|
|
|
# ---- Setup executable ----------------------------------------------------
|
|
# `verify_update` re-reads this numeric version after the GUI exits and before
|
|
# it runs the installer, so a mis-stamped VersionInfoVersion is an update that
|
|
# aborts on the user's machine.
|
|
if (-not (Test-Path -LiteralPath $Setup -PathType Leaf)) {
|
|
Fail "the Windows installer is missing: $Setup"
|
|
} else {
|
|
$info = (Get-Item -LiteralPath $Setup).VersionInfo
|
|
$actual = "$($info.FileMajorPart).$($info.FileMinorPart).$($info.FileBuildPart)"
|
|
if ($actual -ne $VersionCore) {
|
|
Fail "$Setup reports file version '$actual', expected '$VersionCore'"
|
|
}
|
|
}
|
|
|
|
if ($failures.Count -gt 0) {
|
|
foreach ($failure in $failures) { Write-Output "::error::$failure" }
|
|
throw "the Windows release package would not be updatable in place ($($failures.Count) problem(s))"
|
|
}
|
|
|
|
Write-Output "Windows package verified: markers, tty7-updater.exe and versions match $Version"
|