X-Git-Url: https://vilimpoc.org/repos/dotfiles/blobdiff_plain/b8144088df5abe5fb042890ef5bc8fd72b279b79..ea83999a6597ec98e54525a0b832967c4308978c:/setup-windows-with-uac.ps1?ds=sidebyside diff --git a/setup-windows-with-uac.ps1 b/setup-windows-with-uac.ps1 index 8d990f5..17d02ee 100644 --- a/setup-windows-with-uac.ps1 +++ b/setup-windows-with-uac.ps1 @@ -15,12 +15,38 @@ - Visual Studio 2022 Community (C++ desktop workload, Spectre libs, WDK VSIX, Win11 SDK 26100, Clang/LLVM, and the v141 + Windows XP targeting toolset) - Windows Driver Kit 10.0.26100 + - Windows Performance Toolkit - xperf, wpr and Windows Performance Analyzer + (wpa.exe) - on the machine PATH + - Performance Log Users membership for one ordinary account, so it can run + user-mode ETW sessions (xperf -start ... -on ) without elevation. + Kernel traces are NOT covered - the NT Kernel Logger is admin-only; see the + step for what was measured. Change $VsInstallerUrl below to the Professional or Enterprise bootstrapper if needed: Professional : https://aka.ms/vs/17/release/vs_professional.exe Enterprise : https://aka.ms/vs/17/release/vs_enterprise.exe #> +param( + # Account to put in Performance Log Users (see the "ETW session control" + # step, which runs first). Defaults to the interactive console user, but + # setup-windows.bat passes it explicitly: with over-the-shoulder elevation + # THIS script runs as the administrator whose credentials went into the UAC + # prompt, not as the user who started the batch file, so $env:USERNAME here + # is the wrong answer. + # + # Pass an empty string to skip it; adding an account later is one + # `net localgroup` away. + [string] $TraceUser = '', + + # Do the ETW step and nothing else. It is a group membership and no + # downloads, where a full run is dominated by the three Visual Studio + # passes, which take minutes even when they have nothing to do. It is why + # that step runs FIRST: -EtwRightsOnly is then just an early exit rather + # than a set of guards down the rest of the script. + [switch] $EtwRightsOnly +) + $ErrorActionPreference = 'Stop' function Write-Step([string]$Msg) { @@ -106,6 +132,121 @@ try { Start-Transcript -Path $LogFile -Force | Out-Null } catch {} try { +# --------------------------------------------------------------------------- +# ETW session control for an ordinary account +# +# Creating or controlling an event tracing 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. The +# default grants the session-control rights (TRACELOG_CREATE_ONDISK, +# TRACELOG_CREATE_REALTIME, TRACELOG_GUID_ENABLE, TRACELOG_LOG_EVENT) to SYSTEM, +# Administrators, the service accounts and BUILTIN\Performance Log Users, and to +# nobody else. Its own description says members "may ... enable trace providers, +# and collect event traces", and that is what membership buys: +# +# xperf -start MySession -on Microsoft-Windows-Kernel-Process -f trace.etl +# xperf -stop MySession +# +# runs unelevated for a member and is "Access is denied. (0x5)" for everyone +# else. Enough to trace your own application's providers without a UAC prompt. +# +# Membership is read into the access token at LOGON, so the account has to sign +# out and back in. Any NEW logon does it - an ssh login into this box is one, +# which is the quick way to check without dropping the desktop. +# +# WHAT THIS DOES NOT BUY: system-wide kernel traces. `xperf -on base` and +# `wpr -start` drive the NT Kernel Logger, which is reserved for Administrators +# and LocalSystem - Microsoft documents Performance Log Users access as +# explicitly NOT extending to it. Measured here, so that nobody repeats it: with +# the account in the group, SeSystemProfilePrivilege ("Profile system +# performance") granted to that group, and an explicit ACE giving the group +# TRACELOG_ACCESS_KERNEL_LOGGER on SystemTraceControlGuid - all three in place, +# across a reboot - xperf still answered +# +# xperf: error: NT Kernel Logger: Access is denied. (0x5). +# +# It is not a check an ACE overrides. Those two grants were dropped again rather +# than left on the box earning nothing, and CPU sampling and whole-system traces +# are elevated work: run xperf, wpr or VTune from an Administrator prompt. +# +# Analysis needs none of this either way - wpa.exe opens an existing .etl as a +# plain user. +# --------------------------------------------------------------------------- +Write-Step 'ETW session control (non-elevated user-mode tracing)' +$PerfLogUsersSid = 'S-1-5-32-559' # BUILTIN\Performance Log Users +try { + # Fall back to the console user when the caller did not name one: with + # over-the-shoulder elevation that is the person who started + # setup-windows.bat, which is who wants to trace. + $target = $TraceUser + if (-not $target) { + $target = (Get-CimInstance Win32_ComputerSystem -ErrorAction SilentlyContinue).UserName + if ($target) { Write-Host " No -TraceUser given; using the console user $target" } + } + + if (-not $target) { + Write-Warning 'No account to add to Performance Log Users (pass -TraceUser DOMAIN\user).' + Write-Warning 'To do it later:' + Write-Warning ' net localgroup "Performance Log Users" DOMAIN\user /add' + } else { + # Resolve to a SID first: it validates the name, and it is what the + # membership check compares, so a member spelled ".\claude" in one place + # and "LATISLAB\claude" in another is still recognised as the same account. + $targetSid = (New-Object System.Security.Principal.NTAccount($target)).Translate( + [System.Security.Principal.SecurityIdentifier]) + + # By SID, never by name: "Performance Log Users" is localised, and + # Get-LocalGroup -SID is how this stays correct on a non-English box. + $group = Get-LocalGroup -SID $PerfLogUsersSid + + # Get-LocalGroupMember throws on a group holding a SID that no longer + # resolves (a known Windows 10 bug), so a failure to READ the membership + # must not stop us from writing it - fall through and let the add report. + $already = $false + try { + $already = @(Get-LocalGroupMember -SID $PerfLogUsersSid | + Where-Object { $_.SID.Value -eq $targetSid.Value }).Count -gt 0 + } catch { + Write-Host " (could not enumerate $($group.Name) members: $($_.Exception.Message))" -ForegroundColor DarkGray + } + + if ($already) { + Write-Host " OK: $target is already in $($group.Name)" + } else { + try { + Add-LocalGroupMember -SID $PerfLogUsersSid -Member $targetSid.Value + } catch { + # "already a member" is only reachable when the enumeration above + # failed, and is not an error. Matched on the type NAME rather + # than in a typed catch clause: catch types are resolved when the + # script is PARSED, before the LocalAccounts module has been + # autoloaded, so naming the type there is a parse error that + # would take the whole script down. + if ($_.Exception.GetType().Name -ne 'MemberExistsException') { throw } + } + Write-Host " Added $target to $($group.Name)" + } + + Write-Host '' + Write-Host " $target must sign out and back in before this takes effect." -ForegroundColor Yellow + Write-Host ' Then, from that account (NOT elevated):' -ForegroundColor Yellow + Write-Host ' xperf -start T -on Microsoft-Windows-Kernel-Process -f trace.etl' -ForegroundColor Yellow + Write-Host ' xperf -stop T' -ForegroundColor Yellow + } +} catch { + Write-Warning "Performance Log Users membership failed: $($_.Exception.Message)" + Write-Warning 'Do it by hand with:' + Write-Warning ' net localgroup "Performance Log Users" /add' +} + +if ($EtwRightsOnly) { + # `exit` inside the try still runs the finally below, so the transcript is + # stopped and the log is left readable by the non-elevated caller. + Write-Host "`n-EtwRightsOnly: skipping the installs." -ForegroundColor Green + exit 0 +} + + # --------------------------------------------------------------------------- # Base tools via winget # --------------------------------------------------------------------------- @@ -448,30 +589,48 @@ if ($WdkInstalledRoot -and $WdkInstalledRoot -match [regex]::Escape($WdkVersion) } # --------------------------------------------------------------------------- -# Windows Performance Toolkit (xperf / WPA / wpr) -- ETW CPU + loader profiling, -# used by the perf/ measurement scripts. WPT is an OPTIONAL Windows SDK feature -# that the VS "Windows 11 SDK" component does NOT select, so a fresh box lacks it. -# The Windows ADK bundles WPT and winget owns the (versioned) download URL, so it -# is the most reliable source. Idempotent (skips if xperf is already present in -# either the SDK or ADK location) and non-fatal so it never aborts provisioning. -# Lighter alternative if you don't want the full ADK: install the Windows SDK's -# "Windows Performance Toolkit" optional feature via winsdksetup.exe /features -# OptionId.WindowsPerformanceToolkit. +# Windows Performance Toolkit: xperf, wpr, and Windows Performance Analyzer +# (wpa.exe) -- ETW CPU + loader profiling and the GUI that reads the traces. +# +# WPA is NOT a Visual Studio component and has no relationship to VS's own +# Performance Profiler (a separate, .diagsession-based tool that cannot open an +# .etl). It ships in exactly two places: as an optional FEATURE of the Windows +# SDK ("Windows Performance Toolkit", OptionId.WindowsPerformanceToolkit), and +# in the Windows ADK, which bundles the same toolkit. Whether the SDK install +# that Visual Studio performs happens to select that feature varies with the VS +# and SDK version - when it does, WPT lands in +# %ProgramFiles(x86)%\Windows Kits\10\Windows Performance Toolkit and the SDK +# puts that directory on the machine PATH itself - so this step DETECTS first +# and only falls back to installing the ADK (winget owns the versioned download +# URL, which makes it the reliable source) when nothing is there. That fallback +# is a large download; to install just the toolkit instead, run the standalone +# SDK setup with +# winsdksetup.exe /features OptionId.WindowsPerformanceToolkit /q +# +# There is also a newer WPA in the Microsoft Store (`winget install --id +# 9N0W1B2BXGNZ --source msstore`), which updates independently of the SDK. It is +# not installed here: the Store package needs an interactive, signed-in session, +# which is exactly what this elevated, unattended half does not have. +# +# Idempotent and non-fatal - it never aborts provisioning. # --------------------------------------------------------------------------- -Write-Step 'Windows Performance Toolkit (xperf / WPA)' -$wptRoots = @( - (Join-Path ${env:ProgramFiles(x86)} 'Windows Kits\10\Windows Performance Toolkit\xperf.exe'), - (Join-Path $env:ProgramFiles 'Windows Kits\10\Windows Performance Toolkit\xperf.exe'), - (Join-Path ${env:ProgramFiles(x86)} 'Windows Kits\10\Assessment and Deployment Kit\Windows Performance Toolkit\xperf.exe') +Write-Step 'Windows Performance Toolkit (xperf / wpr / WPA)' +$WptDirs = @( + (Join-Path ${env:ProgramFiles(x86)} 'Windows Kits\10\Windows Performance Toolkit'), + (Join-Path $env:ProgramFiles 'Windows Kits\10\Windows Performance Toolkit'), + (Join-Path ${env:ProgramFiles(x86)} 'Windows Kits\10\Assessment and Deployment Kit\Windows Performance Toolkit') ) -$xperf = $wptRoots | Where-Object { Test-Path $_ } | Select-Object -First 1 -if ($xperf) { - Write-Host " OK: WPT already present ($xperf)" -ForegroundColor Green +function Find-WptDir { $script:WptDirs | Where-Object { Test-Path (Join-Path $_ 'xperf.exe') } | Select-Object -First 1 } + +$WptDir = Find-WptDir +if ($WptDir) { + Write-Host " OK: WPT already present ($WptDir)" -ForegroundColor Green } else { try { winget install --id Microsoft.WindowsADK --exact --silent --disable-interactivity ` --accept-source-agreements --accept-package-agreements Write-Host ' Windows ADK (includes Windows Performance Toolkit) installed.' + $WptDir = Find-WptDir } catch { Write-Warning "WPT install failed: $($_.Exception.Message)" Write-Warning 'Install manually: winget install Microsoft.WindowsADK, or add the' @@ -479,6 +638,78 @@ if ($xperf) { } } +if ($WptDir) { + # Report what actually landed. wpa.exe is the piece people come looking for + # and it is the one that is absent if a trimmed toolkit ever shows up. + foreach ($tool in 'xperf.exe', 'wpr.exe', 'wpa.exe', 'wpaexporter.exe') { + $p = Join-Path $WptDir $tool + if (Test-Path $p) { + Write-Host " $tool $((Get-Item $p).VersionInfo.ProductVersion)" + } else { + Write-Warning "$tool is missing from $WptDir" + } + } + + # The WPT installer normally adds this to the machine PATH itself (and the + # Start Menu gets "Windows Kits > Windows Performance Toolkit" shortcuts for + # WPA and WPR). Re-assert it anyway: on the machine PATH rather than a user + # one so it also resolves for the non-interactive sshd sessions this box is + # driven through, which build their environment from the registry PATH. + # Compared trailing-backslash-insensitively - the installer's own entry has + # one, and adding a second spelling of the same directory is just noise. + $m = [Environment]::GetEnvironmentVariable('Path', 'Machine') + if (-not $m) { $m = '' } + $have = ($m -split ';') | Where-Object { $_.TrimEnd('\') -eq $WptDir.TrimEnd('\') } + if ($have) { + Write-Host " OK: $WptDir already in the machine PATH" + } else { + $new = if ($m.Trim()) { $m.TrimEnd(';') + ';' + $WptDir } else { $WptDir } + [Environment]::SetEnvironmentVariable('Path', $new, 'Machine') + Write-Host " Added $WptDir to the machine PATH (restart shells to pick it up)." + } +} + + +# --------------------------------------------------------------------------- +# Intel VTune Profiler - reported, not installed +# +# Deliberately NOT automated, unlike everything above. The offline installer is +# a ~750 MB download from a URL carrying a per-release GUID +# (registrationcenter-download.intel.com/akdlm/IRC_NAS//intel-vtune-_offline.exe) +# with no "latest" redirect behind it, so every new build means editing a +# hard-coded link in here - and it is only worth having on Intel silicon, since +# hardware event-based sampling reads Intel PMU counters. Not a good trade for a +# script that has to keep working unattended on any box. +# +# So this step only reports. To install it, take the Windows offline installer +# from +# https://www.intel.com/content/www/us/en/developer/tools/oneapi/vtune-profiler-download.html +# and run it elevated; it installs unattended with +# intel-vtune-_offline.exe -a --silent --cli --eula accept +# --------------------------------------------------------------------------- +Write-Step 'Intel VTune Profiler (status only)' +$UninstallKeys = @( + 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*' + 'HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*' +) +$vtune = Get-ItemProperty $UninstallKeys -ErrorAction SilentlyContinue | + Where-Object { $_.DisplayName -match 'VTune' } | + Select-Object -First 1 +if ($vtune) { + Write-Host " Installed: $($vtune.DisplayName.Trim()) $($vtune.DisplayVersion)" -ForegroundColor Green + # The oneAPI layout keeps a `latest` junction beside the versioned directory, + # so this path stays right across upgrades. + $VTuneCli = Join-Path $vtune.InstallLocation 'vtune\latest\bin64\vtune.exe' + if (Test-Path $VTuneCli) { Write-Host " CLI: $VTuneCli" } +} else { + Write-Host ' Not installed.' -ForegroundColor Yellow + Write-Host ' https://www.intel.com/content/www/us/en/developer/tools/oneapi/vtune-profiler-download.html' -ForegroundColor Yellow + $cpu = (Get-CimInstance Win32_Processor -ErrorAction SilentlyContinue | Select-Object -First 1).Manufacturer + if ($cpu -and $cpu -notmatch 'Intel') { + Write-Host " (This CPU reports itself as '$cpu' - VTune's hardware event-based sampling wants Intel silicon.)" -ForegroundColor Yellow + } +} + # --------------------------------------------------------------------------- Write-Host "`nAll done." -ForegroundColor Green Write-Host 'If a reboot was flagged above, restart before opening VS or building drivers.'