development environment: editors and shells, Python, the Visual Studio 2022
toolchain (including the Clang and Windows XP targeting toolsets), the Windows
Driver Kit, and a handful of analysis tools (Sysinternals, OpenCppCoverage,
-BinSkim, the Windows Performance Toolkit).
+BinSkim, the Windows Performance Toolkit). It also sets the box up to be driven
+remotely: OpenSSH Server plus an rsync build for Windows, which is what makes a
+throwaway VM reachable from a Linux host.
## Files
| File | Purpose |
| --- | --- |
-| `setup-windows.bat` | Entry point. Runs the non-elevated, per-user half (winget installs, user `PATH` edits, global git config), then launches the elevated half and prints its log. |
-| `setup-windows-with-uac.ps1` | The elevated half, started via UAC by the batch file. Enables `ssh-agent`, installs Visual Studio 2022 Community with the required components, the WDK, and the Windows Performance Toolkit. Can also be run directly from an Administrator prompt. |
+| `setup-windows.bat` | Entry point. Runs the winget installs, then launches the elevated half and prints its log, then runs the non-elevated script. |
+| `setup-windows-no-uac.ps1` | The non-elevated, per-user half: WinMerge and BinSkim on the user `PATH`, and the global git config (identity, plus `core.sshCommand`). Can also be run directly from an ordinary prompt. |
+| `setup-windows-with-uac.ps1` | The elevated half, started via UAC by the batch file. Enables `ssh-agent`, installs the OpenSSH Client and Server capabilities and starts `sshd`, unpacks the `rsync-windows` release zip for this architecture (`rsync.exe` plus the `ssh.exe` it runs) into `C:\Tools\rsync` on the machine `PATH`, then installs Visual Studio 2022 Community with the required components, the WDK, and the Windows Performance Toolkit, and finally grants one ordinary account the rights to collect ETW traces without elevation. Can also be run directly from an Administrator prompt — pass `-TraceUser DOMAIN\user` to say who gets those rights, and `-EtwRightsOnly` to do that step alone. |
+| `setup-windows-7-test-env.bat` | Prepares a **Windows 7 VM** as a test target driven from the host by `VBoxManage guestcontrol`. Copy it into the guest and run it there; it is idempotent, so re-run it after any snapshot restore. The per-user half needs no UAC (crash-dialog suppression, no screen blanking, a staging directory, the shared folder on `Z:`); the machine-wide half is skipped with a notice unless run elevated inside the guest. It then reports what the box can actually test: DWM composition, printers, audio capture devices. |
## Usage
-1. **Edit `setup-windows.bat` first.** The global git identity near the middle of
- the file is placeholder text:
+1. **Edit `setup-windows-no-uac.ps1` first.** The global git identity near the
+ top of the file is empty:
- ```bat
- git config --global user.name "PLACEHOLDER_NAME"
- git config --global user.email "PLACEHOLDER_EMAIL"
+ ```powershell
+ $GitUserName = '' # e.g. 'Ada Lovelace'
+ $GitUserEmail = '' # e.g. 'ada@example.com'
```
- Substitute your own name and email, or comment both lines out to keep your
- identity per-repository.
+ Fill in your own name and email, or leave them empty to keep your identity
+ per-repository - the script skips `user.name` / `user.email` rather than
+ writing a placeholder, and says so. Everything else in that script is set
+ either way.
2. Run it from a normal (non-elevated) prompt:
- The elevated half writes a transcript to `setup-windows-uac.log` next to the
script; the batch file prints it when the elevated window closes. The log is
gitignored, as it contains local paths.
-- Both halves are idempotent — re-running skips anything already installed.
+- **git uses a Win32-OpenSSH client.** `setup-windows-no-uac.ps1` sets
+ `core.sshCommand`. Git for Windows otherwise prefers its own bundled MSYS2
+ `ssh.exe`, which cannot reach the Windows `ssh-agent` service that the elevated
+ half enables - Win32-OpenSSH publishes the agent on a named pipe the MSYS2
+ build does not speak. Without this, keys loaded with `ssh-add` from PowerShell
+ are invisible to `git`, and a push falls back to hunting for a key file and
+ prompting for its passphrase. The value uses forward slashes on purpose: git
+ parses `core.sshCommand` with shell quoting rules, in which a backslash is an
+ escape character.
+- **Which `ssh.exe` git gets.** `C:\Tools\rsync\ssh.exe` — the fast build the
+ elevated half unpacks — if it is there, `%WINDIR%/System32/OpenSSH/ssh.exe`
+ otherwise. It is the same client with the same `~/.ssh`, agent and
+ `known_hosts`; the difference is the stdin pump, and without it anything git
+ *pushes* is capped at ~17 MB/s. Candidates are tried by **running** them
+ (`ssh -V`), not by `Test-Path`: the fast build needs a `libcrypto.dll` that an
+ image without the OpenSSH Client capability does not have, and a client that
+ will not start should be found here rather than on the next `git push`.
+ This is also why `setup-windows.bat` now runs the elevated half **first** — the
+ fast `ssh.exe` has to exist before the git config step can prefer it. The
+ non-elevated half still runs even when the elevated one failed; it just falls
+ back. Re-run `setup-windows-no-uac.ps1` on its own at any time to re-pick.
+- All three scripts are idempotent — re-running skips anything already installed.
+ BinSkim in particular checks NuGet for the newest stable version *before*
+ downloading: the package is a self-contained .NET build well over 100 MB, and
+ re-provisioning an up-to-date box should not pay for it. The installed version
+ is tracked in `nupkg-version.txt` beside the tool.
+- `setup-windows-no-uac.ps1` runs its steps independently: one failing warns and
+ the rest still run, and it exits 1 if any did. The `.bat` reports that and
+ carries on to the elevated half, which is the part worth the UAC prompt. Use
+ `-Skip` to re-run a subset, e.g. `.\setup-windows-no-uac.ps1 -Skip BinSkim`.
+ Run it **non-elevated**: it writes per-user state (the `HKCU` `PATH`, the
+ `.gitconfig` under `%USERPROFILE%`), so an elevated run would configure the
+ administrator's profile instead. It warns if you do.
+- **Remote access.** OpenSSH Server is installed from the Windows on-demand
+ capability (10/1809+), set to start automatically, and given an inbound TCP 22
+ firewall rule on *all* profiles — a VM's host-only or bridged adapter is
+ routinely classified Public, which is the usual reason a running `sshd` is
+ unreachable. Windows ships no `rsync`, so a build of it
+ ([nuket/rsync-windows](https://github.com/nuket/rsync-windows)) is installed to
+ `C:\Tools\rsync` and added to the **machine** `PATH`. That last detail matters:
+ the remote end of an `rsync` runs non-interactively, with no login shell, and
+ Win32-OpenSSH builds that environment from the registry `PATH` rather than from
+ a profile. Key auth needs `~/.ssh/authorized_keys` ACL'd to just you and
+ `SYSTEM`; accounts in the Administrators group use
+ `C:\ProgramData\ssh\administrators_authorized_keys` instead.
+- **rsync brings its own `ssh.exe`.** That release ships as one zip per
+ architecture — `rsync-windows-x64.zip` / `rsync-windows-x86.zip`, each holding
+ `rsync.exe`, an `ssh.exe`, `COPYING.txt` and `NOTICE-ssh.txt` — and the
+ elevated half picks the zip for the OS bitness, verifies it against the
+ published `.sha256`, and unpacks the pair together. Together is the point:
+ `rsync.exe` prefers an `ssh.exe` sitting in its own directory, and the release
+ builds one because the client Windows ships reads its stdin 3 KB at a time,
+ which holds a transfer *from* the box at ~17 MB/s however fast the link is.
+ Nothing else about it differs — same `~/.ssh`, same `ssh-agent`, same
+ `known_hosts` — and a bare `ssh` still resolves to the in-box client, which
+ sits ahead of `C:\Tools\rsync` on the `PATH`. It links against the
+ `libcrypto.dll` the **OpenSSH Client** capability puts in `System32` (Windows'
+ own LibreSSL, which uses AES-NI) and ships no copy of its own, so the elevated
+ half installs that capability first and falls back to `rsync.exe` alone,
+ warning, on an image that will not offer it.
+- The `rsync` download follows the `releases/latest/download/` redirect rather
+ than the GitHub API: unauthenticated API calls are rate-limited to 60/hour per
+ IP, which a provisioning run behind a shared NAT can genuinely exhaust. To hold
+ a box on a known build, pin the tag in `$RsyncUrl`
+ (`.../releases/download/<tag>/<asset>`) instead.
- Visual Studio is installed in three labelled passes (base workload, Clang/LLVM,
XP toolset) so a failure identifies which component group is responsible.
+- **Windows Performance Analyzer is not part of Visual Studio.** VS has its own
+ Performance Profiler, which is a different, `.diagsession`-based tool and
+ cannot open an `.etl`. WPA ships with `xperf` and `wpr` in the Windows
+ Performance Toolkit, which exists in exactly two places: as an optional
+ *feature* of the Windows SDK (`OptionId.WindowsPerformanceToolkit`) and inside
+ the Windows ADK, which bundles the same toolkit. Whether the SDK install that
+ Visual Studio performs selects that feature varies by version, so the elevated
+ half **detects first** — `%ProgramFiles(x86)%\Windows Kits\10\Windows
+ Performance Toolkit`, its 64-bit twin, and the ADK location — and only falls
+ back to `winget install Microsoft.WindowsADK` when nothing is there. It then
+ re-asserts that directory on the machine `PATH` (the toolkit's own installer
+ usually does this, and the Start Menu gets *Windows Kits > Windows Performance
+ Toolkit* shortcuts for WPA and WPR). To install just the toolkit instead of the
+ whole ADK, run the standalone SDK setup with
+ `winsdksetup.exe /features OptionId.WindowsPerformanceToolkit /q`. A newer WPA
+ also exists in the Microsoft Store (`winget install --id 9N0W1B2BXGNZ --source
+ msstore`); it is not installed here because the Store source needs an
+ interactive, signed-in session, which the unattended elevated half does not
+ have.
+- **Tracing without a UAC prompt.** `xperf` and `wpr` fail for a standard user in
+ two different ways, because two different things are missing:
+
+ ```text
+ xperf -on base -> NT Kernel Logger: Access is denied. (0x5)
+ wpr -start GeneralProfile -> Failed to enable the policy to profile system performance.
+ ```
+
+ Three separate things are in the way, and all three have to be dealt with:
+
+ 1. Creating or controlling *any* ETW session — even a user-mode one naming a
+ single provider — is checked against the security descriptor ETW keeps per
+ provider GUID under `HKLM\SYSTEM\CurrentControlSet\Control\WMI\Security`,
+ whose **default** grants the session-control rights to SYSTEM,
+ Administrators, the service accounts and `BUILTIN\Performance Log Users`,
+ and to nobody else.
+ 2. Switching on the *kernel* provider needs the `SeSystemProfilePrivilege` user
+ right ("Profile system performance"), held by default only by
+ Administrators and `NT SERVICE\WdiServiceHost` — that is the one `wpr`
+ names.
+ 3. The kernel logger is not covered by that default descriptor.
+ `SystemTraceControlGuid` — the session both `xperf -on` and `wpr` drive —
+ carries an explicit one that does not mention Performance Log Users. With
+ 1 and 2 in place a user-mode session starts and the privilege is held, and
+ `xperf -on base` *still* answers "NT Kernel Logger: Access is denied" while
+ `wpr`'s error changes to a bare `0x80070005`; even reading that descriptor
+ comes back access-denied, which is the tell. So an ACE for the group is
+ added with `EventAccessControl`, `TRACELOG_ACCESS_KERNEL_LOGGER` included.
+
+ The privilege and the ACE both go to the **group**, and the account then goes
+ into the group, so membership alone is the switch and enabling another account
+ afterwards is just `net localgroup "Performance Log Users" <user> /add`.
+ `SeDebugPrivilege` is deliberately *not* granted: CPU sampling and stack walks
+ of your own processes do not need it, and it is equivalent to handing out
+ administrator. Worth being clear about the cost: a member of that group can
+ capture system-wide kernel traces — process, image, file and registry activity
+ across every account on the box, paths and command lines included.
+
+ Two consequences worth knowing. A privilege and a group membership are both
+ read into the access token **at logon**, so the account must sign out and back
+ in for 1 and 2 — any new logon does it, and an `ssh` login into the box is the
+ quick way to check without dropping the desktop. The ACE in 3 is machine state
+ instead, and a logon does nothing for it: ETW reads these descriptors into a
+ cache, so it takes a **reboot** — with the ACE written and readable, `xperf -on
+ base` was still denied from a fresh shell on the running system. Plan on both
+ on a first run. And this only
+ helps a **non-admin** account: UAC hands an administrator a filtered token
+ carrying just five harmless privileges, so an admin's ordinary shell still
+ cannot trace however the policy reads. Verify from the target account,
+ unelevated:
+
+ ```powershell
+ whoami /priv | findstr SeSystemProfilePrivilege
+ xperf -on base ; xperf -stop C:\Temp\trace.etl
+ ```
+
+ Analysis never needed any of this — `wpa.exe` opens an existing `.etl` as a
+ plain user. This is only about collection.
+
+ The rights step runs **first** in the elevated half, and `-EtwRightsOnly` runs
+ it and nothing else. It is seconds of LSA and registry work where a full run is
+ dominated by the three Visual Studio passes, which take minutes even with
+ nothing to do:
+
+ ```powershell
+ Start-Process powershell -Verb RunAs -ArgumentList '-NoProfile','-ExecutionPolicy','Bypass',
+ '-File','<repo>\setup-windows-with-uac.ps1','-TraceUser','DOMAIN\user','-EtwRightsOnly'
+ ```
- The scripts were extracted from a native Windows project, so the component
selection is tuned for that: Spectre-mitigated runtimes, the v141/XP toolset,
and driver-kit headers. Trim the component lists in the `.ps1` if you don't