Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions .github/workflows/alpha.yml
Original file line number Diff line number Diff line change
Expand Up @@ -91,19 +91,21 @@ jobs:
GH_TOKEN: ${{ github.token }}
GH_REPO: ${{ github.repository }}
ALPHA_NOTE: ${{ inputs.note }}
ALPHA_BRANCH: ${{ github.ref_name }} # a branch name can hold $( ) and backticks
run: |
$ErrorActionPreference = 'Continue'
$PSNativeCommandUseErrorActionPreference = $false
$tag = '${{ steps.ver.outputs.tag }}'
$exe = '${{ steps.asset.outputs.path }}'
$note = $env:ALPHA_NOTE # data, never spliced into the script
$branch = $env:ALPHA_BRANCH
$body = @"
**Alpha build - not a release.** Installs over a normal Wind install and shares its
settings; roll back by running the stable installer from the Latest release.

| | |
|---|---|
| branch | ``${{ github.ref_name }}`` |
| branch | ``$branch`` |
| commit | ``${{ steps.ver.outputs.sha7 }}`` |
| base version | ``${{ steps.ver.outputs.version }}`` |
| sha256 | ``${{ steps.asset.outputs.sha256 }}`` |
Expand Down Expand Up @@ -138,10 +140,12 @@ jobs:

- name: Summary
shell: pwsh
env:
ALPHA_BRANCH: ${{ github.ref_name }}
run: |
"## Wind ${{ steps.ver.outputs.alpha }} (pre-release)" >> $env:GITHUB_STEP_SUMMARY
"" >> $env:GITHUB_STEP_SUMMARY
"- branch ``${{ github.ref_name }}``" >> $env:GITHUB_STEP_SUMMARY
"- branch ``$env:ALPHA_BRANCH``" >> $env:GITHUB_STEP_SUMMARY
"- tag ``${{ steps.ver.outputs.tag }}``" >> $env:GITHUB_STEP_SUMMARY
"- sha256 ``${{ steps.asset.outputs.sha256 }}``" >> $env:GITHUB_STEP_SUMMARY
"- the stable release keeps the Latest badge; this does not" >> $env:GITHUB_STEP_SUMMARY
4 changes: 4 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ jobs:
shell: cmd
working-directory: ui
run: npm ci
- name: Keybind rules, JS half (node --test)
shell: cmd
working-directory: ui
run: npm run test:rules
- name: Build settings host and UI (WindConfig.exe, ui/dist)
shell: cmd
run: build.bat config
Expand Down
10 changes: 6 additions & 4 deletions docs/architecture/11-build-test-release.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,23 +30,25 @@ headlessly, so the unit tests are the only verification loop that runs everywher
- The `test` target compiles `tests\*.cpp` with `/DWIND_TESTS` against only the pure sources. The
authoritative list is the `:test` target in `build.bat`; today it is `transform`,
`zoom_controller`, `config`, `profiles`, `cursor_mapper`, `lock_detector`, `cursor_lock`,
`mouse_ballistics`, `crosshair`, `config_ui/ini_edit`, `logging` and `tray_items`.
`mouse_ballistics`, `crosshair`, `config_ui/ini_edit`, `logging`, `tray_items` and `hitch_record`.
- Header-only pure modules (`engine_pick.h`, `drag_follow.h`, `hdr_scale.h`, `keybind_rules.h`,
`config_ui/wind_watchdog.h` and others) ride in through their test files.
- One test file per module (`tests/test_<module>.cpp`). A new pure `.cpp` also goes into the
`:test` list.
- Files that straddle the line put their OS half under `#ifndef WIND_TESTS`. `src/config.cpp` is
the example: `ParseConfig` and `StripUiOnlyKeys` above, `LoadConfig` and the only
`#include <windows.h>` below.
- `tests/fixtures/keybind_cases.txt` is shared by the C++ and the JavaScript bind rules.
- `tests/fixtures/keybind_cases.txt` is shared by the C++ and the JavaScript bind rules. The JS half
is `ui/tests/keybind-rules.nodetest.mjs`, run by `npm run test:rules` in `ui/` (`node --test`, no
browser) and by CI.

