hypertwist/scripts/Set-HyperTwistBootstrapLaunchArguments.ps1
2026-07-23 08:42:30 +00:00

408 lines
12 KiB
PowerShell

param(
[Parameter(Mandatory = $true)]
[string]$ExecutablePath,
[string]$ProjectToken = 'UnrealHyperTwist',
[string[]]$AdditionalArguments = @(
'-notraceserver',
'-traceautostart=0'
),
[string]$ReportPath = '',
[switch]$VerifyOnly
)
$ErrorActionPreference = 'Stop'
$RawDataResourceType = 10
$BootstrapArgumentsResourceId = 202
function Write-Utf8JsonFile {
param(
[Parameter(Mandatory = $true)]
[string]$Path,
[Parameter(Mandatory = $true)]
[object]$Value
)
$ParentPath = Split-Path -Parent $Path
if (-not [string]::IsNullOrWhiteSpace($ParentPath))
{
New-Item -ItemType Directory -Force -Path $ParentPath | Out-Null
}
[System.IO.File]::WriteAllText(
$Path,
($Value | ConvertTo-Json -Depth 10),
(New-Object System.Text.UTF8Encoding($false))
)
}
if (-not ('HyperTwistBootstrapResources.NativeMethods' -as [type]))
{
Add-Type -TypeDefinition @'
using System;
using System.Runtime.InteropServices;
namespace HyperTwistBootstrapResources
{
public static class NativeMethods
{
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
public static extern IntPtr LoadLibraryEx(
string fileName,
IntPtr fileHandle,
uint flags
);
[DllImport("kernel32.dll", SetLastError = true)]
public static extern bool FreeLibrary(IntPtr moduleHandle);
[DllImport("kernel32.dll", SetLastError = true)]
public static extern IntPtr FindResource(
IntPtr moduleHandle,
IntPtr resourceName,
IntPtr resourceType
);
[DllImport("kernel32.dll", SetLastError = true)]
public static extern uint SizeofResource(
IntPtr moduleHandle,
IntPtr resourceInfo
);
[DllImport("kernel32.dll", SetLastError = true)]
public static extern IntPtr LoadResource(
IntPtr moduleHandle,
IntPtr resourceInfo
);
[DllImport("kernel32.dll", SetLastError = true)]
public static extern IntPtr LockResource(IntPtr resourceData);
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
public static extern IntPtr BeginUpdateResource(
string fileName,
bool deleteExistingResources
);
[DllImport("kernel32.dll", SetLastError = true)]
public static extern bool UpdateResource(
IntPtr updateHandle,
IntPtr resourceType,
IntPtr resourceName,
ushort language,
byte[] data,
uint dataLength
);
[DllImport("kernel32.dll", SetLastError = true)]
public static extern bool EndUpdateResource(
IntPtr updateHandle,
bool discard
);
}
}
'@
}
function New-Win32Exception {
param(
[Parameter(Mandatory = $true)]
[string]$Operation
)
$ErrorCode = [Runtime.InteropServices.Marshal]::GetLastWin32Error()
return New-Object `
-TypeName ComponentModel.Win32Exception `
-ArgumentList $ErrorCode, "$Operation failed with Win32 error $ErrorCode."
}
function Get-BootstrapArgumentsResource {
param(
[Parameter(Mandatory = $true)]
[string]$Path
)
$LoadLibraryAsDataFile = 0x00000002
$LoadLibraryAsImageResource = 0x00000020
$ModuleHandle = [HyperTwistBootstrapResources.NativeMethods]::LoadLibraryEx(
$Path,
[IntPtr]::Zero,
($LoadLibraryAsDataFile -bor $LoadLibraryAsImageResource)
)
if ($ModuleHandle -eq [IntPtr]::Zero)
{
throw (New-Win32Exception -Operation "LoadLibraryEx('$Path')")
}
try
{
$ResourceInfo = [HyperTwistBootstrapResources.NativeMethods]::FindResource(
$ModuleHandle,
[IntPtr]$BootstrapArgumentsResourceId,
[IntPtr]$RawDataResourceType
)
if ($ResourceInfo -eq [IntPtr]::Zero)
{
throw (
New-Win32Exception -Operation (
"FindResource(type=$RawDataResourceType,id=$BootstrapArgumentsResourceId)"
)
)
}
$ResourceSize = [HyperTwistBootstrapResources.NativeMethods]::SizeofResource(
$ModuleHandle,
$ResourceInfo
)
if ($ResourceSize -eq 0)
{
throw (
New-Win32Exception -Operation (
"SizeofResource(type=$RawDataResourceType,id=$BootstrapArgumentsResourceId)"
)
)
}
$LoadedResource = [HyperTwistBootstrapResources.NativeMethods]::LoadResource(
$ModuleHandle,
$ResourceInfo
)
if ($LoadedResource -eq [IntPtr]::Zero)
{
throw (
New-Win32Exception -Operation (
"LoadResource(type=$RawDataResourceType,id=$BootstrapArgumentsResourceId)"
)
)
}
$ResourcePointer = [HyperTwistBootstrapResources.NativeMethods]::LockResource(
$LoadedResource
)
if ($ResourcePointer -eq [IntPtr]::Zero)
{
throw (
New-Win32Exception -Operation (
"LockResource(type=$RawDataResourceType,id=$BootstrapArgumentsResourceId)"
)
)
}
$ResourceBytes = New-Object byte[] $ResourceSize
[Runtime.InteropServices.Marshal]::Copy(
$ResourcePointer,
$ResourceBytes,
0,
[int]$ResourceSize
)
return [Text.Encoding]::Unicode.GetString($ResourceBytes).TrimEnd([char]0)
}
finally
{
[void][HyperTwistBootstrapResources.NativeMethods]::FreeLibrary($ModuleHandle)
}
}
function Set-BootstrapArgumentsResource {
param(
[Parameter(Mandatory = $true)]
[string]$Path,
[Parameter(Mandatory = $true)]
[string]$Value
)
$ResourceBytes = [Text.Encoding]::Unicode.GetBytes($Value + [char]0)
$UpdateHandle = [HyperTwistBootstrapResources.NativeMethods]::BeginUpdateResource(
$Path,
$false
)
if ($UpdateHandle -eq [IntPtr]::Zero)
{
throw (New-Win32Exception -Operation "BeginUpdateResource('$Path')")
}
$UpdateClosed = $false
try
{
if (-not [HyperTwistBootstrapResources.NativeMethods]::UpdateResource(
$UpdateHandle,
[IntPtr]$RawDataResourceType,
[IntPtr]$BootstrapArgumentsResourceId,
[uint16]0,
$ResourceBytes,
[uint32]$ResourceBytes.Length
))
{
throw (
New-Win32Exception -Operation (
"UpdateResource(type=$RawDataResourceType,id=$BootstrapArgumentsResourceId)"
)
)
}
$UpdateCompleted = [HyperTwistBootstrapResources.NativeMethods]::EndUpdateResource(
$UpdateHandle,
$false
)
$UpdateClosed = $true
if (-not $UpdateCompleted)
{
throw (New-Win32Exception -Operation "EndUpdateResource('$Path')")
}
}
finally
{
if (-not $UpdateClosed)
{
[void][HyperTwistBootstrapResources.NativeMethods]::EndUpdateResource(
$UpdateHandle,
$true
)
}
}
}
if (-not (Test-Path -LiteralPath $ExecutablePath -PathType Leaf))
{
throw "HyperTwist bootstrap executable '$ExecutablePath' was not found."
}
if ([string]::IsNullOrWhiteSpace($ProjectToken))
{
throw 'ProjectToken must not be empty.'
}
$NormalizedAdditionalArguments = @(
$AdditionalArguments |
ForEach-Object {
if ($null -eq $_)
{
return
}
$_.Trim()
} |
Where-Object { -not [string]::IsNullOrWhiteSpace($_) }
)
foreach ($Argument in $NormalizedAdditionalArguments)
{
if ($Argument.IndexOf([char]0) -ge 0 `
-or $Argument.IndexOf("`r") -ge 0 `
-or $Argument.IndexOf("`n") -ge 0)
{
throw "Bootstrap launch argument '$Argument' contains an unsupported control character."
}
}
$ResolvedExecutablePath = (Resolve-Path -LiteralPath $ExecutablePath).Path
$ExpectedArguments = @($ProjectToken) + $NormalizedAdditionalArguments
$ExpectedResourceValue = $ExpectedArguments -join ' '
if ([string]::IsNullOrWhiteSpace($ReportPath))
{
$ReportPath = "$ResolvedExecutablePath.bootstrap-launch-arguments.json"
}
$OriginalHash = (Get-FileHash -LiteralPath $ResolvedExecutablePath -Algorithm SHA256).Hash.ToLowerInvariant()
$OriginalResourceValue = Get-BootstrapArgumentsResource -Path $ResolvedExecutablePath
$Result = [ordered]@{
reportVersion = 'ht-bootstrap-launch-arguments/v1'
generatedAtUtc = [DateTime]::UtcNow.ToString('o')
executablePath = $ResolvedExecutablePath
resourceType = $RawDataResourceType
resourceId = $BootstrapArgumentsResourceId
verifyOnly = [bool]$VerifyOnly
expectedArguments = @($ExpectedArguments)
expectedResourceValue = $ExpectedResourceValue
originalResourceValue = $OriginalResourceValue
finalResourceValue = $null
originalSha256 = $OriginalHash
finalSha256 = $null
changed = $false
result = 'failed'
error = $null
}
$TemporaryPath = $null
$BackupPath = $null
try
{
if ($VerifyOnly)
{
if ($OriginalResourceValue -ne $ExpectedResourceValue)
{
throw (
"Bootstrap argument resource was '$OriginalResourceValue'; " +
"expected '$ExpectedResourceValue'."
)
}
}
elseif ($OriginalResourceValue -ne $ExpectedResourceValue)
{
$TemporaryPath = '{0}.bootstrap-{1}-{2}.tmp' -f (
$ResolvedExecutablePath,
$PID,
[Guid]::NewGuid().ToString('N')
)
$BackupPath = '{0}.bootstrap-{1}-{2}.bak' -f (
$ResolvedExecutablePath,
$PID,
[Guid]::NewGuid().ToString('N')
)
Copy-Item -LiteralPath $ResolvedExecutablePath -Destination $TemporaryPath -Force
Set-BootstrapArgumentsResource -Path $TemporaryPath -Value $ExpectedResourceValue
$TemporaryResourceValue = Get-BootstrapArgumentsResource -Path $TemporaryPath
if ($TemporaryResourceValue -ne $ExpectedResourceValue)
{
throw (
"Patched bootstrap argument resource was '$TemporaryResourceValue'; " +
"expected '$ExpectedResourceValue'."
)
}
[System.IO.File]::Replace(
$TemporaryPath,
$ResolvedExecutablePath,
$BackupPath,
$true
)
$TemporaryPath = $null
$Result.changed = $true
}
$Result.finalResourceValue = Get-BootstrapArgumentsResource -Path $ResolvedExecutablePath
if ($Result.finalResourceValue -ne $ExpectedResourceValue)
{
throw (
"Final bootstrap argument resource was '$($Result.finalResourceValue)'; " +
"expected '$ExpectedResourceValue'."
)
}
$Result.finalSha256 = (
Get-FileHash -LiteralPath $ResolvedExecutablePath -Algorithm SHA256
).Hash.ToLowerInvariant()
$Result.result = 'passed'
}
catch
{
$Result.error = $_.Exception.Message
throw
}
finally
{
if (-not [string]::IsNullOrWhiteSpace($TemporaryPath) `
-and (Test-Path -LiteralPath $TemporaryPath))
{
Remove-Item -LiteralPath $TemporaryPath -Force -ErrorAction SilentlyContinue
}
if (-not [string]::IsNullOrWhiteSpace($BackupPath) `
-and (Test-Path -LiteralPath $BackupPath))
{
Remove-Item -LiteralPath $BackupPath -Force -ErrorAction SilentlyContinue
}
Write-Utf8JsonFile -Path $ReportPath -Value $Result
}
$Result | ConvertTo-Json -Depth 10