diff --git a/.gitignore b/.gitignore index 0e7fa8f..91a1c12 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ bin/ build/ +.build/ projects/* !projects/*.sln !projects/*.vcxproj diff --git a/README.md b/README.md index 8473f63..8467dd6 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,25 @@ to be added clumsy must be run as Administrator because it uses WinDivert to intercept packets. +## Building on Windows + +Install Visual Studio 2022 or Visual Studio Build Tools with the **Desktop development +with C++** workload, then run: + +```bat +build.bat +``` + +This creates a 64-bit release build by default. Pass the configuration and platform +to select another build, for example `build.bat Debug x32`. The script locates +MSBuild, copies the required runtime files, writes a build log under `.build`, and +prints detected compiler or MSBuild errors when the build fails. When started by +double-clicking, a failed build waits for a key press so the error remains visible. +Set `BUILD_NO_PAUSE=1` before running the script to disable the pause for automation; +the script also disables it automatically when the `CI` environment variable is set. +Incremental builds reuse MSBuild outputs and skip the generated project's obsolete +absolute-path post-build commands. + ## What's New In 4.0 - Native "Limit to application" controls in the Filtering panel. diff --git a/build.bat b/build.bat new file mode 100644 index 0000000..85fe2bb --- /dev/null +++ b/build.bat @@ -0,0 +1,152 @@ +@echo off +setlocal EnableExtensions + +rem Build clumsy with Visual Studio 2022. Arguments are optional: +rem build.bat [Release^|Debug] [x64^|x32] + +set "CONFIGURATION=%~1" +set "PLATFORM=%~2" +if not defined CONFIGURATION set "CONFIGURATION=Release" +if not defined PLATFORM set "PLATFORM=x64" + +if /i not "%CONFIGURATION%"=="Release" if /i not "%CONFIGURATION%"=="Debug" goto :usage +if /i not "%PLATFORM%"=="x64" if /i not "%PLATFORM%"=="x32" goto :usage +set "MSBUILD_PLATFORM=%PLATFORM%" +if /i "%PLATFORM%"=="x32" set "MSBUILD_PLATFORM=Win32" + +set "ROOT=%~dp0" +set "SOLUTION=%ROOT%projects\clumsy.sln" +set "OUTPUT=%ROOT%bin\vs\%CONFIGURATION%\%PLATFORM%\clumsy.exe" +set "LOGDIR=%ROOT%.build" +set "BUILDLOG=%LOGDIR%\msbuild-%CONFIGURATION%-%PLATFORM%.log" +set "BOOTLOG=%LOGDIR%\build-%CONFIGURATION%-%PLATFORM%.log" + +if not exist "%LOGDIR%" mkdir "%LOGDIR%" 2>nul +if errorlevel 1 ( + call :fail 1 "Could not create the build-log directory: %LOGDIR%" + exit /b 1 +) +>"%BOOTLOG%" echo Build started %DATE% %TIME% +>>"%BOOTLOG%" echo Configuration: %CONFIGURATION% +>>"%BOOTLOG%" echo Platform: %MSBUILD_PLATFORM% +>>"%BOOTLOG%" echo Solution: %SOLUTION% + +if not exist "%SOLUTION%" ( + call :fail 1 "Visual Studio solution not found: %SOLUTION%" + exit /b 1 +) + +if /i "%PLATFORM%"=="x64" ( + set "WINDIVERTDIR=%ROOT%external\WinDivert-2.2.0-A\x64" + set "IUPDIR=%ROOT%external\iup-3.30_Win64_dll16_lib" +) else ( + set "WINDIVERTDIR=%ROOT%external\WinDivert-2.2.0-A\x86" + set "IUPDIR=%ROOT%external\iup-3.30_Win32_dll16_lib" +) + +rem Fail before starting an expensive compile if the runtime package is incomplete. +if not exist "%WINDIVERTDIR%\WinDivert.dll" ( + call :fail 1 "Missing runtime dependency: %WINDIVERTDIR%\WinDivert.dll" + exit /b 1 +) +if not exist "%WINDIVERTDIR%\WinDivert*.sys" ( + call :fail 1 "Missing WinDivert driver under: %WINDIVERTDIR%" + exit /b 1 +) +if not exist "%IUPDIR%\iup.dll" ( + call :fail 1 "Missing runtime dependency: %IUPDIR%\iup.dll" + exit /b 1 +) +if not exist "%ROOT%etc\config.txt" ( + call :fail 1 "Missing runtime dependency: %ROOT%etc\config.txt" + exit /b 1 +) + +set "MSBUILD=" +set "VSWHERE=%ProgramFiles(x86)%\Microsoft Visual Studio\Installer\vswhere.exe" +for /f "delims=" %%I in ('where MSBuild.exe 2^>nul') do if not defined MSBUILD set "MSBUILD=%%I" +if not defined MSBUILD ( + if exist "%VSWHERE%" ( + for /f "usebackq delims=" %%I in (`"%VSWHERE%" -latest -products * -requires Microsoft.Component.MSBuild -find MSBuild\**\Bin\MSBuild.exe`) do if not defined MSBUILD set "MSBUILD=%%I" + ) +) + +if not defined MSBUILD ( + >>"%BOOTLOG%" echo MSBuild discovery failed. + call :fail 1 "MSBuild was not found. Install Visual Studio 2022 or Build Tools with the Desktop development with C++ workload." + exit /b 1 +) +>>"%BOOTLOG%" echo MSBuild: %MSBUILD% + +echo Building %CONFIGURATION%^|%MSBUILD_PLATFORM% with "%MSBUILD%"... +del /q "%BUILDLOG%" 2>nul +rem Disable the generated project's legacy absolute-path post-build commands. +rem This script performs the same copies below using paths relative to the checkout. +"%MSBUILD%" "%SOLUTION%" /m /nologo /verbosity:minimal /t:Build /p:Configuration=%CONFIGURATION% /p:Platform=%MSBUILD_PLATFORM% /p:PostBuildEventUseInBuild=false /fl /flp:"logfile=%BUILDLOG%;verbosity=normal" +set "RESULT=%ERRORLEVEL%" +>>"%BOOTLOG%" echo MSBuild exit code: %RESULT% + +if not "%RESULT%"=="0" ( + echo. + echo BUILD FAILED with exit code %RESULT%. + echo -------------------- detected issues -------------------- + findstr /i /c:": error " /c:"fatal error" /c:"error MSB" "%BUILDLOG%" + if errorlevel 1 echo No individual error line was detected; inspect the complete log. + echo --------------------------------------------------------- + echo Complete build log: "%BUILDLOG%" + call :wait_on_error + exit /b %RESULT% +) + +if not exist "%OUTPUT%" ( + echo ERROR: MSBuild reported success, but the executable was not found: + echo "%OUTPUT%" + echo Complete build log: "%BUILDLOG%" + call :wait_on_error + exit /b 1 +) + +set "OUTPUTDIR=%ROOT%bin\vs\%CONFIGURATION%\%PLATFORM%" +copy /y "%WINDIVERTDIR%\WinDivert.dll" "%OUTPUTDIR%\" >nul +if errorlevel 1 goto :copy_failed +copy /y "%WINDIVERTDIR%\WinDivert*.sys" "%OUTPUTDIR%\" >nul +if errorlevel 1 goto :copy_failed +copy /y "%IUPDIR%\iup.dll" "%OUTPUTDIR%\" >nul +if errorlevel 1 goto :copy_failed +copy /y "%ROOT%etc\config.txt" "%OUTPUTDIR%\" >nul +if errorlevel 1 goto :copy_failed + +echo. +echo BUILD SUCCEEDED +echo Executable: "%OUTPUT%" +echo Build log: "%BUILDLOG%" +exit /b 0 + +:usage +echo Usage: %~nx0 [Release^|Debug] [x64^|x32] +echo Set BUILD_NO_PAUSE=1 to prevent pausing after an error. +call :wait_on_error +exit /b 2 + +:copy_failed +echo ERROR: The executable was built, but a required runtime file could not be copied. +echo Verify the dependency files under "%ROOT%external" and review "%BUILDLOG%". +>>"%BOOTLOG%" echo Runtime dependency copy failed. +call :wait_on_error +exit /b 1 + +:fail +echo. +echo ERROR: %~2 +if defined BOOTLOG >>"%BOOTLOG%" echo ERROR: %~2 +if defined BOOTLOG echo Diagnostic log: "%BOOTLOG%" +call :wait_on_error +exit /b %~1 + +:wait_on_error +if defined CI exit /b 0 +if defined BUILD_NO_PAUSE exit /b 0 +echo. +echo Press any key to close this window... +pause >nul +exit /b 0 diff --git a/tools/get_genie.ps1 b/tools/get_genie.ps1 index e36005e..52f5aea 100644 --- a/tools/get_genie.ps1 +++ b/tools/get_genie.ps1 @@ -6,49 +6,182 @@ This binary supports Visual Studio 2022 (vs2022), 2019, 2017, 2015, and MinGW (gmake). .PARAMETER OutDir Directory to save genie.exe. Defaults to the script's parent directory (tools/). +.PARAMETER SourceUrl + HTTPS URL of the GENie executable. This can point to a pinned revision or mirror. .PARAMETER Force Overwrite genie.exe if it already exists. +.PARAMETER ExpectedSha256 + Optional SHA-256 checksum used to verify the downloaded executable. +.PARAMETER DownloadMethod + Downloader to use. Auto tries BITS first and then Invoke-WebRequest. +.PARAMETER TimeoutSec + Timeout, in seconds, for Invoke-WebRequest. +.PARAMETER KeepBackup + Keep the previous executable as genie.exe.bak when replacing it. +.PARAMETER PassThru + Return the installed genie.exe FileInfo object for use in automation. .EXAMPLE .\get_genie.ps1 Downloads genie.exe to the tools/ directory. .EXAMPLE .\get_genie.ps1 -Force Re-downloads even if genie.exe already exists. +.EXAMPLE + .\get_genie.ps1 -SourceUrl https://example.test/genie.exe ` + -ExpectedSha256 <64-character-hash> -PassThru + Download from a mirror, verify the result, and return the installed file. #> param( [string]$OutDir = $PSScriptRoot, - [switch]$Force + [uri]$SourceUrl = 'https://github.com/bkaradzic/bx/raw/master/tools/bin/windows/genie.exe', + [switch]$Force, + [ValidatePattern('^[0-9a-fA-F]{64}$')] + [string]$ExpectedSha256, + [ValidateSet('Auto', 'Bits', 'WebRequest')] + [string]$DownloadMethod = 'Auto', + [ValidateRange(1, 3600)] + [int]$TimeoutSec = 120, + [switch]$KeepBackup, + [switch]$PassThru ) -$genieUrl = "https://github.com/bkaradzic/bx/raw/master/tools/bin/windows/genie.exe" -$outFile = Join-Path $OutDir "genie.exe" +$ErrorActionPreference = "Stop" + +$tempFile = $null +$outFile = Join-Path $OutDir 'genie.exe' -if (Test-Path $outFile -PathType Leaf) { - if (-not $Force) { - Write-Host "genie.exe already exists at $outFile" -ForegroundColor Green - Write-Host "Use -Force to re-download." -ForegroundColor Yellow - exit 0 +try { + if (-not $SourceUrl.IsAbsoluteUri -or $SourceUrl.Scheme -ne 'https') { + throw 'SourceUrl must be an absolute HTTPS URL.' } - Write-Host "Overwriting existing genie.exe..." -ForegroundColor Yellow -} -Write-Host "Downloading GENie from $genieUrl ..." -ForegroundColor Cyan + if (-not (Test-Path $OutDir -PathType Container)) { + New-Item -ItemType Directory -Path $OutDir -Force | Out-Null + } -try { - # Try to use BITS if available (more reliable), fall back to Invoke-WebRequest - if (Get-Command Start-BitsTransfer -ErrorAction SilentlyContinue) { - Start-BitsTransfer -Source $genieUrl -Destination $outFile + $OutDir = (Resolve-Path -LiteralPath $OutDir).Path + $outFile = Join-Path $OutDir "genie.exe" + + if (Test-Path $outFile -PathType Leaf) { + if (-not $Force) { + if ($ExpectedSha256) { + $existingSha256 = (Get-FileHash -LiteralPath $outFile -Algorithm SHA256).Hash + if ($existingSha256 -ne $ExpectedSha256) { + throw "Existing genie.exe checksum mismatch. Use -Force to replace it." + } + Write-Host "Existing genie.exe passed SHA-256 verification." -ForegroundColor Green + } + Write-Host "genie.exe already exists at $outFile" -ForegroundColor Green + Write-Host "Use -Force to re-download." -ForegroundColor Yellow + if ($PassThru) { + Get-Item -LiteralPath $outFile + } + exit 0 + } + Write-Host "A valid download will replace $outFile." -ForegroundColor Yellow + } + + Write-Host "Downloading GENie from $SourceUrl ..." -ForegroundColor Cyan + + # Download to a temporary file so an interrupted request cannot replace a + # working copy with a partial executable. A unique name also permits + # concurrent invocations that target the same directory. + $tempFile = Join-Path $OutDir ("genie.{0}.download" -f ([guid]::NewGuid())) + + $bitsAvailable = [bool](Get-Command Start-BitsTransfer -ErrorAction SilentlyContinue) + if ($DownloadMethod -eq 'Bits' -and -not $bitsAvailable) { + throw 'Start-BitsTransfer is not available. Use -DownloadMethod WebRequest.' + } + + $downloaded = $false + if ($DownloadMethod -ne 'WebRequest' -and $bitsAvailable) { + try { + Start-BitsTransfer -Source $SourceUrl.AbsoluteUri -Destination $tempFile + $downloaded = $true + } catch { + if ($DownloadMethod -eq 'Bits') { + throw + } + Write-Warning "BITS download failed; retrying with Invoke-WebRequest: $_" + Remove-Item $tempFile -Force -ErrorAction SilentlyContinue + } + } + + if (-not $downloaded) { + [Net.ServicePointManager]::SecurityProtocol = + [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12 + Invoke-WebRequest -Uri $SourceUrl.AbsoluteUri -OutFile $tempFile -UseBasicParsing ` + -TimeoutSec $TimeoutSec -Headers @{ 'User-Agent' = 'clumsy-get-genie' } + } + + # GitHub can return an HTML error document, so confirm the download has the + # DOS header expected at the start of a Windows executable. FileStream is + # compatible with both Windows PowerShell 5.1 and modern PowerShell. + $stream = [IO.File]::OpenRead($tempFile) + try { + $firstByte = $stream.ReadByte() + $secondByte = $stream.ReadByte() + } finally { + $stream.Dispose() + } + if ($firstByte -ne 0x4D -or $secondByte -ne 0x5A) { + throw "The downloaded file is not a valid Windows executable." + } + + # Validate the PE signature as well as the initial DOS marker. This rejects + # truncated files and arbitrary data that merely begins with "MZ". + $stream = [IO.File]::OpenRead($tempFile) + $reader = [IO.BinaryReader]::new($stream) + try { + if ($stream.Length -lt 64) { + throw 'The downloaded executable is truncated.' + } + $stream.Position = 0x3C + $peOffset = $reader.ReadUInt32() + if ($peOffset -gt ($stream.Length - 4)) { + throw 'The downloaded executable has an invalid PE header offset.' + } + $stream.Position = $peOffset + if ($reader.ReadUInt32() -ne 0x00004550) { + throw 'The downloaded file does not contain a valid PE signature.' + } + } finally { + $reader.Dispose() + $stream.Dispose() + } + + if ($ExpectedSha256) { + $actualSha256 = (Get-FileHash -LiteralPath $tempFile -Algorithm SHA256).Hash + if ($actualSha256 -ne $ExpectedSha256) { + throw "Checksum mismatch. Expected $ExpectedSha256, received $actualSha256." + } + } + + if (Test-Path $outFile -PathType Leaf) { + $backupFile = if ($KeepBackup) { "$outFile.bak" } else { $null } + if ($backupFile) { + Remove-Item -LiteralPath $backupFile -Force -ErrorAction SilentlyContinue + } + # File.Replace performs a same-volume atomic replacement on Windows. + [IO.File]::Replace($tempFile, $outFile, $backupFile) } else { - [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 - Invoke-WebRequest -Uri $genieUrl -OutFile $outFile -UseBasicParsing + Move-Item -LiteralPath $tempFile -Destination $outFile } + $tempFile = $null Write-Host "genie.exe downloaded successfully to $outFile" -ForegroundColor Green + if ($PassThru) { + Get-Item -LiteralPath $outFile + } } catch { Write-Host "ERROR: Failed to download genie.exe: $_" -ForegroundColor Red Write-Host "" Write-Host "Alternative options:" -ForegroundColor Yellow Write-Host "1. Build GENie from source: https://github.com/bkaradzic/GENie" - Write-Host "2. Download manually from: $genieUrl" + Write-Host "2. Download manually from: $SourceUrl" Write-Host " and place it at: $outFile" exit 1 +} finally { + if ($tempFile) { + Remove-Item $tempFile -Force -ErrorAction SilentlyContinue + } }