**UI suite.** `ui/tests/` holds 12 Playwright specs, run with `npx playwright test` in `ui/` (the
**UI suite.** `ui/tests/` holds 11 Playwright specs, run with `npx playwright test` in `ui/` (the
config starts the Vite server). A fake `window.chrome.webview` answers the host's messages and
records them in `window.__msgs`. When you add a bridge message to `HandleWebMessage`, extend the
mock in the same change. Details in [09](09-settings-ui.md).

**CI** (`.github/workflows/build.yml`, on pull requests and pushes to `main`) builds the app and
runs the doctest suite. It does not run the UI suite; that is a local gate for UI changes.
runs the doctest suite. It also runs the JS keybind-rule test. The Playwright UI suite is a local gate for UI changes.

## Local UIAccess deploy

Expand Down
12 changes: 8 additions & 4 deletions installer/app.nsh
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@

!include "LogicLib.nsh"

!define WV2_CLIENT "SOFTWARE\WOW6432Node\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}"
; The machine-wide install is under WOW6432Node; the per-user install is not (no WOW6432Node in HKCU).
!define WV2_CLIENT_HKLM "SOFTWARE\WOW6432Node\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}"
!define WV2_CLIENT_HKCU "Software\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}"

; Wind's own two kernel objects. Both are Local\, i.e. session-scoped, and UAC elevation
; stays inside the session, so the elevated installer and the user's Wind see the same
Expand Down Expand Up @@ -117,12 +119,14 @@ Var LicenceDir ; where "Read the full licence" put its copy, empty until then
; ---- WebView2 ----------------------------------------------------------------
; WindConfig.exe paints an empty shell without the Evergreen runtime. Microsoft's
; uninstaller leaves `pv` behind set to "0.0.0.0" rather than deleting it, so a value
; being present is not proof (src\webview2_probe.h pins that rule and tests it).
; being present is not proof. src\webview2_probe.h is the tested statement of that rule; NSIS
; cannot call it, so this macro mirrors it for the two values the uninstaller leaves.
!macro WIND_ENSURE_WEBVIEW2
StrCpy $0 ""
ReadRegStr $0 HKLM "${WV2_CLIENT}" "pv"
ReadRegStr $0 HKLM "${WV2_CLIENT_HKLM}" "pv"
${If} $0 == ""
ReadRegStr $0 HKCU "${WV2_CLIENT}" "pv"
${OrIf} $0 == "0.0.0.0"
ReadRegStr $0 HKCU "${WV2_CLIENT_HKCU}" "pv"
${EndIf}
${If} $0 == ""
${OrIf} $0 == "0.0.0.0"
Expand Down
4 changes: 2 additions & 2 deletions src/version.h
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

#define WIND_VER_MAJOR 0
#define WIND_VER_MINOR 24
#define WIND_VER_PATCH 8
#define WIND_VER_PATCH 9

// String form for logs/snapshot/UI. Keep in sync with the numeric parts above.
#define WIND_VERSION_STR "0.24.8"
#define WIND_VERSION_STR "0.24.9"
4 changes: 3 additions & 1 deletion src/webview2_probe.h
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@
// only subtlety is that Microsoft's uninstaller leaves the value behind set to "0.0.0.0"
// rather than deleting it, so a non-empty value is not proof of presence.
// WindConfig.exe paints an empty shell without the runtime, which is why this is checked.
// NO <windows.h>: pure, so the rule is testable.
// NO <windows.h>: pure, so the rule is testable. Nothing in the C++ binaries calls it: the check
// that matters runs in the NSIS installer (installer\app.nsh, WIND_ENSURE_WEBVIEW2), which mirrors
// this rule and cannot include it. Keep the two in step.
#include <string>
#include "installer_state.h"

Expand Down
2 changes: 1 addition & 1 deletion tests/fixtures/keybind_cases.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
# key <vk hex> <mods> <verdict> mods: bit 1 Ctrl, 2 Alt, 4 Shift, 8 Win
# wheel <mods> <verdict>
# click <button> <mods> <verdict> button: 1/2 side, 3 left, 4 right, 5 middle, 6 wheel up, 7 wheel down
# Read by tests/test_keybind_rules.cpp (C++ rules) and ui/tests/keybind-rules.spec.js (UI mirror).
# Read by tests/test_keybind_rules.cpp (C++ rules) and ui/tests/keybind-rules.nodetest.mjs (UI mirror).
# verdicts: ok never modifier notalone shifttypes altgr system windows needsmod ctrlalone shiftalone

# alone: navigation, arrows, Delete, F-keys, Pause, ScrollLock, numpad are fine
Expand Down
12 changes: 11 additions & 1 deletion tests/test_keybind_rules.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,21 @@
#include <fstream>
#include <sstream>
#include <string>
#include <vector>
using namespace wind;

// Runs every case in the shared list (also read by the UI mirror's tests).
TEST_CASE("keybind safety rules match the shared case list (#285)") {
std::ifstream f("tests/fixtures/keybind_cases.txt");
// The cwd varies (repo root from build.bat, a build dir elsewhere), so also try the folder
// this file sits in, then walk up from the cwd.
std::string here = __FILE__;
const size_t cut = here.find_last_of("/\\");
std::vector<std::string> tries;
if (cut != std::string::npos) tries.push_back(here.substr(0, cut) + "/fixtures/keybind_cases.txt");
for (const char* up : { "", "../", "../../", "../../../" })
tries.push_back(std::string(up) + "tests/fixtures/keybind_cases.txt");
std::ifstream f;
for (const std::string& p : tries) { f.open(p); if (f.good()) break; f.clear(); }
REQUIRE(f.good());
std::string line;
int cases = 0;
Expand Down
File renamed without changes.
5 changes: 0 additions & 5 deletions tests/test_smoke.cpp

This file was deleted.

156 changes: 83 additions & 73 deletions tools/installer_check.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,11 @@ if (-not (Test-Path $setup)) {
ForEach-Object { [pscustomobject]@{ Store = $s; Raw = $_.RawData } }
})

# The installer closes a running Wind/WindTray (that is its job), which would also quit the
# developer's own session. Remember what is running and start it again afterwards.
$wasRunning = @(Get-Process -Name Wind, WindTray -ErrorAction SilentlyContinue |
ForEach-Object { $_.Path } | Where-Object { $_ })

$scratch = Join-Path $env:TEMP 'wind-installer-check'
if (Test-Path $scratch) { Remove-Item -LiteralPath $scratch -Recurse -Force }

Expand All @@ -140,89 +145,94 @@ if (-not (Test-Path $setup)) {
$appData = Join-Path $env:LOCALAPPDATA 'Wind'
$seeded = $false
$marker = Join-Path $appData 'installer-check-marker.txt'
if (-not (Test-Path $appData)) {
New-Item -ItemType Directory -Force $appData | Out-Null
Set-Content -LiteralPath $marker -Value 'seeded by tools/installer_check.ps1' -NoNewline
$seeded = $true
}
try {
if (-not (Test-Path $appData)) {
New-Item -ItemType Directory -Force $appData | Out-Null
Set-Content -LiteralPath $marker -Value 'seeded by tools/installer_check.ps1' -NoNewline
$seeded = $true
}

# /D must be the last argument and unquoted. That is an NSIS rule, not a typo.
Start-Process $setup -ArgumentList '/S', "/D=$scratch" -Wait
Check "silent install placed Wind.exe" { Test-Path (Join-Path $scratch 'Wind.exe') }
Check "silent install placed WindConfig.exe" { Test-Path (Join-Path $scratch 'WindConfig.exe') }
Check "silent install placed WindTray.exe" { Test-Path (Join-Path $scratch 'WindTray.exe') }
Check "silent install placed ui\dist" { Test-Path (Join-Path $scratch 'ui\dist\index.html') }
# Local signing (issue #261): only when this build packed the uiAccess variant.
if (Test-Path (Join-Path $root 'WindUA.exe')) {
$sig = Get-AuthenticodeSignature (Join-Path $scratch 'Wind.exe')
Check "local signing: Wind.exe signature is Valid" { $sig.Status -eq 'Valid' }
Check "local signing: signed by CN=Wind Local Signing" { $sig.SignerCertificate.Subject -eq 'CN=Wind Local Signing' }
Check "local signing: WindTray.exe signature is Valid" {
(Get-AuthenticodeSignature (Join-Path $scratch 'WindTray.exe')).Status -eq 'Valid'
# /D must be the last argument and unquoted. That is an NSIS rule, not a typo.
Start-Process $setup -ArgumentList '/S', "/D=$scratch" -Wait
Check "silent install placed Wind.exe" { Test-Path (Join-Path $scratch 'Wind.exe') }
Check "silent install placed WindConfig.exe" { Test-Path (Join-Path $scratch 'WindConfig.exe') }
Check "silent install placed WindTray.exe" { Test-Path (Join-Path $scratch 'WindTray.exe') }
Check "silent install placed ui\dist" { Test-Path (Join-Path $scratch 'ui\dist\index.html') }
# Local signing (issue #261): only when this build packed the uiAccess variant.
if (Test-Path (Join-Path $root 'WindUA.exe')) {
$sig = Get-AuthenticodeSignature (Join-Path $scratch 'Wind.exe')
Check "local signing: Wind.exe signature is Valid" { $sig.Status -eq 'Valid' }
Check "local signing: signed by CN=Wind Local Signing" { $sig.SignerCertificate.Subject -eq 'CN=Wind Local Signing' }
Check "local signing: WindTray.exe signature is Valid" {
(Get-AuthenticodeSignature (Join-Path $scratch 'WindTray.exe')).Status -eq 'Valid'
}
Check "local signing: the signing key is gone" {
-not (Get-ChildItem Cert:\LocalMachine\My | Where-Object Subject -eq 'CN=Wind Local Signing')
}
Check "local signing: exactly one trusted root" {
@(Get-ChildItem Cert:\LocalMachine\Root | Where-Object Subject -eq 'CN=Wind Local Signing').Count -eq 1
}
}
Check "local signing: the signing key is gone" {
-not (Get-ChildItem Cert:\LocalMachine\My | Where-Object Subject -eq 'CN=Wind Local Signing')
Check "ARP DisplayVersion is $version" {
(Get-ItemProperty $ARP -ErrorAction SilentlyContinue).DisplayVersion -eq $version
}
Check "local signing: exactly one trusted root" {
@(Get-ChildItem Cert:\LocalMachine\Root | Where-Object Subject -eq 'CN=Wind Local Signing').Count -eq 1
Check "Run value points at the install" {
(Get-ItemProperty $RUN -ErrorAction SilentlyContinue).Wind -match 'Wind\.exe'
}
}
Check "ARP DisplayVersion is $version" {
(Get-ItemProperty $ARP -ErrorAction SilentlyContinue).DisplayVersion -eq $version
}
Check "Run value points at the install" {
(Get-ItemProperty $RUN -ErrorAction SilentlyContinue).Wind -match 'Wind\.exe'
}

$uninst = Join-Path $scratch 'Uninstall.exe'
if (Test-Path $uninst) {
# _?= keeps the uninstaller in place so -Wait actually waits for it. Without it NSIS
# copies itself to TEMP and returns immediately. The cost is that Uninstall.exe
# cannot delete itself, which is why it is not checked for below.
Start-Process $uninst -ArgumentList '/S', ('_?=' + $scratch) -Wait
Start-Sleep -Milliseconds 1200
Check "uninstall removed Wind.exe" { -not (Test-Path (Join-Path $scratch 'Wind.exe')) }
Check "uninstall removed WindTray.exe" { -not (Test-Path (Join-Path $scratch 'WindTray.exe')) }
Check "uninstall removed ui\dist" { -not (Test-Path (Join-Path $scratch 'ui')) }
Check "uninstall removed the ARP key" { $null -eq (Get-ItemProperty $ARP -ErrorAction SilentlyContinue) }
Check "uninstall removed every Wind Local Signing root" {
-not (Get-ChildItem Cert:\LocalMachine\Root, Cert:\LocalMachine\TrustedPublisher |
Where-Object Subject -eq 'CN=Wind Local Signing')
}
Check "uninstall removed the Run value" {
$null -eq (Get-ItemProperty $RUN -ErrorAction SilentlyContinue).Wind
$uninst = Join-Path $scratch 'Uninstall.exe'
if (Test-Path $uninst) {
# _?= keeps the uninstaller in place so -Wait actually waits for it. Without it NSIS
# copies itself to TEMP and returns immediately. The cost is that Uninstall.exe
# cannot delete itself, which is why it is not checked for below.
Start-Process $uninst -ArgumentList '/S', ('_?=' + $scratch) -Wait
Start-Sleep -Milliseconds 1200
Check "uninstall removed Wind.exe" { -not (Test-Path (Join-Path $scratch 'Wind.exe')) }
Check "uninstall removed WindTray.exe" { -not (Test-Path (Join-Path $scratch 'WindTray.exe')) }
Check "uninstall removed ui\dist" { -not (Test-Path (Join-Path $scratch 'ui')) }
Check "uninstall removed the ARP key" { $null -eq (Get-ItemProperty $ARP -ErrorAction SilentlyContinue) }
Check "uninstall removed every Wind Local Signing root" {
-not (Get-ChildItem Cert:\LocalMachine\Root, Cert:\LocalMachine\TrustedPublisher |
Where-Object Subject -eq 'CN=Wind Local Signing')
}
Check "uninstall removed the Run value" {
$null -eq (Get-ItemProperty $RUN -ErrorAction SilentlyContinue).Wind
}
# A silent uninstall answers the keep-or-delete prompt with /SD IDNO, so the settings
# must still be there. Deleting them silently would be the worst bug in the installer.
Check "uninstall kept %LOCALAPPDATA%\Wind" { Test-Path $appData }
if ($seeded) { Check "uninstall kept the seeded file" { Test-Path $marker } }
} else {
Check "uninstaller was written" { $false }
}
# A silent uninstall answers the keep-or-delete prompt with /SD IDNO, so the settings
# must still be there. Deleting them silently would be the worst bug in the installer.
Check "uninstall kept %LOCALAPPDATA%\Wind" { Test-Path $appData }
if ($seeded) { Check "uninstall kept the seeded file" { Test-Path $marker } }
} else {
Check "uninstaller was written" { $false }
}

# Take back only what this script created; a real settings folder is never touched.
if ($seeded) {
Remove-Item -LiteralPath $marker -Force -ErrorAction SilentlyContinue
if (-not (Get-ChildItem $appData -Force -ErrorAction SilentlyContinue)) {
Remove-Item -LiteralPath $appData -Force -ErrorAction SilentlyContinue
} finally {
# Take back only what this script created; a real settings folder is never touched.
if ($seeded) {
Remove-Item -LiteralPath $marker -Force -ErrorAction SilentlyContinue
if (-not (Get-ChildItem $appData -Force -ErrorAction SilentlyContinue)) {
Remove-Item -LiteralPath $appData -Force -ErrorAction SilentlyContinue
}
}
}

if (Test-Path $scratch) { Remove-Item -LiteralPath $scratch -Recurse -Force -ErrorAction SilentlyContinue }
if (Test-Path $scratch) { Remove-Item -LiteralPath $scratch -Recurse -Force -ErrorAction SilentlyContinue }

# Put back whatever was here before the smoke test.
if ($savedArp) {
New-Item -Path $ARP -Force | Out-Null
foreach ($n in 'DisplayName','DisplayVersion','Publisher','DisplayIcon','InstallLocation','UninstallString') {
if ($null -ne $savedArp.$n) { New-ItemProperty $ARP -Name $n -Value $savedArp.$n -PropertyType String -Force | Out-Null }
# Put back whatever was here before the smoke test.
if ($savedArp) {
New-Item -Path $ARP -Force | Out-Null
foreach ($n in 'DisplayName','DisplayVersion','Publisher','DisplayIcon','InstallLocation','UninstallString') {
if ($null -ne $savedArp.$n) { New-ItemProperty $ARP -Name $n -Value $savedArp.$n -PropertyType String -Force | Out-Null }
}
}
if ($savedRun) { New-ItemProperty $RUN -Name 'Wind' -Value $savedRun -PropertyType String -Force | Out-Null }
foreach ($r in $savedRoots) {
$st = New-Object System.Security.Cryptography.X509Certificates.X509Store($r.Store, 'LocalMachine')
$st.Open('ReadWrite')
$st.Add([System.Security.Cryptography.X509Certificates.X509Certificate2]::new([byte[]]$r.Raw))
$st.Close()
}
foreach ($exe in $wasRunning) {
if (Test-Path -LiteralPath $exe) { Start-Process -FilePath $exe -ErrorAction SilentlyContinue }
}
}
if ($savedRun) { New-ItemProperty $RUN -Name 'Wind' -Value $savedRun -PropertyType String -Force | Out-Null }
foreach ($r in $savedRoots) {
$st = New-Object System.Security.Cryptography.X509Certificates.X509Store($r.Store, 'LocalMachine')
$st.Open('ReadWrite')
$st.Add([System.Security.Cryptography.X509Certificates.X509Certificate2]::new([byte[]]$r.Raw))
$st.Close()
}
}

Expand Down
Loading
Loading