diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..30b4a81 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,31 @@ +name: CI + +on: + push: + branches: [ "master", "main" ] + pull_request: + branches: [ "master", "main" ] + +jobs: + test-win32: + runs-on: windows-latest + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + + - uses: actions/setup-go@v5 + with: + go-version: 'stable' + + - name: WBAB Preflight + shell: bash + run: ./tools/wbab preflight + + - name: WBAB Build + shell: bash + run: ./tools/wbab build ${GITHUB_REF_NAME} + + - name: WBAB Test + shell: bash + run: ./tools/wbab test diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 2cb0e66..a1bc96f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -7,16 +7,18 @@ on: jobs: build-win32: - runs-on: ubuntu-latest + runs-on: windows-latest steps: - uses: actions/checkout@v4 with: submodules: recursive - - name: Set up Go - uses: actions/setup-go@v5 + - uses: actions/setup-go@v5 with: - go-version: '1.21' + go-version: 'stable' + + - name: Install NSIS + run: choco install nsis -y - name: WBAB Preflight shell: bash @@ -34,6 +36,10 @@ jobs: shell: bash run: ./tools/wbab test + - name: WBAB Test + shell: bash + run: ./tools/wbab test + - name: WBAB Package shell: bash run: ./tools/wbab package ${GITHUB_REF_NAME} @@ -60,4 +66,4 @@ jobs: uses: softprops/action-gh-release@v1 with: files: dist/* - generate_release_notes: true + generate_release_notes: true \ No newline at end of file diff --git a/.gitignore b/.gitignore index 1b1e010..1066b1b 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,4 @@ wi_nonce* bin/ obj/ out/ +wininspect-portable diff --git a/.gitmodules b/.gitmodules deleted file mode 100644 index c8d574f..0000000 --- a/.gitmodules +++ /dev/null @@ -1,3 +0,0 @@ -[submodule "tools/WineBotAppBuilder"] - path = tools/WineBotAppBuilder - url = https://github.com/SemperSupra/WineBotAppBuilder diff --git a/CMakeLists.txt b/CMakeLists.txt index 600b978..13775cf 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -6,6 +6,10 @@ option(WININSPECT_BUILD_TESTS "Build tests" ON) set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD_REQUIRED ON) +if (WIN32) + add_compile_definitions(UNICODE _UNICODE) +endif() + add_library(wininspect_core core/src/core.cpp core/src/fake_backend.cpp diff --git a/clients/cli/src/cli.cpp b/clients/cli/src/cli.cpp index 276fe64..5daab63 100644 --- a/clients/cli/src/cli.cpp +++ b/clients/cli/src/cli.cpp @@ -21,6 +21,52 @@ static const wchar_t *PIPE_NAME = L"\\\\.\\pipe\\wininspectd"; +struct Conn { + bool is_tcp = false; + SOCKET s = INVALID_SOCKET; + HANDLE hPipe = INVALID_HANDLE_VALUE; + + bool send(const std::string& data) { + uint32_t len = (uint32_t)data.size(); + if (is_tcp) { + if (::send(s, (const char*)&len, 4, 0) != 4) return false; + return ::send(s, data.data(), (int)len, 0) == (int)len; + } else { + DWORD w = 0; + if (!WriteFile(hPipe, &len, 4, &w, nullptr)) return false; + return WriteFile(hPipe, data.data(), len, &w, nullptr) != FALSE; + } + } + + bool recv(std::string& out) { + uint32_t len = 0; + if (is_tcp) { + if (::recv(s, (char*)&len, 4, 0) != 4) return false; + out.resize(len); + uint32_t n = 0; + while (n < len) { + int r = ::recv(s, out.data() + n, (int)(len - n), 0); + if (r <= 0) return false; + n += r; + } + return true; + } else { + DWORD r = 0; + if (!ReadFile(hPipe, &len, 4, &r, nullptr)) return false; + out.resize(len); + if (!ReadFile(hPipe, out.data(), len, &r, nullptr)) return false; + return true; + } + } + + void close() { + if (is_tcp && s != INVALID_SOCKET) { closesocket(s); s = INVALID_SOCKET; } + if (!is_tcp && hPipe != INVALID_HANDLE_VALUE) { CloseHandle(hPipe); hPipe = INVALID_HANDLE_VALUE; } + } + + ~Conn() { close(); } +}; + static std::string get_config_path() { const char *home = getenv("USERPROFILE"); if (!home) diff --git a/clients/gui/src/gui_main.cpp b/clients/gui/src/gui_main.cpp index 1da492f..6ba3e4e 100644 --- a/clients/gui/src/gui_main.cpp +++ b/clients/gui/src/gui_main.cpp @@ -16,29 +16,23 @@ using namespace wininspect_gui; // Simple pipe transport for the GUI class PipeTransport : public ITransport { public: - std::string request(const std::string &json) override { - HANDLE h = CreateFileW(L"\.\pipe\wininspectd", GENERIC_READ | GENERIC_WRITE, - 0, nullptr, OPEN_EXISTING, 0, nullptr); - if (h == INVALID_HANDLE_VALUE) - return "{" ok ":false," error ":" no daemon "}"; - - uint32_t len = (uint32_t)json.size(); - DWORD w = 0; - WriteFile(h, &len, 4, &w, nullptr); - WriteFile(h, json.data(), len, &w, nullptr); - - uint32_t rlen = 0; - DWORD r = 0; - if (!ReadFile(h, &rlen, 4, &r, nullptr)) { - CloseHandle(h); - return "{" ok ":false}"; + std::string request(const std::string& json) override { + HANDLE h = CreateFileW(L"\\\\.\\pipe\\wininspectd", GENERIC_READ|GENERIC_WRITE, 0, nullptr, OPEN_EXISTING, 0, nullptr); + if (h == INVALID_HANDLE_VALUE) return "{\"ok\":false,\"error\":\"no daemon\"}"; + + uint32_t len = (uint32_t)json.size(); + DWORD w = 0; + WriteFile(h, &len, 4, &w, nullptr); + WriteFile(h, json.data(), len, &w, nullptr); + + uint32_t rlen = 0; + DWORD r = 0; + if (!ReadFile(h, &rlen, 4, &r, nullptr)) { CloseHandle(h); return "{\"ok\":false}"; } + std::string resp; resp.resize(rlen); + ReadFile(h, resp.data(), rlen, &r, nullptr); + CloseHandle(h); + return resp; } - std::string resp; - resp.resize(rlen); - ReadFile(h, resp.data(), rlen, &r, nullptr); - CloseHandle(h); - return resp; - } }; class WinInspectWindow { diff --git a/clients/portable/main.go b/clients/portable/main.go index 54fa63f..5f2f090 100644 --- a/clients/portable/main.go +++ b/clients/portable/main.go @@ -23,8 +23,8 @@ type Request struct { } type Response struct { - ID string `json:"id"` - OK bool `json:"ok"` + ID string `json:"id"` + OK bool `json:"ok"` Result json.RawMessage `json:"result,omitempty"` Error *ErrorDetail `json:"error,omitempty"` } @@ -107,14 +107,10 @@ func runCommand(addr, keyPath, method string, args []string) error { params := make(map[string]interface{}) if method == "info" || method == "children" { - if len(args) < 1 { - return fmt.Errorf("missing hwnd") - } + if len(args) < 1 { return fmt.Errorf("missing hwnd") } params["hwnd"] = args[0] } else if method == "pick" { - if len(args) < 2 { - return fmt.Errorf("missing x y") - } + if len(args) < 2 { return fmt.Errorf("missing x y") } params["x"] = args[0] params["y"] = args[1] } @@ -140,9 +136,7 @@ func runCommand(addr, keyPath, method string, args []string) error { func handshake(conn net.Conn, keyPath string) (*CryptoSession, error) { // Recv Hello helloData, err := recvRaw(conn) - if err != nil { - return nil, err - } + if err != nil { return nil, err } var hello map[string]interface{} json.Unmarshal(helloData, &hello) @@ -158,14 +152,10 @@ func handshake(conn net.Conn, keyPath string) (*CryptoSession, error) { "signature": "SSHSIG_STUB", } respData, _ := json.Marshal(resp) - if err := sendRaw(conn, respData); err != nil { - return nil, err - } + if err := sendRaw(conn, respData); err != nil { return nil, err } authStatus, err := recvRaw(conn) - if err != nil { - return nil, err - } + if err != nil { return nil, err } if !strings.Contains(string(authStatus), "\"ok\":true") { return nil, fmt.Errorf("auth failed") } @@ -187,9 +177,7 @@ func sendRaw(conn net.Conn, data []byte) error { func recvRaw(conn net.Conn) ([]byte, error) { lenBuf := make([]byte, 4) _, err := io.ReadFull(conn, lenBuf) - if err != nil { - return nil, err - } + if err != nil { return nil, err } length := binary.LittleEndian.Uint32(lenBuf) data := make([]byte, length) _, err = io.ReadFull(conn, data) @@ -197,23 +185,36 @@ func recvRaw(conn net.Conn) ([]byte, error) { } func sendEncrypted(conn net.Conn, s *CryptoSession, data []byte) error { - nonce := make([]byte, 12) // In real version, increment s.nonce - ciphertext := s.aesGCM.Seal(nil, nonce, data, nil) + nonce := make([]byte, 12) + binary.LittleEndian.PutUint64(nonce, s.nonce) + s.nonce++ - // Matches our C++ logic: [Nonce(12)][Tag(16)][Ciphertext(N)] - // Note: Seal returns [Ciphertext][Tag], we may need to reorder - return sendRaw(conn, ciphertext) + // Seal appends [Ciphertext][Tag] to dst (nil here, so new slice) + sealed := s.aesGCM.Seal(nil, nonce, data, nil) + + // C++ Logic: [Nonce(12)][Tag(16)][Ciphertext(N)] + // Sealed is: [Ciphertext(N)][Tag(16)] + tagSize := 16 + if len(sealed) < tagSize { + return fmt.Errorf("encryption error") + } + + realCipher := sealed[:len(sealed)-tagSize] + tag := sealed[len(sealed)-tagSize:] + + total := make([]byte, 12 + 16 + len(realCipher)) + copy(total[0:12], nonce) + copy(total[12:28], tag) + copy(total[28:], realCipher) + + return sendRaw(conn, total) } func recvEncrypted(conn net.Conn, s *CryptoSession) ([]byte, error) { data, err := recvRaw(conn) - if err != nil { - return nil, err - } + if err != nil { return nil, err } // In this scaffold, decrypt is just returning the slice past 28 bytes - if len(data) < 28 { - return nil, fmt.Errorf("malformed packet") - } + if len(data) < 28 { return nil, fmt.Errorf("malformed packet") } return data[28:], nil } diff --git a/core/include/wininspect/backend.hpp b/core/include/wininspect/backend.hpp index de15f8f..29a9a57 100644 --- a/core/include/wininspect/backend.hpp +++ b/core/include/wininspect/backend.hpp @@ -33,8 +33,7 @@ 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/crypto.cpp b/core/src/crypto.cpp index d80dccc..2d30237 100644 --- a/core/src/crypto.cpp +++ b/core/src/crypto.cpp @@ -1,11 +1,14 @@ #ifdef _WIN32 #define WIN32_LEAN_AND_MEAN #include +#ifndef BCRYPT_ECD_PUBLIC_GENERIC_MAGIC +#define BCRYPT_ECD_PUBLIC_GENERIC_MAGIC 0x50434345 +#endif +#include +#include +#include #include #include -#include -#include -#include #include #include "wininspect/crypto.hpp" @@ -74,24 +77,22 @@ bool CryptoSession::compute_shared_secret( return false; } - BCRYPT_BUFFER_DESC derDesc = {0}; - BCRYPT_BUFFER derBuffers[1] = {0}; - derDesc.cBuffers = 1; - derDesc.pBuffers = derBuffers; - derDesc.ulVersion = BCRYPTBUFFER_VERSION; - derBuffers[0].BufferType = KDF_HASH_ALGORITHM; - derBuffers[0].cbBuffer = - (ULONG)((wcslen(BCRYPT_SHA256_ALGORITHM) + 1) * sizeof(wchar_t)); - derBuffers[0].pvBuffer = (PVOID)BCRYPT_SHA256_ALGORITHM; - - uint8_t derived[32]; - ULONG cbDerived = 0; - if (BCryptDeriveKey(hSecret, BCRYPT_KDF_HASH, &derDesc, derived, 32, - &cbDerived, 0) != 0) { - BCryptDestroySecret(hSecret); - BCryptDestroyKey(hRemoteKey); - return false; - } + BCryptBufferDesc derDesc = { 0 }; + BCryptBuffer derBuffers[1] = { 0 }; + derDesc.cBuffers = 1; + derDesc.pBuffers = derBuffers; + derDesc.ulVersion = BCRYPTBUFFER_VERSION; + derBuffers[0].BufferType = KDF_HASH_ALGORITHM; + derBuffers[0].cbBuffer = (ULONG)((wcslen(BCRYPT_SHA256_ALGORITHM) + 1) * sizeof(wchar_t)); + derBuffers[0].pvBuffer = (PVOID)BCRYPT_SHA256_ALGORITHM; + + uint8_t derived[32]; + ULONG cbDerived = 0; + if (BCryptDeriveKey(hSecret, BCRYPT_KDF_HASH, &derDesc, derived, 32, &cbDerived, 0) != 0) { + BCryptDestroySecret(hSecret); + BCryptDestroyKey(hRemoteKey); + return false; + } BCryptDestroySecret(hSecret); BCryptDestroyKey(hRemoteKey); diff --git a/core/src/fake_backend.cpp b/core/src/fake_backend.cpp index 5e45979..f2b7eff 100644 --- a/core/src/fake_backend.cpp +++ b/core/src/fake_backend.cpp @@ -121,8 +121,7 @@ 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 4519543..826b4cd 100644 --- a/core/src/win32_backend.cpp +++ b/core/src/win32_backend.cpp @@ -2,12 +2,14 @@ #ifdef _WIN32 #define WIN32_LEAN_AND_MEAN -#include -#include #include #include +#include +#include #include #include +#include +#include namespace wininspect { @@ -30,8 +32,7 @@ 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); } @@ -192,174 +193,164 @@ 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; - } - - 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. - } + 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; + } - 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(); + 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 (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(); + 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(); } - } - 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(); + } + } + pChildren->Release(); + } + pRoot->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) { @@ -415,9 +406,7 @@ 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/daemon/src/server.cpp b/daemon/src/server.cpp index ed8f303..16253ba 100644 --- a/daemon/src/server.cpp +++ b/daemon/src/server.cpp @@ -27,6 +27,7 @@ struct ServerState { std::map snaps; std::list lru_order; // LRU: front is oldest, back is newest static constexpr size_t MAX_SNAPSHOTS = 1000; // Increased limit + bool auth_enabled = false; }; struct ClientSession { @@ -109,8 +110,8 @@ void handle_client(HANDLE hPipe, ServerState *st, IBackend *backend, std::lock_guard lk(st->mu); o["active_snapshots"] = (double)st->snaps.size(); o["max_snapshots"] = (double)ServerState::MAX_SNAPSHOTS; + o["auth_enabled"] = st->auth_enabled; } - o["auth_enabled"] = !auth_keys_u8.empty(); o["version"] = "1.0.0"; resp.ok = true; resp.result = o; @@ -198,8 +199,7 @@ void handle_client(HANDLE hPipe, ServerState *st, IBackend *backend, CloseHandle(hPipe); } -void run_server(std::atomic *running, ServerState *st, - IBackend *backend) { +void run_server(std::atomic* running, ServerState* st, IBackend* backend, bool read_only) { while (running->load()) { HANDLE hPipe = CreateNamedPipeW( PIPE_NAME, PIPE_ACCESS_DUPLEX, @@ -244,14 +244,7 @@ int wmain(int argc, wchar_t **argv) { } } - ServerState st; - Win32Backend backend; - std::atomic running{true}; - - std::thread server_thread(run_server, &running, &st, &backend, read_only); - - // Start TCP server for cross-environment access (Host <-> Guest, Host <-> - // Wine) + // Start TCP server for cross-environment access (Host <-> Guest, Host <-> Wine) std::string auth_keys_u8; if (!auth_keys.empty()) { int len = WideCharToMultiByte(CP_UTF8, 0, auth_keys.c_str(), @@ -262,6 +255,13 @@ int wmain(int argc, wchar_t **argv) { auth_keys_u8.data(), len, nullptr, nullptr); } + ServerState st; + st.auth_enabled = !auth_keys_u8.empty(); + Win32Backend backend; + std::atomic running{true}; + + std::thread server_thread(run_server, &running, &st, &backend, read_only); + std::thread([&, tcp_port, bind_public, auth_keys_u8, read_only]() { wininspectd::TcpServer tcp(tcp_port, &st, &backend); tcp.start(&running, bind_public, auth_keys_u8, read_only); diff --git a/daemon/src/tcp_server.cpp b/daemon/src/tcp_server.cpp index d9337d0..0d9cbea 100644 --- a/daemon/src/tcp_server.cpp +++ b/daemon/src/tcp_server.cpp @@ -1,258 +1,238 @@ #ifdef _WIN32 #define WIN32_LEAN_AND_MEAN -#include -#include -#include #include #include #include +#include +#include +#include +#include +#include +#include #include "tcp_server.hpp" #include "wininspect/core.hpp" #include "wininspect/crypto.hpp" +#pragma comment(lib, "Advapi32.lib") + namespace wininspectd { -static std::string base64_encode(const std::vector &in) { - static const char *b64 = - "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; - std::string out; - int val = 0, valb = -6; - for (uint8_t c : in) { - val = (val << 8) + c; - valb += 8; - while (valb >= 0) { - out.push_back(b64[(val >> valb) & 0x3F]); - valb -= 6; +static bool socket_read_all(SOCKET s, void* buf, uint32_t len) { + uint32_t n = 0; + char* p = (char*)buf; + while (n < len) { + int r = recv(s, p + n, (int)(len - n), 0); + if (r <= 0) return false; + n += r; } - } - if (valb > -6) - out.push_back(b64[((val << 8) >> (valb + 8)) & 0x3F]); - while (out.size() % 4) - out.push_back('='); - return out; + return true; } -static bool verify_identity(const std::string &auth_keys_path, - const std::string &identity, - const std::string &sig_b64, - const std::vector &nonce) { - std::ifstream f(auth_keys_path); - std::string line; - while (std::getline(f, line)) { - if (line.empty() || line[0] == '#') - continue; - if (line.find(identity) != std::string::npos) { - // In a production system, we'd extract the key from this line - return wininspect::crypto::verify_ssh_sig(nonce, sig_b64, line); +static bool socket_write_all(SOCKET s, const void* buf, uint32_t len) { + uint32_t n = 0; + const char* p = (const char*)buf; + while (n < len) { + int r = send(s, p + n, (int)(len - n), 0); + if (r <= 0) return false; + n += r; } - } - return false; + return true; } -static void handle_socket_client(SOCKET s, wininspect::ServerState *st, - wininspect::IBackend *backend, - std::string auth_keys, bool read_only) { - wininspect::CoreEngine core(backend); - - // Set 5 second timeout for handshake - DWORD timeout = 5000; - setsockopt(s, SOL_SOCKET, SO_RCVTIMEO, (const char *)&timeout, - sizeof(timeout)); - - if (!auth_keys.empty()) { - std::vector nonce(32); - HCRYPTPROV hProv; - if (CryptAcquireContext(&hProv, nullptr, nullptr, PROV_RSA_FULL, - CRYPT_VERIFYCONTEXT)) { - CryptGenRandom(hProv, (DWORD)nonce.size(), nonce.data()); - CryptReleaseContext(hProv, 0); +static std::string base64_encode(const std::vector& in) { + static const char* b64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + std::string out; + int val = 0, valb = -6; + for (uint8_t c : in) { + val = (val << 8) + c; + valb += 8; + while (valb >= 0) { + out.push_back(b64[(val >> valb) & 0x3F]); + valb -= 6; + } } + if (valb > -6) out.push_back(b64[((val << 8) >> (valb + 8)) & 0x3F]); + while (out.size() % 4) out.push_back('='); + return out; +} - wininspect::json::Object challenge; - challenge["type"] = "hello"; - challenge["version"] = std::string(wininspect::PROTOCOL_VERSION); - challenge["nonce"] = base64_encode(nonce); - std::string cj = wininspect::json::dumps(challenge); - uint32_t clen = (uint32_t)cj.size(); - if (!socket_write_all(s, &clen, 4) || - !socket_write_all(s, cj.data(), clen)) { - closesocket(s); - return; +static bool verify_identity(const std::string& auth_keys_path, const std::string& identity, const std::string& sig_b64, const std::vector& nonce) { + std::ifstream f(auth_keys_path); + std::string line; + while (std::getline(f, line)) { + if (line.empty() || line[0] == '#') continue; + if (line.find(identity) != std::string::npos) { + // In a production system, we'd extract the key from this line + return wininspect::crypto::verify_ssh_sig(nonce, sig_b64, line); + } } + return false; +} - uint32_t rlen = 0; - if (!socket_read_all(s, &rlen, 4)) { - closesocket(s); - return; - } - std::string resp_json; - resp_json.resize(rlen); - if (!socket_read_all(s, resp_json.data(), rlen)) { - closesocket(s); - return; - } +static void handle_socket_client(SOCKET s, wininspect::ServerState* st, wininspect::IBackend* backend, std::string auth_keys, bool read_only) { + wininspect::CoreEngine core(backend); - try { - auto v = wininspect::json::parse(resp_json).as_obj(); - if (v.at("version").as_str() != wininspect::PROTOCOL_VERSION) { - closesocket(s); - return; - } - if (!verify_identity(auth_keys, v.at("identity").as_str(), - v.at("signature").as_str(), nonce)) { - closesocket(s); - return; - } - } catch (...) { - closesocket(s); - return; - } + // Set 5 second timeout for handshake + DWORD timeout = 5000; + setsockopt(s, SOL_SOCKET, SO_RCVTIMEO, (const char*)&timeout, sizeof(timeout)); - wininspect::json::Object status; - status["type"] = "auth_status"; - status["ok"] = true; - std::string sj = wininspect::json::dumps(status); - uint32_t slen = (uint32_t)sj.size(); - if (!socket_write_all(s, &slen, 4) || - !socket_write_all(s, sj.data(), slen)) { - closesocket(s); - return; - } - } - - // Handshake successful, set a longer idle timeout (30 mins) - DWORD idle_timeout = 30 * 60 * 1000; - setsockopt(s, SOL_SOCKET, SO_RCVTIMEO, (const char *)&idle_timeout, - sizeof(idle_timeout)); - - while (true) { - uint32_t len = 0; - if (!socket_read_all(s, &len, 4)) - break; - - // Security: Prevent OOM/DoS by enforcing a reasonable maximum message size - // (10MB) - if (len == 0 || len > 10 * 1024 * 1024) - break; - - std::string json_req; - json_req.resize(len); - if (!socket_read_all(s, json_req.data(), len)) - break; - - wininspect::CoreResponse resp; - bool canonical = false; - - try { - auto req = wininspect::parse_request_json(json_req); - resp.id = req.id; - - // Security: Check Read-Only mode - if (read_only && - (req.method == "window.postMessage" || req.method == "input.send")) { - resp.ok = false; - resp.error_code = "E_ACCESS_DENIED"; - resp.error_message = "daemon is running in read-only mode"; - goto send_resp; - } - - auto itc = req.params.find("canonical"); - if (itc != req.params.end() && itc->second.is_bool()) - canonical = itc->second.as_bool(); - - if (req.method == "snapshot.capture") { - wininspect::Snapshot snap = backend->capture_snapshot(); - resp = core.handle(req, snap); - } else { - wininspect::Snapshot snap = backend->capture_snapshot(); - const wininspect::Snapshot *old_ptr = nullptr; - wininspect::Snapshot old_storage; - - auto itos = req.params.find("old_snapshot_id"); - if (itos != req.params.end() && itos->second.is_str()) { - // TCP server currently captures fresh for main, - // but we need to support old_snapshot if we want polling to work over - // TCP. For now, we'll just handle the handle call signature. + if (!auth_keys.empty()) { + std::vector nonce(32); + HCRYPTPROV hProv; + if (CryptAcquireContext(&hProv, nullptr, nullptr, PROV_RSA_FULL, CRYPT_VERIFYCONTEXT)) { + CryptGenRandom(hProv, (DWORD)nonce.size(), nonce.data()); + CryptReleaseContext(hProv, 0); } - resp = core.handle(req, snap, old_ptr); - } - } catch (...) { - resp.ok = false; - resp.error_code = "E_BAD_REQUEST"; + wininspect::json::Object challenge; + challenge["type"] = "hello"; + challenge["version"] = std::string(wininspect::PROTOCOL_VERSION); + challenge["nonce"] = base64_encode(nonce); + std::string cj = wininspect::json::dumps(challenge); + uint32_t clen = (uint32_t)cj.size(); + if (!socket_write_all(s, &clen, 4) || !socket_write_all(s, cj.data(), clen)) { closesocket(s); return; } + + uint32_t rlen = 0; + if (!socket_read_all(s, &rlen, 4)) { closesocket(s); return; } + std::string resp_json; resp_json.resize(rlen); + if (!socket_read_all(s, resp_json.data(), rlen)) { closesocket(s); return; } + + try { + auto v = wininspect::json::parse(resp_json).as_obj(); + if (v.at("version").as_str() != wininspect::PROTOCOL_VERSION) { + closesocket(s); return; + } + if (!verify_identity(auth_keys, v.at("identity").as_str(), v.at("signature").as_str(), nonce)) { + closesocket(s); return; + } + } catch (...) { closesocket(s); return; } + + wininspect::json::Object status; + status["type"] = "auth_status"; + status["ok"] = true; + std::string sj = wininspect::json::dumps(status); + uint32_t slen = (uint32_t)sj.size(); + if (!socket_write_all(s, &slen, 4) || !socket_write_all(s, sj.data(), slen)) { closesocket(s); return; } } - send_resp: - std::string out = wininspect::serialize_response_json(resp, canonical); - uint32_t out_len = (uint32_t)out.size(); - if (!socket_write_all(s, &out_len, 4)) - break; - if (!socket_write_all(s, out.data(), out_len)) - break; - } - closesocket(s); + // Handshake successful, set a longer idle timeout (30 mins) + DWORD idle_timeout = 30 * 60 * 1000; + setsockopt(s, SOL_SOCKET, SO_RCVTIMEO, (const char*)&idle_timeout, sizeof(idle_timeout)); + + while (true) { + uint32_t len = 0; + if (!socket_read_all(s, &len, 4)) break; + + // Security: Prevent OOM/DoS by enforcing a reasonable maximum message size (10MB) + if (len == 0 || len > 10 * 1024 * 1024) break; + + std::string json_req; + json_req.resize(len); + if (!socket_read_all(s, json_req.data(), len)) break; + + wininspect::CoreResponse resp; + bool canonical = false; + + try { + auto req = wininspect::parse_request_json(json_req); + resp.id = req.id; + + // Security: Check Read-Only mode + if (read_only && (req.method == "window.postMessage" || req.method == "input.send")) { + resp.ok = false; + resp.error_code = "E_ACCESS_DENIED"; + resp.error_message = "daemon is running in read-only mode"; + goto send_resp; + } + + auto itc = req.params.find("canonical"); + if (itc != req.params.end() && itc->second.is_bool()) canonical = itc->second.as_bool(); + + if (req.method == "snapshot.capture") { + wininspect::Snapshot snap = backend->capture_snapshot(); + resp = core.handle(req, snap); + } else { + wininspect::Snapshot snap = backend->capture_snapshot(); + const wininspect::Snapshot* old_ptr = nullptr; + wininspect::Snapshot old_storage; + + auto itos = req.params.find("old_snapshot_id"); + if (itos != req.params.end() && itos->second.is_str()) { + // TCP server currently captures fresh for main, + // but we need to support old_snapshot if we want polling to work over TCP. + // For now, we'll just handle the handle call signature. + } + + resp = core.handle(req, snap, old_ptr); + } + } catch (...) { + resp.ok = false; + resp.error_code = "E_BAD_REQUEST"; + } + + send_resp: + std::string out = wininspect::serialize_response_json(resp, canonical); + uint32_t out_len = (uint32_t)out.size(); + if (!socket_write_all(s, &out_len, 4)) break; + if (!socket_write_all(s, out.data(), out_len)) break; + } + closesocket(s); } -TcpServer::TcpServer(int port, wininspect::ServerState *state, - wininspect::IBackend *backend) +TcpServer::TcpServer(int port, wininspect::ServerState* state, wininspect::IBackend* backend) : port_(port), state_(state), backend_(backend) {} TcpServer::~TcpServer() {} -void TcpServer::start(std::atomic *running, bool bind_public, - const std::string &auth_keys, bool read_only) { - WSADATA wsaData; - if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) - return; +void TcpServer::start(std::atomic* running, bool bind_public, const std::string& auth_keys, bool read_only) { + WSADATA wsaData; + if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) return; - SOCKET listen_sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); - if (listen_sock == INVALID_SOCKET) - return; + SOCKET listen_sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); + if (listen_sock == INVALID_SOCKET) return; - sockaddr_in addr{}; - addr.sin_family = AF_INET; - // Security: Bind to localhost (127.0.0.1) by default. - // Only bind to all interfaces (0.0.0.0) if explicitly requested by --public. - addr.sin_addr.s_addr = htonl(bind_public ? INADDR_ANY : INADDR_LOOPBACK); - addr.sin_port = htons((u_short)port_); + sockaddr_in addr{}; + addr.sin_family = AF_INET; + // Security: Bind to localhost (127.0.0.1) by default. + // Only bind to all interfaces (0.0.0.0) if explicitly requested by --public. + addr.sin_addr.s_addr = htonl(bind_public ? INADDR_ANY : INADDR_LOOPBACK); + addr.sin_port = htons((u_short)port_); - if (bind(listen_sock, (sockaddr *)&addr, sizeof(addr)) == SOCKET_ERROR) { - closesocket(listen_sock); - return; - } + if (bind(listen_sock, (sockaddr*)&addr, sizeof(addr)) == SOCKET_ERROR) { + closesocket(listen_sock); + return; + } - if (listen(listen_sock, SOMAXCONN) == SOCKET_ERROR) { - closesocket(listen_sock); - return; - } - - // Set non-blocking to check 'running' flag - u_long mode = 1; - ioctlsocket(listen_sock, FIONBIO, &mode); - - while (running->load()) { - SOCKET client = accept(listen_sock, nullptr, nullptr); - if (client == INVALID_SOCKET) { - if (WSAGetLastError() == WSAEWOULDBLOCK) { - Sleep(100); - continue; - } - break; + if (listen(listen_sock, SOMAXCONN) == SOCKET_ERROR) { + closesocket(listen_sock); + return; } - // Set back to blocking for the handler thread - u_long m2 = 0; - ioctlsocket(client, FIONBIO, &m2); + // Set non-blocking to check 'running' flag + u_long mode = 1; + ioctlsocket(listen_sock, FIONBIO, &mode); + + while (running->load()) { + SOCKET client = accept(listen_sock, nullptr, nullptr); + if (client == INVALID_SOCKET) { + if (WSAGetLastError() == WSAEWOULDBLOCK) { + Sleep(100); + continue; + } + break; + } - std::thread(handle_socket_client, client, state_, backend_, auth_keys, - read_only) - .detach(); - } + // Set back to blocking for the handler thread + u_long m2 = 0; + ioctlsocket(client, FIONBIO, &m2); - closesocket(listen_sock); - WSACleanup(); + std::thread(handle_socket_client, client, state_, backend_, auth_keys, read_only).detach(); + } + + closesocket(listen_sock); + WSACleanup(); } } // namespace wininspectd diff --git a/daemon/src/tray.cpp b/daemon/src/tray.cpp index a18c4cf..6e17f02 100644 --- a/daemon/src/tray.cpp +++ b/daemon/src/tray.cpp @@ -5,111 +5,104 @@ namespace wininspectd { TrayManager::TrayManager(OnExitCallback onExit) : onExit_(onExit) {} -TrayManager::~TrayManager() { stop(); } +TrayManager::~TrayManager() { + stop(); +} bool TrayManager::init(HINSTANCE hInstance) { - hInst_ = hInstance; + hInst_ = hInstance; - WNDCLASSEXW wc = {sizeof(WNDCLASSEXW)}; - wc.lpfnWndProc = windowProc; - wc.hInstance = hInst_; - wc.lpszClassName = L"WinInspectTrayWindow"; + WNDCLASSEXW wc = { sizeof(WNDCLASSEXW) }; + wc.lpfnWndProc = windowProc; + wc.hInstance = hInst_; + wc.lpszClassName = L"WinInspectTrayWindow"; - if (!RegisterClassExW(&wc)) - return false; + if (!RegisterClassExW(&wc)) return false; - hwnd_ = CreateWindowExW(0, wc.lpszClassName, L"WinInspect Daemon Tray", 0, 0, - 0, 0, 0, HWND_MESSAGE, nullptr, hInst_, this); - if (!hwnd_) - return false; + hwnd_ = CreateWindowExW(0, wc.lpszClassName, L"WinInspect Daemon Tray", 0, 0, 0, 0, 0, HWND_MESSAGE, nullptr, hInst_, this); + if (!hwnd_) return false; - NOTIFYICONDATAW nid = {sizeof(NOTIFYICONDATAW)}; - nid.hWnd = hwnd_; - nid.uID = 1; - nid.uFlags = NIF_ICON | NIF_MESSAGE | NIF_TIP; - nid.uCallbackMessage = WM_TRAYICON; - nid.hIcon = LoadIcon(nullptr, IDI_APPLICATION); - wcscpy_s(nid.szTip, L"WinInspect Daemon"); + NOTIFYICONDATAW nid = { sizeof(NOTIFYICONDATAW) }; + nid.hWnd = hwnd_; + nid.uID = 1; + nid.uFlags = NIF_ICON | NIF_MESSAGE | NIF_TIP; + nid.uCallbackMessage = WM_TRAYICON; + nid.hIcon = LoadIcon(nullptr, IDI_APPLICATION); + wcscpy_s(nid.szTip, L"WinInspect Daemon"); - if (!Shell_NotifyIconW(NIM_ADD, &nid)) - return false; + if (!Shell_NotifyIconW(NIM_ADD, &nid)) return false; - return true; + return true; } void TrayManager::run() { - running_ = true; - MSG msg; - while (running_ && GetMessage(&msg, nullptr, 0, 0)) { - TranslateMessage(&msg); - DispatchMessage(&msg); - } + running_ = true; + MSG msg; + while (running_ && GetMessage(&msg, nullptr, 0, 0)) { + TranslateMessage(&msg); + DispatchMessage(&msg); + } } void TrayManager::stop() { - if (hwnd_) { - NOTIFYICONDATAW nid = {sizeof(NOTIFYICONDATAW)}; - nid.hWnd = hwnd_; - nid.uID = 1; - Shell_NotifyIconW(NIM_DELETE, &nid); - DestroyWindow(hwnd_); - hwnd_ = nullptr; - } - running_ = false; + if (hwnd_) { + NOTIFYICONDATAW nid = { sizeof(NOTIFYICONDATAW) }; + nid.hWnd = hwnd_; + nid.uID = 1; + Shell_NotifyIconW(NIM_DELETE, &nid); + DestroyWindow(hwnd_); + hwnd_ = nullptr; + } + running_ = false; } -LRESULT CALLBACK TrayManager::windowProc(HWND hwnd, UINT uMsg, WPARAM wParam, - LPARAM lParam) { - TrayManager *self = nullptr; - if (uMsg == WM_NCCREATE) { - CREATESTRUCT *cs = reinterpret_cast(lParam); - self = reinterpret_cast(cs->lpCreateParams); - SetWindowLongPtr(hwnd, GWLP_USERDATA, reinterpret_cast(self)); - } else { - self = - reinterpret_cast(GetWindowLongPtr(hwnd, GWLP_USERDATA)); - } - - if (self) { - if (uMsg == WM_TRAYICON) { - self->handleTrayMessage(lParam); - return 0; - } else if (uMsg == WM_COMMAND) { - if (LOWORD(wParam) == ID_TRAY_EXIT) { - if (self->onExit_) - self->onExit_(); - self->stop(); - } else if (LOWORD(wParam) == ID_TRAY_ABOUT) { - MessageBoxW(hwnd, L"WinInspect Daemon -Monitoring windows with style.", L"About", MB_OK | MB_ICONINFORMATION); - } - return 0; +LRESULT CALLBACK TrayManager::windowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) { + TrayManager* self = nullptr; + if (uMsg == WM_NCCREATE) { + CREATESTRUCT* cs = reinterpret_cast(lParam); + self = reinterpret_cast(cs->lpCreateParams); + SetWindowLongPtr(hwnd, GWLP_USERDATA, reinterpret_cast(self)); + } else { + self = reinterpret_cast(GetWindowLongPtr(hwnd, GWLP_USERDATA)); + } + + if (self) { + if (uMsg == WM_TRAYICON) { + self->handleTrayMessage(lParam); + return 0; + } else if (uMsg == WM_COMMAND) { + if (LOWORD(wParam) == ID_TRAY_EXIT) { + if (self->onExit_) self->onExit_(); + self->stop(); + } else if (LOWORD(wParam) == ID_TRAY_ABOUT) { + MessageBoxW(hwnd, L"WinInspect Daemon\nMonitoring windows with style.", L"About", MB_OK | MB_ICONINFORMATION); + } + return 0; + } } - } - return DefWindowProcW(hwnd, uMsg, wParam, lParam); + return DefWindowProcW(hwnd, uMsg, wParam, lParam); } void TrayManager::handleTrayMessage(LPARAM lParam) { - if (lParam == WM_RBUTTONUP || lParam == WM_LBUTTONUP) { - showContextMenu(); - } + if (lParam == WM_RBUTTONUP || lParam == WM_LBUTTONUP) { + showContextMenu(); + } } void TrayManager::showContextMenu() { - HMENU hMenu = CreatePopupMenu(); - if (hMenu) { - InsertMenuW(hMenu, 0, MF_BYPOSITION | MF_STRING, ID_TRAY_ABOUT, L"About"); - InsertMenuW(hMenu, 1, MF_BYPOSITION | MF_SEPARATOR, 0, nullptr); - InsertMenuW(hMenu, 2, MF_BYPOSITION | MF_STRING, ID_TRAY_EXIT, L"Exit"); - - POINT pt; - GetCursorPos(&pt); - SetForegroundWindow(hwnd_); - TrackPopupMenu(hMenu, TPM_BOTTOMALIGN | TPM_LEFTALIGN, pt.x, pt.y, 0, hwnd_, - nullptr); - DestroyMenu(hMenu); - } + HMENU hMenu = CreatePopupMenu(); + if (hMenu) { + InsertMenuW(hMenu, 0, MF_BYPOSITION | MF_STRING, ID_TRAY_ABOUT, L"About"); + InsertMenuW(hMenu, 1, MF_BYPOSITION | MF_SEPARATOR, 0, nullptr); + InsertMenuW(hMenu, 2, MF_BYPOSITION | MF_STRING, ID_TRAY_EXIT, L"Exit"); + + POINT pt; + GetCursorPos(&pt); + SetForegroundWindow(hwnd_); + TrackPopupMenu(hMenu, TPM_BOTTOMALIGN | TPM_LEFTALIGN, pt.x, pt.y, 0, hwnd_, nullptr); + DestroyMenu(hMenu); + } } } // namespace wininspectd diff --git a/tools/WineBotAppBuilder b/tools/WineBotAppBuilder deleted file mode 160000 index b2fd649..0000000 --- a/tools/WineBotAppBuilder +++ /dev/null @@ -1 +0,0 @@ -Subproject commit b2fd6499dbac93f28191a4610cc603e2887478e5 diff --git a/tools/WineBotAppBuilder/.keep b/tools/WineBotAppBuilder/.keep new file mode 100644 index 0000000..fb3d8d6 --- /dev/null +++ b/tools/WineBotAppBuilder/.keep @@ -0,0 +1 @@ +Placeholder for WBAB submodule. diff --git a/tools/ci-build.sh b/tools/ci-build.sh deleted file mode 100755 index 7d269c7..0000000 --- a/tools/ci-build.sh +++ /dev/null @@ -1,31 +0,0 @@ -#!/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 c05efa5..58de2b7 100755 --- a/tools/package-nsis.sh +++ b/tools/package-nsis.sh @@ -10,9 +10,7 @@ mkdir -p "${DIST_DIR}" echo "--- Packaging WinInspect Installer (Version: ${VERSION}) ---" if command -v makensis &> /dev/null; then - # 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" + makensis -DVERSION="${VERSION}" "${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 4b63647..e8c7fc6 100755 --- a/tools/wbab +++ b/tools/wbab @@ -12,18 +12,30 @@ wbab (WinInspect) Usage: tools/wbab [args] This repository is scaffolded to work with WineBotAppBuilder (WBAB). -If WBAB submodule is present (tools/WineBotAppBuilder), build/test/package -commands will run inside WBAB containers. +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. EOF } run_local_build() { - "${ROOT}/tools/winbuild-build.sh" "$@" + cmake -S "${ROOT}" -B "${ROOT}/build" -DWININSPECT_BUILD_TESTS=ON + cmake --build "${ROOT}/build" --config Release } run_local_test() { - "${ROOT}/scripts/wbab-test.sh" "$@" + 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 } case "${cmd}" in @@ -31,67 +43,20 @@ 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) - 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 + "${ROOT}/tools/winbuild-build.sh" "$@" ;; test) - if [[ -d "${WBAB_DIR}" ]]; then - 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 "$@" + if [[ -f "${WBAB_DIR}/tools/wbab" ]]; then + "${ROOT}/scripts/wbab-test.sh" "$@" else - run_local_test "$@" + run_local_test fi ;; package) - 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 + "${ROOT}/tools/package-nsis.sh" "$@" ;; sign) "${ROOT}/tools/sign-dev.sh" "$@" diff --git a/tools/wininspect.nsi b/tools/wininspect.nsi index 3c90082..d13f33d 100644 --- a/tools/wininspect.nsi +++ b/tools/wininspect.nsi @@ -4,10 +4,6 @@ !define VERSION "dev" !endif -!ifndef BUILD_SRC - !define BUILD_SRC "..\build\Release" -!endif - Name "WinInspect ${VERSION}" OutFile "..\\dist\\WinInspect-Installer.exe" InstallDir "$PROGRAMFILES64\\WinInspect" @@ -31,9 +27,9 @@ Section "WinInspect Core" SecCore SetOutPath "$INSTDIR" ; Binaries (assuming they are in build/Release/) - File "${BUILD_SRC}\wininspectd.exe" - File "${BUILD_SRC}\wininspect.exe" - File "${BUILD_SRC}\wininspect-gui.exe" + File "..\build\Release\wininspectd.exe" + File "..\build\Release\wininspect.exe" + File "..\build\Release\wininspect-gui.exe" ; License File "..\LICENSE"