mirror of
https://github.com/ChrisTitusTech/winutil.git
synced 2026-10-08 11:39:10 +11:00
* perf: keep UI startup responsive * test: make favicon checks runner-safe * fix: qualify favicon visibility state * test: isolate favicon visibility fixture * fix: enforce total favicon deadline * fix: address favicon review feedback * fix: recognize disabled SSH firewall rule * perf: restore favicon download throughput * docs: allow public PR review uploads * perf: restore overlapping favicon loading
268 lines
11 KiB
PowerShell
268 lines
11 KiB
PowerShell
<#
|
|
.NOTES
|
|
Author : Chris Titus @christitustech
|
|
Runspace Author: @DeveloperDurp
|
|
GitHub : https://github.com/ChrisTitusTech
|
|
Version : #{replaceme}
|
|
#>
|
|
|
|
param (
|
|
[string]$Config,
|
|
[ValidateSet("Standard", "Minimal", "Advanced", "")]
|
|
[string]$Preset,
|
|
[switch]$Offline
|
|
)
|
|
|
|
function Test-WinUtilOwnsFileProcess {
|
|
<#
|
|
.SYNOPSIS
|
|
Whether the current process was launched with this script as its file target
|
|
#>
|
|
param(
|
|
[string]$ScriptPath = $PSCommandPath,
|
|
[string[]]$CommandLineArgs = [Environment]::GetCommandLineArgs()
|
|
)
|
|
|
|
if ([string]::IsNullOrWhiteSpace($ScriptPath)) { return $false }
|
|
|
|
$hostOptionKinds = [ordered]@{
|
|
Command = "Command"
|
|
EncodedCommand = "Command"
|
|
CommandWithArgs = "Command"
|
|
File = "File"
|
|
ConfigurationFile = "Value"
|
|
ConfigurationName = "Value"
|
|
CustomPipeName = "Value"
|
|
ExecutionPolicy = "Value"
|
|
InputFormat = "Value"
|
|
Interactive = "Switch"
|
|
Login = "Switch"
|
|
MTA = "Switch"
|
|
NoLogo = "Switch"
|
|
NonInteractive = "Switch"
|
|
NoProfile = "Switch"
|
|
NoProfileLoadTime = "Switch"
|
|
OutputFormat = "Value"
|
|
PSConsoleFile = "Value"
|
|
SettingsFile = "Value"
|
|
SSHServerMode = "Switch"
|
|
STA = "Switch"
|
|
Version = "Value"
|
|
WindowStyle = "Value"
|
|
WorkingDirectory = "Value"
|
|
NoExit = "NoExit"
|
|
}
|
|
$hostOptionAliases = @{
|
|
c = "Command"; cwa = "Command"; e = "Command"; ec = "Command"; f = "File"
|
|
noe = "NoExit"
|
|
config = "Value"; ConfigName = "Value"; CustomPipe = "Value"; ep = "Value"; ex = "Value"
|
|
i = "Switch"; Input = "Value"; In = "Value"; if = "Value"
|
|
Output = "Value"; Out = "Value"; of = "Value"
|
|
Settings = "Value"; Window = "Value"; w = "Value"; Working = "Value"; wd = "Value"
|
|
}
|
|
|
|
function Get-WinUtilHostOptionKind {
|
|
param([string]$Argument)
|
|
|
|
if ([string]::IsNullOrWhiteSpace($Argument) -or -not $Argument.StartsWith("-")) {
|
|
return $null
|
|
}
|
|
|
|
$optionName = $Argument.TrimStart("-")
|
|
if ($hostOptionAliases.ContainsKey($optionName)) {
|
|
return $hostOptionAliases[$optionName]
|
|
}
|
|
|
|
# pwsh accepts any unambiguous prefix of a host option, such as -WorkingD.
|
|
$matchingOptions = @($hostOptionKinds.Keys | Where-Object {
|
|
$_.StartsWith($optionName, [StringComparison]::OrdinalIgnoreCase)
|
|
})
|
|
$matchingKinds = @($matchingOptions | ForEach-Object { $hostOptionKinds[$_] } | Select-Object -Unique)
|
|
if ($matchingKinds.Count -eq 1) {
|
|
return $matchingKinds[0]
|
|
}
|
|
|
|
return $null
|
|
}
|
|
|
|
:hostArguments for ($index = 1; $index -lt $CommandLineArgs.Count; $index++) {
|
|
$optionKind = Get-WinUtilHostOptionKind -Argument $CommandLineArgs[$index]
|
|
switch ($optionKind) {
|
|
"Command" { return $false }
|
|
"NoExit" { return $false }
|
|
"Switch" { continue hostArguments }
|
|
"File" {
|
|
if ($index + 1 -ge $CommandLineArgs.Count) { return $false }
|
|
$fileTarget = $CommandLineArgs[$index + 1]
|
|
if ([string]::IsNullOrWhiteSpace($fileTarget) -or $fileTarget -eq "-") {
|
|
return $false
|
|
}
|
|
|
|
return [string]::Equals(
|
|
[IO.Path]::GetFullPath($fileTarget),
|
|
[IO.Path]::GetFullPath($ScriptPath),
|
|
[StringComparison]::OrdinalIgnoreCase
|
|
)
|
|
}
|
|
"Value" {
|
|
$index++
|
|
continue hostArguments
|
|
}
|
|
}
|
|
|
|
if ($CommandLineArgs[$index].StartsWith("-")) {
|
|
continue
|
|
}
|
|
|
|
return [string]::Equals(
|
|
[IO.Path]::GetFullPath($CommandLineArgs[$index]),
|
|
[IO.Path]::GetFullPath($ScriptPath),
|
|
[StringComparison]::OrdinalIgnoreCase
|
|
)
|
|
}
|
|
|
|
return $false
|
|
}
|
|
|
|
# A headless script launched with powershell.exe/pwsh.exe -File owns its process and must set
|
|
# that process's exit code. An invoked or in-memory script must return without closing its caller.
|
|
$script:WinUtilIsFileProcess = Test-WinUtilOwnsFileProcess
|
|
|
|
$PARAM_OFFLINE = $false
|
|
if ($Offline) {
|
|
$PARAM_OFFLINE = $true
|
|
}
|
|
|
|
if ($ExecutionContext.SessionState.LanguageMode -ne 'FullLanguage') {
|
|
Write-Host "WinUtil is unable to run on your system. PowerShell execution is restricted by security policies." -ForegroundColor Red
|
|
$global:LASTEXITCODE = 1
|
|
if ($env:WINUTIL_HEADLESS_CHILD -eq "1" -or $script:WinUtilIsFileProcess) { exit 1 }
|
|
return 1
|
|
}
|
|
|
|
function New-WinUtilElevationCommand {
|
|
<#
|
|
.SYNOPSIS
|
|
Encodes the relaunch target and bound parameters as data for an elevated child
|
|
#>
|
|
param(
|
|
[string]$ScriptPath,
|
|
[hashtable]$Parameters = @{},
|
|
[switch]$Headless
|
|
)
|
|
|
|
$launchData = @{
|
|
ScriptPath = $ScriptPath
|
|
Parameters = $Parameters
|
|
Headless = [bool]$Headless
|
|
}
|
|
$serializedLaunch = [System.Management.Automation.PSSerializer]::Serialize($launchData)
|
|
$launchPayload = [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes($serializedLaunch))
|
|
|
|
# Only base64 is embedded in executable text. User-controlled values are deserialized and
|
|
# splatted as parameter data in the child, so quotes in Config cannot become PowerShell code.
|
|
$bootstrap = @"
|
|
`$launchXml = [Text.Encoding]::UTF8.GetString([Convert]::FromBase64String('$launchPayload'))
|
|
`$launch = [System.Management.Automation.PSSerializer]::Deserialize(`$launchXml)
|
|
`$invokeParameters = `$launch.Parameters
|
|
if (`$launch.Headless) { `$env:WINUTIL_HEADLESS_CHILD = '1' }
|
|
if (`$launch.ScriptPath) {
|
|
& `$launch.ScriptPath @invokeParameters
|
|
} else {
|
|
`$remoteScript = [ScriptBlock]::Create((Invoke-RestMethod 'https://github.com/ChrisTitusTech/winutil/releases/latest/download/winutil.ps1'))
|
|
& `$remoteScript @invokeParameters
|
|
}
|
|
"@
|
|
|
|
return [Convert]::ToBase64String([Text.Encoding]::Unicode.GetBytes($bootstrap))
|
|
}
|
|
|
|
if (!([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) {
|
|
Write-Output "WinUtil needs to be run as Administrator. Attempting to relaunch."
|
|
$elevationParameters = @{}
|
|
foreach ($parameter in $PSBoundParameters.GetEnumerator()) {
|
|
$elevationParameters[$parameter.Key] = if ($parameter.Value -is [switch]) {
|
|
[bool]$parameter.Value
|
|
} else {
|
|
$parameter.Value
|
|
}
|
|
}
|
|
|
|
$powershellCmd = if (Get-Command pwsh -ErrorAction SilentlyContinue) { "pwsh" } else { "powershell" }
|
|
|
|
# A headless caller is waiting on this process for an outcome, so the elevated run has to be
|
|
# waited on and its code handed back. A terminal tab is skipped for the same reason: the
|
|
# exit code of wt.exe is its own, not the run's.
|
|
if ($Config -or $Preset) {
|
|
# A declined UAC prompt throws, which would leave $elevated null and exit 0: the caller
|
|
# waiting on this process would read that as a successful run
|
|
try {
|
|
$elevationCommand = New-WinUtilElevationCommand -ScriptPath $PSCommandPath -Parameters $elevationParameters -Headless
|
|
$elevated = Start-Process $powershellCmd -ArgumentList @("-ExecutionPolicy", "Bypass", "-NoProfile", "-EncodedCommand", $elevationCommand) -Verb RunAs -Wait -PassThru -ErrorAction Stop
|
|
} catch {
|
|
Write-Host "Elevation was declined or failed: $($_.Exception.Message)" -ForegroundColor Red
|
|
$global:LASTEXITCODE = 1
|
|
if ($script:WinUtilIsFileProcess) { exit 1 }
|
|
return 1
|
|
}
|
|
$global:LASTEXITCODE = $elevated.ExitCode
|
|
if ($script:WinUtilIsFileProcess) { exit $elevated.ExitCode }
|
|
return $elevated.ExitCode
|
|
}
|
|
|
|
$processCmd = if (Get-Command wt.exe -ErrorAction SilentlyContinue) { "wt.exe" } else { "$powershellCmd" }
|
|
$elevationCommand = New-WinUtilElevationCommand -ScriptPath $PSCommandPath -Parameters $elevationParameters
|
|
|
|
if ($processCmd -eq "wt.exe") {
|
|
Start-Process $processCmd -ArgumentList "$powershellCmd -ExecutionPolicy Bypass -NoProfile -EncodedCommand $elevationCommand" -Verb RunAs
|
|
} else {
|
|
Start-Process $processCmd -ArgumentList @("-ExecutionPolicy", "Bypass", "-NoProfile", "-EncodedCommand", $elevationCommand) -Verb RunAs
|
|
}
|
|
|
|
break
|
|
}
|
|
|
|
# Variable to sync between runspaces
|
|
$sync = [Hashtable]::Synchronized(@{})
|
|
$sync.version = "#{replaceme}"
|
|
$sync.IsLocalCompile = "#{islocalcompile}" -eq "true"
|
|
$sync.configs = @{}
|
|
$sync.Buttons = [System.Collections.Generic.List[PSObject]]::new()
|
|
$sync.preferences = @{}
|
|
# Name of the job currently running, or $null when idle. Owned by Start-WinUtilJob.
|
|
$sync.ActiveJob = $null
|
|
# Serializes worker-pool startup with recycling and shutdown.
|
|
$sync.RunspacePoolLock = [object]::new()
|
|
# Serializes the speculative and UI-thread taskbar overlay renderers.
|
|
$sync.AssetRenderLock = [object]::new()
|
|
$sync.RenderedAssetCache = [Hashtable]::Synchronized(@{})
|
|
# Every step recorded by Measure-WinUtilStep, from any thread
|
|
$sync.StepTimings = [System.Collections.ArrayList]::Synchronized([System.Collections.ArrayList]::new())
|
|
# Every error logged, so a job can report that something went wrong even when it did not throw
|
|
$sync.LoggedErrors = [System.Collections.ArrayList]::Synchronized([System.Collections.ArrayList]::new())
|
|
$sync.StartedAt = Get-Date
|
|
$sync.selectedAppx = [System.Collections.Generic.List[string]]::new()
|
|
$sync.selectedApps = [System.Collections.Generic.List[string]]::new()
|
|
$sync.selectedTweaks = [System.Collections.Generic.List[string]]::new()
|
|
$sync.selectedToggles = [System.Collections.Generic.List[string]]::new()
|
|
$sync.selectedFeatures = [System.Collections.Generic.List[string]]::new()
|
|
$sync.currentTab = "Install"
|
|
|
|
$dateTime = Get-Date -Format "yyyy-MM-dd_HH-mm-ss"
|
|
$winutildir = "$env:LocalAppData\winutil"
|
|
$sync.winutildir = $winutildir
|
|
|
|
$logdir = "$winutildir\logs"
|
|
# Start-Transcript fails outright when the directory is missing, which is every first run
|
|
if (-not (Test-Path $logdir)) {
|
|
New-Item -ItemType Directory -Path $logdir -Force | Out-Null
|
|
}
|
|
# Keep console output and structured entries in the path reported to the user. Write-WinUtilLog
|
|
# writes through the host while this transcript owns the file, avoiding competing file handles.
|
|
$sync.logPath = "$logdir\winutil_$dateTime.log"
|
|
$sync.transcriptPath = $sync.logPath
|
|
Start-Transcript -Path $sync.transcriptPath -Append -NoClobber | Out-Null
|
|
|
|
$Host.UI.RawUI.WindowTitle = "WinUtil"
|
|
Clear-Host
|