diff --git a/.github/workflows/.gitkeep b/.github/workflows/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..0997bb70 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,55 @@ +name: PresentMon CI + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +jobs: + build: + runs-on: windows-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '18' + + - name: Setup MSVC + uses: microsoft/setup-msvc@v1.1 + + - name: Define CEF Download URL + id: cef-url + run: echo "CEF_URL=https://cef-builds.spotifycdn.com/cef_binary_136.1.6%2Bg1ac1b14%2Bchromium-136.0.7103.114_windows64_minimal.tar.bz2" >> $env:GITHUB_ENV + + - name: Cache CEF + id: cache-cef + uses: actions/cache@v4 + with: + path: C:\cef + key: ${{ runner.os }}-cef-v136 + + - name: Download and extract CEF + if: steps.cache-cef.outputs.cache-hit != 'true' + shell: pwsh + run: | + echo "Downloading CEF from ${{ env.CEF_URL }}" + Invoke-WebRequest -Uri ${{ env.CEF_URL }} -OutFile cef.tar.bz2 + + New-Item -ItemType Directory -Force -Path C:\cef_temp + + tar -xjf cef.tar.bz2 -C C:\cef_temp + + $extractedDir = Get-ChildItem -Path C:\cef_temp | Select-Object -First 1 + Move-Item -Path $extractedDir.FullName\* -Destination C:\cef + + Remove-Item -Path C:\cef_temp, cef.tar.bz2 -Recurse + + - name: Run build script + shell: cmd + run: | + build.bat C:\cef diff --git a/BUILDING.md b/BUILDING.md index eb4b3e17..4d2cd06c 100644 --- a/BUILDING.md +++ b/BUILDING.md @@ -14,6 +14,18 @@ Note: if you only want to build the PresentData library, or the PresentMon Conso you only need Visual Studio. Ignore the other build and source dependency instructions and build `PresentData\PresentData.vcxproj` or `PresentMon\ConsoleApplication.sln`. +## Automated Build Script + +For convenience, a batch script `build.bat` is provided to automate the entire build process. This script will perform all the steps described below, including installing vcpkg dependencies, building CEF, building the web assets, and building the final PresentMon solution. + +To use the script: +1. Open a Command Prompt or PowerShell terminal. +2. Run the script from the root of the repository: `.\build.bat` +3. The script will pause and ask you to provide the full path to your downloaded and extracted CEF (Chromium Embedded Framework) directory. +4. The script will attempt to create and install a test certificate, which requires administrator privileges. It is recommended to run the script in a terminal with Administrator rights. + +The manual steps are still documented below if you prefer to execute the build process step-by-step. + ## Install Source Dependencies 1. Download and install *vcpkg*, which will be used to obtain source package dependencies during the build: diff --git a/PresentMon/CommandLine.cpp b/PresentMon/CommandLine.cpp index c0236ed7..3e15a157 100644 --- a/PresentMon/CommandLine.cpp +++ b/PresentMon/CommandLine.cpp @@ -534,13 +534,12 @@ bool ParseCommandLine(int argc, wchar_t** argv) } // Ignore CSV-only options when --no_csv is used - if (csvOutputNone && (qpcTime || qpcmsTime || dtTime || args->mMultiCsv || args->mHotkeySupport)) { + if (csvOutputNone && (qpcTime || qpcmsTime || dtTime || args->mMultiCsv)) { PrintWarning(L"warning: ignoring CSV-related options due to --no_csv:"); if (qpcTime) { qpcTime = false; PrintWarning(L" --qpc_time"); } if (qpcmsTime) { qpcmsTime = false; PrintWarning(L" --qpc_time_ms"); } if (dtTime) { dtTime = false; PrintWarning(L" --date_time"); } if (args->mMultiCsv) { args->mMultiCsv = false; PrintWarning(L" --multi_csv"); } - if (args->mHotkeySupport) { args->mHotkeySupport = false; PrintWarning(L" --hotkey"); } PrintWarning(L"\n"); } diff --git a/Tests/CommandLineTests.cpp b/Tests/CommandLineTests.cpp index e38466e5..df9393e8 100644 --- a/Tests/CommandLineTests.cpp +++ b/Tests/CommandLineTests.cpp @@ -227,3 +227,30 @@ void InputTest(uint32_t v) TEST(CommandLineTests, Input_v1) { InputTest(1); } TEST(CommandLineTests, Input_v2) { InputTest(2); } + +TEST(CommandLineTests, HotkeyWithoutCsv) +{ + PresentMon pm; + pm.Add(L"--stop_existing_session --no_csv --hotkey F11"); + pm.PMSTART(); + EXPECT_TRUE(pm.IsRunning(1000)); + + // Simulate F11 key press + INPUT input[2] = {}; + input[0].type = INPUT_KEYBOARD; + input[0].ki.wVk = VK_F11; + input[1].type = INPUT_KEYBOARD; + input[1].ki.wVk = VK_F11; + input[1].ki.dwFlags = KEYEVENTF_KEYUP; + SendInput(2, input, sizeof(INPUT)); + + // Wait for hotkey to be processed + Sleep(1000); + + // Check the output + std::string output = pm.GetOutput(); + EXPECT_NE(output.find("Started recording."), std::string::npos); + + // Terminate the process + pm.PMEXITED(1000, 0); +} diff --git a/Tests/PresentMon.cpp b/Tests/PresentMon.cpp index 1bc1584f..2d968ff0 100644 --- a/Tests/PresentMon.cpp +++ b/Tests/PresentMon.cpp @@ -394,7 +394,7 @@ PresentMon::PresentMon() { cmdline_ += L'\"'; cmdline_ += exePath_; - cmdline_ += L"\" --no_console_stats"; + cmdline_ += L'\"'; } PresentMon::~PresentMon() @@ -402,6 +402,12 @@ PresentMon::~PresentMon() if (::testing::Test::HasFailure()) { printf("%ls\n", cmdline_.c_str()); } + if (hOutputRead_ != NULL) { + CloseHandle(hOutputRead_); + } + if (hOutputWrite_ != NULL) { + CloseHandle(hOutputWrite_); + } } void PresentMon::AddEtlPath(std::wstring const& etlPath) @@ -437,9 +443,26 @@ void PresentMon::Start(char const* file, int line) csvArgSet_ = true; } + SECURITY_ATTRIBUTES sa = {}; + sa.nLength = sizeof(sa); + sa.bInheritHandle = TRUE; + + if (!CreatePipe(&hOutputRead_, &hOutputWrite_, &sa, 0)) { + AddTestFailure(file, line, "Failed to create output pipe"); + return; + } + + if (!SetHandleInformation(hOutputRead_, HANDLE_FLAG_INHERIT, 0)) { + AddTestFailure(file, line, "Failed to make output pipe inheritable"); + return; + } + STARTUPINFO si = {}; si.cb = sizeof(si); si.dwFlags = STARTF_USESTDHANDLES; + si.hStdOutput = hOutputWrite_; + si.hStdError = hOutputWrite_; + if (CreateProcess(nullptr, &cmdline_[0], nullptr, nullptr, TRUE, 0, nullptr, nullptr, &si, (PROCESS_INFORMATION*) this) == 0) { AddTestFailure(file, line, "Failed to start PresentMon"); } @@ -470,3 +493,26 @@ void PresentMon::ExpectExited(char const* file, int line, DWORD timeoutMilliseco CloseHandle(hProcess); CloseHandle(hThread); } + +std::string PresentMon::GetOutput() +{ + if (hOutputRead_ == NULL) { + return ""; + } + + // Close the write handle to the pipe so that ReadFile() will not block + // if the child process has already exited. + if (hOutputWrite_ != NULL) { + CloseHandle(hOutputWrite_); + hOutputWrite_ = NULL; + } + + std::string output; + char buffer[4096]; + DWORD bytesRead; + while (ReadFile(hOutputRead_, buffer, sizeof(buffer), &bytesRead, NULL) && bytesRead > 0) { + output.append(buffer, bytesRead); + } + + return output; +} diff --git a/Tests/PresentMonTests.h b/Tests/PresentMonTests.h index 2a359bc1..5934aa0b 100644 --- a/Tests/PresentMonTests.h +++ b/Tests/PresentMonTests.h @@ -211,6 +211,8 @@ struct PresentMon : PROCESS_INFORMATION { static std::wstring exePath_; std::wstring cmdline_; bool csvArgSet_; + HANDLE hOutputRead_ = NULL; + HANDLE hOutputWrite_ = NULL; PresentMon(); ~PresentMon(); @@ -226,6 +228,9 @@ struct PresentMon : PROCESS_INFORMATION { // Expect the process to exit with expectedExitCode within // timeoutMilliseconds (or kill it otherwise). void ExpectExited(char const* file, int line, DWORD timeoutMilliseconds=INFINITE, DWORD expectedExitCode=0); + + // Read all output from the process. + std::string GetOutput(); }; #define PMSTART() Start(__FILE__, __LINE__) diff --git a/build.bat b/build.bat new file mode 100644 index 00000000..97b1054f --- /dev/null +++ b/build.bat @@ -0,0 +1,155 @@ +@echo off +setlocal + +echo ================================================================= +echo PresentMon Build Script +echo ================================================================= +echo. +echo This script will automate the build process for PresentMon. +echo It follows the instructions from BUILDING.md. +echo. +echo Please ensure you have the following prerequisites installed: +echo - Visual Studio 2022 (with C++ workload) +echo - CMake +echo - Node.js / NPM +echo - WiX toolset v3 +echo. +echo ================================================================= +echo. + +REM Function to check for errors +:check_error +if %errorlevel% neq 0 ( + echo. + echo *************************************************************** + echo An error occurred. Aborting script. + echo *************************************************************** + exit /b %errorlevel% +) +goto :eof + + +echo [1/6] Installing vcpkg dependencies... +echo ----------------------------------------------------------------- +if not exist "build\vcpkg\vcpkg.exe" ( + echo Cloning vcpkg repository... + git clone https://github.com/Microsoft/vcpkg.git build\vcpkg + call :check_error + + echo Bootstrapping vcpkg... + call build\vcpkg\bootstrap-vcpkg.bat + call :check_error +) else ( + echo vcpkg already found. Skipping clone and bootstrap. +) + +echo Integrating vcpkg... +call build\vcpkg\vcpkg.exe integrate install +call :check_error + +echo Installing vcpkg packages... +call build\vcpkg\vcpkg.exe install +call :check_error +echo ----------------------------------------------------------------- +echo. + + +echo [2/6] Building Chromium Embedded Framework (CEF) +echo ----------------------------------------------------------------- +if "%~1"=="" ( + set /p "CEF_DIR=Please enter the full path to your extracted CEF directory (e.g., C:\cef_133): " +) else ( + set "CEF_DIR=%~1" + echo Using CEF directory from command line argument: %CEF_DIR% +) + +if not exist "%CEF_DIR%\CMakeLists.txt" ( + echo Error: CEF directory not found or invalid at '%CEF_DIR%'. + echo Please download and extract the CEF minimal distribution from + echo https://cef-builds.spotifycdn.com/index.html + exit /b 1 +) + +echo Building CEF (Debug and Release)... +cmake -G "Visual Studio 17 2022" -A x64 -DUSE_SANDBOX=OFF -S "%CEF_DIR%" -B "%CEF_DIR%\build" +call :check_error + +cmake --build "%CEF_DIR%\build" --config Debug +call :check_error + +cmake --build "%CEF_DIR%\build" --config Release +call :check_error + +echo Pulling CEF build outputs into the project... +call IntelPresentMon\AppCef\Batch\pull-cef.bat "%CEF_DIR%" +call :check_error +echo ----------------------------------------------------------------- +echo. + + +echo [3/6] Building Web Asset Dependencies (NPM) +echo ----------------------------------------------------------------- +pushd IntelPresentMon\AppCef\ipm-ui-vue +echo Installing npm packages... +npm ci +call :check_error + +echo Building web assets... +npm run build +call :check_error +popd +echo ----------------------------------------------------------------- +echo. + + +echo [4/6] Creating and Installing Test Certificate +echo ----------------------------------------------------------------- +echo This step requires Administrator privileges. +echo If the script fails here, please re-run it from an +echo Administrator command prompt. +echo. +makecert -r -pe -n "CN=Test Certificate - For Internal Use Only" -ss PrivateCertStore testcert.cer > nul 2>&1 +if %errorlevel% neq 0 ( + echo WARNING: Failed to create certificate. This may be because + echo 'makecert.exe' is not in your PATH or you are not + echo running as an Administrator. The Release build might + echo fail to run. +) else ( + certutil -addstore root testcert.cer + call :check_error + echo Certificate created and installed successfully. +) +echo ----------------------------------------------------------------- +echo. + + +echo [5/6] Building PresentMon Solution +echo ----------------------------------------------------------------- +echo Building Debug configuration... +msbuild /p:Platform=x64,Configuration=Debug PresentMon.sln +call :check_error + +echo Building Release configuration... +msbuild /p:Platform=x64,Configuration=Release PresentMon.sln +call :check_error +echo ----------------------------------------------------------------- +echo. + + +echo [6/6] Build complete! +echo ================================================================= +echo. +echo Binaries can be found in the 'build\Debug' and 'build\Release' +echo directories. +echo. +echo To run the application: +echo - Start the service (as Administrator): +echo sc.exe create PresentMonService binPath=\"%cd%\build\Release\PresentMonService.exe\" +echo sc.exe start PresentMonService +echo - Run the capture app from its directory: +echo cd build\Release +echo PresentMon.exe +echo. +echo ================================================================= + +endlocal