diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 646766c..2cb0e66 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -7,13 +7,17 @@ on: jobs: build-win32: - runs-on: windows-latest + runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 + with: + submodules: recursive - - name: Install NSIS - run: choco install nsis -y - + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: '1.21' + - name: WBAB Preflight shell: bash run: ./tools/wbab preflight @@ -56,4 +60,4 @@ jobs: uses: softprops/action-gh-release@v1 with: files: dist/* - generate_release_notes: true \ No newline at end of file + generate_release_notes: true diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..c8d574f --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "tools/WineBotAppBuilder"] + path = tools/WineBotAppBuilder + url = https://github.com/SemperSupra/WineBotAppBuilder diff --git a/core/include/wininspect/backend.hpp b/core/include/wininspect/backend.hpp index 29a9a57..de15f8f 100644 --- a/core/include/wininspect/backend.hpp +++ b/core/include/wininspect/backend.hpp @@ -33,7 +33,8 @@ class IBackend { virtual bool send_input(const std::vector &raw_input_data) = 0; // Higher-level injection helpers - virtual bool send_mouse_click(int x, int y, int button) = 0; // 0=left, 1=right, 2=middle + virtual bool send_mouse_click(int x, int y, + int button) = 0; // 0=left, 1=right, 2=middle virtual bool send_key_press(int vk) = 0; virtual bool send_text(const std::string &text) = 0; diff --git a/core/src/fake_backend.cpp b/core/src/fake_backend.cpp index f2b7eff..5e45979 100644 --- a/core/src/fake_backend.cpp +++ b/core/src/fake_backend.cpp @@ -121,7 +121,8 @@ std::vector FakeBackend::inspect_ui_elements(hwnd_u64 parent) { return it->second; } -void FakeBackend::add_fake_ui_element(hwnd_u64 parent, const UIElementInfo &info) { +void FakeBackend::add_fake_ui_element(hwnd_u64 parent, + const UIElementInfo &info) { std::lock_guard lk(mu_); ui_elements_[parent].push_back(info); } diff --git a/core/src/win32_backend.cpp b/core/src/win32_backend.cpp index 5bb9fc5..4519543 100644 --- a/core/src/win32_backend.cpp +++ b/core/src/win32_backend.cpp @@ -2,12 +2,12 @@ #ifdef _WIN32 #define WIN32_LEAN_AND_MEAN +#include +#include #include #include #include #include -#include -#include namespace wininspect { @@ -30,7 +30,8 @@ static std::string w2u8(const std::wstring &ws) { } static std::string bstr_to_utf8(BSTR bstr) { - if (!bstr) return {}; + if (!bstr) + return {}; std::wstring ws(bstr, SysStringLen(bstr)); return w2u8(ws); } @@ -191,164 +192,174 @@ bool Win32Backend::send_input(const std::vector &raw_input_data) { } bool Win32Backend::send_mouse_click(int x, int y, int button) { - // 0=left, 1=right, 2=middle - // Use absolute coordinates - int sw = GetSystemMetrics(SM_CXSCREEN); - int sh = GetSystemMetrics(SM_CYSCREEN); - if (sw == 0) sw = 1; - if (sh == 0) sh = 1; - - // Normalize to 0-65535 - int nx = (x * 65535) / sw; - int ny = (y * 65535) / sh; - - INPUT inputs[2] = {}; - inputs[0].type = INPUT_MOUSE; - inputs[0].mi.dx = nx; - inputs[0].mi.dy = ny; - inputs[0].mi.dwFlags = MOUSEEVENTF_ABSOLUTE | MOUSEEVENTF_MOVE; - - if (button == 0) { - inputs[0].mi.dwFlags |= MOUSEEVENTF_LEFTDOWN; - inputs[1] = inputs[0]; - inputs[1].mi.dwFlags = MOUSEEVENTF_ABSOLUTE | MOUSEEVENTF_LEFTUP; - } else if (button == 1) { - inputs[0].mi.dwFlags |= MOUSEEVENTF_RIGHTDOWN; - inputs[1] = inputs[0]; - inputs[1].mi.dwFlags = MOUSEEVENTF_ABSOLUTE | MOUSEEVENTF_RIGHTUP; - } else if (button == 2) { - inputs[0].mi.dwFlags |= MOUSEEVENTF_MIDDLEDOWN; - inputs[1] = inputs[0]; - inputs[1].mi.dwFlags = MOUSEEVENTF_ABSOLUTE | MOUSEEVENTF_MIDDLEUP; - } else { - return false; - } + // 0=left, 1=right, 2=middle + // Use absolute coordinates + int sw = GetSystemMetrics(SM_CXSCREEN); + int sh = GetSystemMetrics(SM_CYSCREEN); + if (sw == 0) + sw = 1; + if (sh == 0) + sh = 1; + + // Normalize to 0-65535 + int nx = (x * 65535) / sw; + int ny = (y * 65535) / sh; + + INPUT inputs[2] = {}; + inputs[0].type = INPUT_MOUSE; + inputs[0].mi.dx = nx; + inputs[0].mi.dy = ny; + inputs[0].mi.dwFlags = MOUSEEVENTF_ABSOLUTE | MOUSEEVENTF_MOVE; + + if (button == 0) { + inputs[0].mi.dwFlags |= MOUSEEVENTF_LEFTDOWN; + inputs[1] = inputs[0]; + inputs[1].mi.dwFlags = MOUSEEVENTF_ABSOLUTE | MOUSEEVENTF_LEFTUP; + } else if (button == 1) { + inputs[0].mi.dwFlags |= MOUSEEVENTF_RIGHTDOWN; + inputs[1] = inputs[0]; + inputs[1].mi.dwFlags = MOUSEEVENTF_ABSOLUTE | MOUSEEVENTF_RIGHTUP; + } else if (button == 2) { + inputs[0].mi.dwFlags |= MOUSEEVENTF_MIDDLEDOWN; + inputs[1] = inputs[0]; + inputs[1].mi.dwFlags = MOUSEEVENTF_ABSOLUTE | MOUSEEVENTF_MIDDLEUP; + } else { + return false; + } - // Send click - return SendInput(2, inputs, sizeof(INPUT)) == 2; + // Send click + return SendInput(2, inputs, sizeof(INPUT)) == 2; } bool Win32Backend::send_key_press(int vk) { - INPUT inputs[2] = {}; - inputs[0].type = INPUT_KEYBOARD; - inputs[0].ki.wVk = (WORD)vk; + INPUT inputs[2] = {}; + inputs[0].type = INPUT_KEYBOARD; + inputs[0].ki.wVk = (WORD)vk; - inputs[1] = inputs[0]; - inputs[1].ki.dwFlags = KEYEVENTF_KEYUP; + inputs[1] = inputs[0]; + inputs[1].ki.dwFlags = KEYEVENTF_KEYUP; - return SendInput(2, inputs, sizeof(INPUT)) == 2; + return SendInput(2, inputs, sizeof(INPUT)) == 2; } bool Win32Backend::send_text(const std::string &text) { - std::wstring wtext; - int len = MultiByteToWideChar(CP_UTF8, 0, text.c_str(), -1, NULL, 0); - if (len > 0) { - wtext.resize(len - 1); - MultiByteToWideChar(CP_UTF8, 0, text.c_str(), -1, wtext.data(), len); - } + std::wstring wtext; + int len = MultiByteToWideChar(CP_UTF8, 0, text.c_str(), -1, NULL, 0); + if (len > 0) { + wtext.resize(len - 1); + MultiByteToWideChar(CP_UTF8, 0, text.c_str(), -1, wtext.data(), len); + } - if (wtext.empty()) return true; + if (wtext.empty()) + return true; - std::vector inputs; - inputs.reserve(wtext.size() * 2); + std::vector inputs; + inputs.reserve(wtext.size() * 2); - for (wchar_t c : wtext) { - INPUT i = {}; - i.type = INPUT_KEYBOARD; - i.ki.wScan = c; - i.ki.dwFlags = KEYEVENTF_UNICODE; - inputs.push_back(i); + for (wchar_t c : wtext) { + INPUT i = {}; + i.type = INPUT_KEYBOARD; + i.ki.wScan = c; + i.ki.dwFlags = KEYEVENTF_UNICODE; + inputs.push_back(i); - i.ki.dwFlags |= KEYEVENTF_KEYUP; - inputs.push_back(i); - } + i.ki.dwFlags |= KEYEVENTF_KEYUP; + inputs.push_back(i); + } - return SendInput((UINT)inputs.size(), inputs.data(), sizeof(INPUT)) == inputs.size(); + return SendInput((UINT)inputs.size(), inputs.data(), sizeof(INPUT)) == + inputs.size(); } std::vector Win32Backend::inspect_ui_elements(hwnd_u64 parent) { - std::vector results; - - // Attempt to initialize COM. If already initialized, it might fail but that's ok if we can create the instance. - HRESULT hrInit = CoInitializeEx(NULL, COINIT_MULTITHREADED); - // Ignore result, just proceed to create instance. - - IUIAutomation* pAutomation = NULL; - HRESULT hr = CoCreateInstance(CLSID_CUIAutomation, NULL, CLSCTX_INPROC_SERVER, IID_IUIAutomation, (void**)&pAutomation); - if (FAILED(hr)) { - // Log failure? - if (SUCCEEDED(hrInit)) CoUninitialize(); - return results; - } + std::vector results; + + // Attempt to initialize COM. If already initialized, it might fail but that's + // ok if we can create the instance. + HRESULT hrInit = CoInitializeEx(NULL, COINIT_MULTITHREADED); + // Ignore result, just proceed to create instance. + + IUIAutomation *pAutomation = NULL; + HRESULT hr = CoCreateInstance(CLSID_CUIAutomation, NULL, CLSCTX_INPROC_SERVER, + IID_IUIAutomation, (void **)&pAutomation); + if (FAILED(hr)) { + // Log failure? + if (SUCCEEDED(hrInit)) + CoUninitialize(); + return results; + } - IUIAutomationElement* pRoot = NULL; - HWND hParent = from_u64(parent); - if (IsWindow(hParent)) { - hr = pAutomation->ElementFromHandle(hParent, &pRoot); - } else { - // If invalid handle, maybe use desktop? No, just return empty. - } + IUIAutomationElement *pRoot = NULL; + HWND hParent = from_u64(parent); + if (IsWindow(hParent)) { + hr = pAutomation->ElementFromHandle(hParent, &pRoot); + } else { + // If invalid handle, maybe use desktop? No, just return empty. + } - if (SUCCEEDED(hr) && pRoot) { - // Find children - IUIAutomationCondition* pTrueCondition = NULL; - pAutomation->CreateTrueCondition(&pTrueCondition); - IUIAutomationElementArray* pChildren = NULL; - if (pTrueCondition) { - pRoot->FindAll(TreeScope_Children, pTrueCondition, &pChildren); - pTrueCondition->Release(); - } + if (SUCCEEDED(hr) && pRoot) { + // Find children + IUIAutomationCondition *pTrueCondition = NULL; + pAutomation->CreateTrueCondition(&pTrueCondition); + IUIAutomationElementArray *pChildren = NULL; + if (pTrueCondition) { + pRoot->FindAll(TreeScope_Children, pTrueCondition, &pChildren); + pTrueCondition->Release(); + } - if (pChildren) { - int length = 0; - pChildren->get_Length(&length); - for (int i = 0; i < length; i++) { - IUIAutomationElement* pNode = NULL; - if (SUCCEEDED(pChildren->GetElement(i, &pNode)) && pNode) { - UIElementInfo info; - - BSTR bStr = NULL; - if (SUCCEEDED(pNode->get_CurrentAutomationId(&bStr))) { - info.automation_id = bstr_to_utf8(bStr); - SysFreeString(bStr); - } - if (SUCCEEDED(pNode->get_CurrentName(&bStr))) { - info.name = bstr_to_utf8(bStr); - SysFreeString(bStr); - } - if (SUCCEEDED(pNode->get_CurrentClassName(&bStr))) { - info.class_name = bstr_to_utf8(bStr); - SysFreeString(bStr); - } - // Control type is int, convert to string? - CONTROLTYPEID cType; - if (SUCCEEDED(pNode->get_CurrentControlType(&cType))) { - // Simple mapping or just raw ID - info.control_type = std::to_string(cType); - } - - RECT r = {}; - if (SUCCEEDED(pNode->get_CurrentBoundingRectangle(&r))) { - info.bounding_rect = {r.left, r.top, r.right, r.bottom}; - } - - BOOL bVal = FALSE; - if (SUCCEEDED(pNode->get_CurrentIsEnabled(&bVal))) info.enabled = bVal; - if (SUCCEEDED(pNode->get_CurrentIsOffscreen(&bVal))) info.visible = !bVal; // IsOffscreen means NOT visible usually - - results.push_back(info); - pNode->Release(); - } - } - pChildren->Release(); + if (pChildren) { + int length = 0; + pChildren->get_Length(&length); + for (int i = 0; i < length; i++) { + IUIAutomationElement *pNode = NULL; + if (SUCCEEDED(pChildren->GetElement(i, &pNode)) && pNode) { + UIElementInfo info; + + BSTR bStr = NULL; + if (SUCCEEDED(pNode->get_CurrentAutomationId(&bStr))) { + info.automation_id = bstr_to_utf8(bStr); + SysFreeString(bStr); + } + if (SUCCEEDED(pNode->get_CurrentName(&bStr))) { + info.name = bstr_to_utf8(bStr); + SysFreeString(bStr); + } + if (SUCCEEDED(pNode->get_CurrentClassName(&bStr))) { + info.class_name = bstr_to_utf8(bStr); + SysFreeString(bStr); + } + // Control type is int, convert to string? + CONTROLTYPEID cType; + if (SUCCEEDED(pNode->get_CurrentControlType(&cType))) { + // Simple mapping or just raw ID + info.control_type = std::to_string(cType); + } + + RECT r = {}; + if (SUCCEEDED(pNode->get_CurrentBoundingRectangle(&r))) { + info.bounding_rect = {r.left, r.top, r.right, r.bottom}; + } + + BOOL bVal = FALSE; + if (SUCCEEDED(pNode->get_CurrentIsEnabled(&bVal))) + info.enabled = bVal; + if (SUCCEEDED(pNode->get_CurrentIsOffscreen(&bVal))) + info.visible = !bVal; // IsOffscreen means NOT visible usually + + results.push_back(info); + pNode->Release(); } - pRoot->Release(); + } + pChildren->Release(); } + pRoot->Release(); + } - pAutomation->Release(); - if (SUCCEEDED(hrInit)) CoUninitialize(); + pAutomation->Release(); + if (SUCCEEDED(hrInit)) + CoUninitialize(); - return results; + return results; } static std::vector sorted(std::vector v) { @@ -404,7 +415,9 @@ bool Win32Backend::send_input(const std::vector &) { return false; } bool Win32Backend::send_mouse_click(int, int, int) { return false; } bool Win32Backend::send_key_press(int) { return false; } bool Win32Backend::send_text(const std::string &) { return false; } -std::vector Win32Backend::inspect_ui_elements(hwnd_u64) { return {}; } +std::vector Win32Backend::inspect_ui_elements(hwnd_u64) { + return {}; +} std::vector Win32Backend::poll_events(const Snapshot &, const Snapshot &) { diff --git a/tools/WineBotAppBuilder b/tools/WineBotAppBuilder new file mode 160000 index 0000000..b2fd649 --- /dev/null +++ b/tools/WineBotAppBuilder @@ -0,0 +1 @@ +Subproject commit b2fd6499dbac93f28191a4610cc603e2887478e5 diff --git a/tools/WineBotAppBuilder/.keep b/tools/WineBotAppBuilder/.keep deleted file mode 100644 index fb3d8d6..0000000 --- a/tools/WineBotAppBuilder/.keep +++ /dev/null @@ -1 +0,0 @@ -Placeholder for WBAB submodule. diff --git a/tools/ci-build.sh b/tools/ci-build.sh new file mode 100755 index 0000000..7d269c7 --- /dev/null +++ b/tools/ci-build.sh @@ -0,0 +1,31 @@ +#!/usr/bin/env bash +set -euo pipefail + +# CI Build script for WBAB container (Cross-compile + Wine setup) + +echo "--- Building WinInspect Core & Win32 Clients (Containerized) ---" + +mkdir -p build +cd build + +# Configure CMake for MinGW-w64 cross-compilation with Wine emulator for tests +cmake -S .. -B . \ + -DCMAKE_SYSTEM_NAME=Windows \ + -DCMAKE_C_COMPILER=x86_64-w64-mingw32-gcc \ + -DCMAKE_CXX_COMPILER=x86_64-w64-mingw32-g++ \ + -DCMAKE_RC_COMPILER=x86_64-w64-mingw32-windres \ + -DCMAKE_FIND_ROOT_PATH=/usr/x86_64-w64-mingw32 \ + -DCMAKE_FIND_ROOT_PATH_MODE_PROGRAM=NEVER \ + -DCMAKE_FIND_ROOT_PATH_MODE_LIBRARY=ONLY \ + -DCMAKE_FIND_ROOT_PATH_MODE_INCLUDE=ONLY \ + -DCMAKE_CROSSCOMPILING_EMULATOR=/usr/bin/wine \ + -DWININSPECT_BUILD_TESTS=ON + +cmake --build . --config Release + +# Copy artifacts to out/ (expected by WBAB convention) +echo "--- Copying artifacts to out/ ---" +rm -rf ../out +mkdir -p ../out +find . -name "*.exe" -exec cp -f {} "../out/" \; +find . -name "*.dll" -exec cp -f {} "../out/" \; diff --git a/tools/package-nsis.sh b/tools/package-nsis.sh index 58de2b7..c05efa5 100755 --- a/tools/package-nsis.sh +++ b/tools/package-nsis.sh @@ -10,7 +10,9 @@ mkdir -p "${DIST_DIR}" echo "--- Packaging WinInspect Installer (Version: ${VERSION}) ---" if command -v makensis &> /dev/null; then - makensis -DVERSION="${VERSION}" "${WORKSPACE_DIR}/tools/wininspect.nsi" + # Default to standard local build path if not specified + BUILD_SRC="${BUILD_SRC:-..\build\Release}" + makensis -DVERSION="${VERSION}" -DBUILD_SRC="${BUILD_SRC}" "${WORKSPACE_DIR}/tools/wininspect.nsi" mv "${DIST_DIR}/WinInspect-Installer.exe" "${DIST_DIR}/WinInspect-Installer-${VERSION}.exe" else echo "ERROR: makensis not found. Cannot build installer." diff --git a/tools/wbab b/tools/wbab index 60d11c1..4b63647 100755 --- a/tools/wbab +++ b/tools/wbab @@ -12,30 +12,18 @@ wbab (WinInspect) Usage: tools/wbab [args] This repository is scaffolded to work with WineBotAppBuilder (WBAB). -If you add WBAB as a submodule at tools/WineBotAppBuilder, this wrapper can -delegate to WBAB's runners. - -Fallback behavior (no submodule): run local CMake build/test on Windows. +If WBAB submodule is present (tools/WineBotAppBuilder), build/test/package +commands will run inside WBAB containers. EOF } run_local_build() { - cmake -S "${ROOT}" -B "${ROOT}/build" -DWININSPECT_BUILD_TESTS=ON - cmake --build "${ROOT}/build" --config Release + "${ROOT}/tools/winbuild-build.sh" "$@" } run_local_test() { - ctest --test-dir "${ROOT}/build" -C Release --output-on-failure - - if command -v go &> /dev/null; then - if [ -d "${ROOT}/clients/portable" ]; then - echo "Running Go tests..." - pushd "${ROOT}/clients/portable" > /dev/null - go test ./... - popd > /dev/null - fi - fi + "${ROOT}/scripts/wbab-test.sh" "$@" } case "${cmd}" in @@ -43,20 +31,67 @@ case "${cmd}" in "${ROOT}/scripts/preflight.sh" ;; lint) + # Linting is kept local for now to avoid container dependency issues (clang-format) "${ROOT}/scripts/lint.sh" "$@" ;; build) - "${ROOT}/tools/winbuild-build.sh" "$@" + if [[ -d "${WBAB_DIR}" ]]; then + echo "--- Running WBAB Containerized Build ---" + # Delegate to WBAB container build + export WBAB_BUILD_CMD="./tools/ci-build.sh" + "${WBAB_DIR}/tools/wbab" build "$@" + + # Post-process artifacts (copy from out/ to dist/) + VERSION="${1:-dev}" + DIST_DIR="${ROOT}/dist" + mkdir -p "${DIST_DIR}" + + echo "--- Post-processing Artifacts ---" + # wbab-build/ci-build puts them in out/ + # We need to mimic tools/winbuild-build.sh naming: + cp "${ROOT}/out/wininspectd.exe" "${DIST_DIR}/wininspectd-${VERSION}-win-x64.exe" 2>/dev/null || true + cp "${ROOT}/out/wininspect.exe" "${DIST_DIR}/wininspect-${VERSION}-win-x64.exe" 2>/dev/null || true + cp "${ROOT}/out/wininspect-gui.exe" "${DIST_DIR}/wininspect-gui-${VERSION}-win-x64.exe" 2>/dev/null || true + + # Run Go build locally + echo "--- Building Portable CLI (Go) Locally ---" + if command -v go &> /dev/null; then + if [ -d "${ROOT}/clients/portable" ]; then + pushd "${ROOT}/clients/portable" > /dev/null + GOOS=linux GOARCH=amd64 go build -o "${DIST_DIR}/wi-portable-${VERSION}-linux-x64" + GOOS=windows GOARCH=amd64 go build -o "${DIST_DIR}/wi-portable-${VERSION}-win-x64.exe" + popd > /dev/null + fi + else + echo "Skipping Go build (compiler not found)." + fi + else + echo "WBAB submodule not found. Running local build..." + run_local_build "$@" + fi ;; test) if [[ -d "${WBAB_DIR}" ]]; then - "${ROOT}/scripts/wbab-test.sh" "$@" + echo "--- Running WBAB Containerized Test ---" + # Delegate to WBAB container test + # We use ctest on the build directory which is persisted in workspace + export WBAB_TEST_CMD="ctest --test-dir build -C Release --output-on-failure" + "${WBAB_DIR}/tools/wbab" test "$@" else - run_local_test + run_local_test "$@" fi ;; package) - "${ROOT}/tools/package-nsis.sh" "$@" + if [[ -d "${WBAB_DIR}" ]]; then + echo "--- Running WBAB Containerized Packaging ---" + # Delegate to WBAB container package + export WBAB_PACKAGE_CMD="./tools/package-nsis.sh" + # Tell package-nsis.sh to look for binaries in ../out (relative to tools/) + export BUILD_SRC="../out" + "${WBAB_DIR}/tools/wbab" package "$@" + else + "${ROOT}/tools/package-nsis.sh" "$@" + fi ;; sign) "${ROOT}/tools/sign-dev.sh" "$@" diff --git a/tools/wininspect.nsi b/tools/wininspect.nsi index d13f33d..3c90082 100644 --- a/tools/wininspect.nsi +++ b/tools/wininspect.nsi @@ -4,6 +4,10 @@ !define VERSION "dev" !endif +!ifndef BUILD_SRC + !define BUILD_SRC "..\build\Release" +!endif + Name "WinInspect ${VERSION}" OutFile "..\\dist\\WinInspect-Installer.exe" InstallDir "$PROGRAMFILES64\\WinInspect" @@ -27,9 +31,9 @@ Section "WinInspect Core" SecCore SetOutPath "$INSTDIR" ; Binaries (assuming they are in build/Release/) - File "..\build\Release\wininspectd.exe" - File "..\build\Release\wininspect.exe" - File "..\build\Release\wininspect-gui.exe" + File "${BUILD_SRC}\wininspectd.exe" + File "${BUILD_SRC}\wininspect.exe" + File "${BUILD_SRC}\wininspect-gui.exe" ; License File "..\LICENSE"