From 84dfa0e477129c8c6abec2f582d5af60c1978482 Mon Sep 17 00:00:00 2001 From: mirkobrombin Date: Wed, 4 Nov 2026 17:01:05 +0000 Subject: [PATCH] feat: find and enumerate the windows of other processes --- native/CMakeLists.txt | 8 ++ native/desktop/foreign_surface.cpp | 36 +++++++- native/desktop/foreign_surface.h | 9 +- native/desktop/user_ext.cpp | 6 ++ native/desktop/window_long.cpp | 3 +- native/desktop/window_manager.cpp | 33 ++++++- native/desktop/windows.cpp | 29 ++++++- native/desktop/windows.h | 4 + testdata/native_loader/cross_window_client.c | 90 ++++++++++++++++++++ 9 files changed, 209 insertions(+), 9 deletions(-) create mode 100644 testdata/native_loader/cross_window_client.c diff --git a/native/CMakeLists.txt b/native/CMakeLists.txt index b8da9db..3606738 100644 --- a/native/CMakeLists.txt +++ b/native/CMakeLists.txt @@ -3363,6 +3363,14 @@ if(CMAKE_SYSTEM_PROCESSOR MATCHES "^(x86_64|amd64|AMD64)$") add_custom_target(dialog_font_fixture${width} ALL DEPENDS ${dialog_font_client}) add_test(NAME native_dialog_font_x${width} COMMAND raven --run-process --execution=native ${dialog_font_client}) set_tests_properties(native_dialog_font_x${width} PROPERTIES TIMEOUT 20 PASS_REGULAR_EXPRESSION "Native entry returned 0 ") + set(cross_window_client ${CMAKE_CURRENT_BINARY_DIR}/cross-window${width}.exe) + add_custom_command(OUTPUT ${cross_window_client} + COMMAND ${webservices_compiler} -nostdlib -O1 -fno-builtin -fno-stack-protector -Wl,--entry,${webservices_entry},--no-insert-timestamp + -o ${cross_window_client} ${LOADER_SOURCES}/cross_window_client.c -luser32 -lkernel32 + DEPENDS ${LOADER_SOURCES}/cross_window_client.c VERBATIM) + add_custom_target(cross_window_fixture${width} ALL DEPENDS ${cross_window_client}) + add_test(NAME native_cross_window_x${width} COMMAND raven --run-process --execution=native --timeout-ms=30000 ${cross_window_client}) + set_tests_properties(native_cross_window_x${width} PROPERTIES TIMEOUT 40 PASS_REGULAR_EXPRESSION "Native entry returned 0 ") # mlang comes with the installer components, whose import library the client links against. if(width EQUAL 64) set(mlang_dir ${RAVEN_INSTALLER_COMPONENT_DIR}) diff --git a/native/desktop/foreign_surface.cpp b/native/desktop/foreign_surface.cpp index 69041f7..60713cf 100644 --- a/native/desktop/foreign_surface.cpp +++ b/native/desktop/foreign_surface.cpp @@ -10,6 +10,8 @@ #include #include #include +#include +#include #include #include #include @@ -115,7 +117,8 @@ void publish_window_traits(uint64_t window, const ForeignWindowTraits &traits) const auto temporary = directory / (window_name(window) + ".traits." + std::to_string(::getpid())); { std::ofstream output(temporary, std::ios::trunc); - output << traits.style << ' ' << traits.ex_style << '\n' << traits.class_name; + output << traits.style << ' ' << traits.ex_style << ' ' << traits.parent << ' ' << (traits.visible ? 1 : 0) << '\n' << traits.class_name << '\n' + << traits.title; } std::error_code error; std::filesystem::rename(temporary, final_path, error); @@ -151,13 +154,40 @@ std::optional foreign_window_traits(uint64_t window) return std::nullopt; std::ifstream input(window_directory() / (window_name(window) + ".traits")); ForeignWindowTraits traits; - if (!(input >> traits.style >> traits.ex_style)) + std::string first; + if (!std::getline(input, first)) return std::nullopt; - input.ignore(1); + std::istringstream fields(first); + int visible = 0; + if (!(fields >> traits.style >> traits.ex_style >> traits.parent >> visible)) + return std::nullopt; + traits.visible = visible != 0; std::getline(input, traits.class_name); + // The title is the rest of the file, line breaks and all. + traits.title.assign(std::istreambuf_iterator(input), std::istreambuf_iterator()); return traits; } +std::vector foreign_windows() +{ + std::vector found; + std::error_code error; + for (const auto &entry : std::filesystem::directory_iterator(window_directory(), error)) { + const auto path = entry.path(); + if (path.extension() != ".window") + continue; + const auto stem = path.stem().string(); + char *end = nullptr; + const auto low = std::strtoull(stem.c_str(), &end, 16); + if (stem.size() != 8 || !end || *end) + continue; + // A handle keeps its process tag in the bits above the low word the name carries; window handles fit in those 32 bits. + if (foreign_record(low)) + found.push_back(low); + } + return found; +} + std::optional foreign_window_owner(uint64_t window) { const auto record = foreign_record(window); diff --git a/native/desktop/foreign_surface.h b/native/desktop/foreign_surface.h index d46dc49..117d3bd 100644 --- a/native/desktop/foreign_surface.h +++ b/native/desktop/foreign_surface.h @@ -6,6 +6,7 @@ #include #include #include +#include namespace raven { @@ -25,13 +26,19 @@ std::optional> foreign_window_size(uint64_t window // The process that owns a window another live process of the prefix created. std::optional foreign_window_owner(uint64_t window); -// What another process may ask of a window it does not own: its styles and the name of its class. +// What another process may ask of a window it does not own: its styles, its parent, whether it is shown, the name of its class and +// its title (both UTF-8). struct ForeignWindowTraits { uint32_t style = 0, ex_style = 0; std::string class_name; + uint64_t parent = 0; + bool visible = false; + std::string title; }; void publish_window_traits(uint64_t window, const ForeignWindowTraits &traits); std::optional foreign_window_traits(uint64_t window); +// The windows other live processes of the prefix own, in no particular order. +std::vector foreign_windows(); // Pixels of a window shared between the process that draws them and the owner: a header page, then 32-bit rows top down. class ForeignSurface { diff --git a/native/desktop/user_ext.cpp b/native/desktop/user_ext.cpp index 0d43cc0..c8841f2 100644 --- a/native/desktop/user_ext.cpp +++ b/native/desktop/user_ext.cpp @@ -983,6 +983,12 @@ void NativeWindows::register_ext_api(ApiRegistry ®istry) call.ret(0); return; } + if (const auto foreign = std::as_const(objects).window(call[0]) ? std::nullopt : foreign_window_traits(call[0])) { + const auto text = to_ansi(from_utf8(foreign->title), 1252); + call.put_narrow(call[1], text, size_t(call.i32(2))); + call.ret(std::min(text.size(), size_t(call.i32(2)) - 1)); + return; + } call.ret(send_ansi(call, call[0], 0x0d, call[2], call[1])); }); registry.add("TrackPopupMenu", 7, [=, this](ApiCall &call) { diff --git a/native/desktop/window_long.cpp b/native/desktop/window_long.cpp index a868648..098b8f5 100644 --- a/native/desktop/window_long.cpp +++ b/native/desktop/window_long.cpp @@ -187,8 +187,7 @@ void NativeWindows::register_long_api(ApiRegistry ®istry) std::memcpy(bytes.data() + 4, &next, 4); process.write_memory(scratch, bytes); send(call[0], 0x7d, index_word, scratch); - if (const auto *changed_window = std::as_const(objects).window(call[0])) - publish_window_traits(call[0], {changed_window->style, changed_window->ex_style, objects.window_class(changed_window->class_id)->name}); + publish_traits(call[0]); if (std::as_const(objects).window(call[0]) && objects.window_visible(call[0])) { invalidate_tree(call[0], true); drawing(call[0]); diff --git a/native/desktop/window_manager.cpp b/native/desktop/window_manager.cpp index 0a9a665..8ff06a5 100644 --- a/native/desktop/window_manager.cpp +++ b/native/desktop/window_manager.cpp @@ -146,6 +146,10 @@ bool NativeWindows::show_window(uint64_t handle, uint32_t command) window->visible = !hide; window->style = (window->style & ~ws_visible) | (window->visible ? ws_visible : 0); objects.update_window_dcs(window->parent ? window->parent : handle); + if (!window->parent) { + publish_traits(handle); + window = objects.window(handle); + } if (show) { // Showing a window paints its whole subtree, so the children's update regions are invalidated with it. if (!objects.invalidate(handle, {0, 0, window->width, window->height}, true)) @@ -353,6 +357,21 @@ void NativeWindows::place_window(uint64_t handle, DesktopRect outer, uint64_t in process.unmap_memory(scratch); } +uint64_t NativeWindows::foreign_top_level(const std::u16string &class_name, bool titled, const std::u16string &title) const +{ + for (const auto handle : foreign_windows()) { + const auto traits = foreign_window_traits(handle); + if (!traits || traits->parent) + continue; + if (!class_name.empty() && !equal_fold(from_utf8(traits->class_name), class_name)) + continue; + if (titled && from_utf8(traits->title) != title) + continue; + return handle; + } + return 0; +} + void NativeWindows::register_manager_api(ApiRegistry ®istry) { registry.add("FindWindowW", 2, [this](ApiCall &call) { @@ -371,6 +390,10 @@ void NativeWindows::register_manager_api(ApiRegistry ®istry) call.ret(*root); return; } + if (const auto found = foreign_top_level(name, call[1] != 0, title)) { + call.ret(found); + return; + } call.fail(1400); }); registry.add("FindWindowA", 2, [this](ApiCall &call) { @@ -390,6 +413,10 @@ void NativeWindows::register_manager_api(ApiRegistry ®istry) call.ret(*root); return; } + if (const auto found = foreign_top_level(name, call[1] != 0, title)) { + call.ret(found); + return; + } call.fail(1400); }); registry.add("FindWindowExW", 4, [this](ApiCall &call) { @@ -654,7 +681,7 @@ void NativeWindows::register_manager_api(ApiRegistry ®istry) registry.add("WindowFromPoint", 1, [this](ApiCall &call) { call.ret(window_at(int32_t(call[0]), int32_t(call[0] >> 32))); }); const auto enumerate = [this](ApiCall &call, const std::vector &handles, uint64_t procedure, uint64_t parameter) { for (const auto handle : handles) { - if (!std::as_const(objects).window(handle)) + if (!std::as_const(objects).window(handle) && !foreign_window_size(handle)) continue; const uint64_t arguments[] = {handle, parameter}; const auto result = invoke(procedure, arguments); @@ -668,6 +695,10 @@ void NativeWindows::register_manager_api(ApiRegistry ®istry) registry.add("EnumWindows", 2, [this, enumerate](ApiCall &call) { auto roots = top_level_windows(); std::reverse(roots.begin(), roots.end()); + // The top-level windows of the other processes of the prefix follow, as one desktop holds them all on Windows. + for (const auto handle : foreign_windows()) + if (const auto traits = foreign_window_traits(handle); traits && !traits->parent) + roots.push_back(handle); enumerate(call, roots, call[0], call[1]); }); registry.add("EnumThreadWindows", 3, [this, enumerate](ApiCall &call) { diff --git a/native/desktop/windows.cpp b/native/desktop/windows.cpp index a06576c..da7d70f 100644 --- a/native/desktop/windows.cpp +++ b/native/desktop/windows.cpp @@ -511,6 +511,15 @@ void NativeWindows::watch_input() }).detach(); } +void NativeWindows::publish_traits(uint64_t handle) +{ + const auto *window=std::as_const(objects).window(handle); + if (!window || window->destroying) return; + const auto *definition=objects.window_class(window->class_id); + publish_window_traits(handle,{window->style,window->ex_style,definition ? definition->name : std::string(),window->parent,window->visible, + to_utf8(window->title)}); +} + uint64_t NativeWindows::foreign_dc(uint64_t handle) { auto size=foreign_window_size(handle); @@ -1605,6 +1614,8 @@ uint64_t NativeWindows::default_message(std::span args) const auto address=message==0x81 ? (narrow ? read(process.read_memory(args[3],48),36,4) : read(process.read_memory(args[3],80),56,8)) : args[3]; if (!objects.set_title(args[0],window_text(address))) throw uint32_t(8); + // Other processes look top-level windows up by title; a control's text changing often stays out of the shared directory. + if (!window->parent) publish_traits(args[0]); if (objects.window_visible(args[0])) present(args[0]); return 1; } @@ -1914,7 +1925,7 @@ uint64_t NativeWindows::create(std::span args) if (child) parent_notify(handle,1); if (!std::as_const(objects).window(handle)) return 0; if (const auto *created=std::as_const(objects).window(handle)) publish_window(handle,uint32_t(std::max(created->width,0)),uint32_t(std::max(created->height,0))); - if (const auto *created=std::as_const(objects).window(handle)) publish_window_traits(handle,{created->style,created->ex_style,objects.window_class(created->class_id)->name}); + publish_traits(handle); if (effective_style & 0x10000000U) { auto *window=objects.window(handle);window->visible=true;objects.update_window_dcs(child ? window->parent : handle); if (objects.window_visible(handle)) present(handle); @@ -2648,6 +2659,19 @@ bool NativeWindows::dispatch(std::string_view name) process.write_memory(args[1],rect); result=1; } else if (name=="DefWindowProcW") result=default_message(args); + // The title of another process's window comes from what it published; its procedure is not asked. + else if (const auto foreign=(name=="GetWindowTextLengthW" || name=="GetWindowTextW") && !std::as_const(objects).window(args[0]) ? + foreign_window_traits(args[0]) : std::nullopt) { + const auto title=from_utf8(foreign->title); + if (name=="GetWindowTextLengthW") result=title.size(); + else if (int32_t(args[2])>0) { + const auto count=std::min(title.size(),size_t(uint32_t(args[2]))-1); + std::vector bytes((count+1)*2); + std::memcpy(bytes.data(),title.data(),count*2); + process.write_memory(args[1],bytes); + result=count; + } + } else if (name=="GetWindowTextLengthW") result=send(args[0],0x0e,0,0); else if (name=="SetWindowTextW") result=send(args[0],0x0c,0,args[1]); else if (name=="GetWindowTextW") { @@ -2833,7 +2857,8 @@ bool NativeWindows::dispatch(std::string_view name) if (withdraw_screen_dc(args[1])) publish_screen_dc(owner); } } else if (name=="IsWindowVisible") { - result=objects.window_visible(args[0]); + if (std::as_const(objects).window(args[0])) result=objects.window_visible(args[0]); + else if (const auto foreign=foreign_window_traits(args[0])) result=foreign->visible; } else if (name=="ShowWindow") { result=show_window(args[0],uint32_t(args[1])); } else if (name=="UpdateWindow") { diff --git a/native/desktop/windows.h b/native/desktop/windows.h index 8861b0a..8301373 100644 --- a/native/desktop/windows.h +++ b/native/desktop/windows.h @@ -185,6 +185,10 @@ private: struct ForeignTarget { std::shared_ptr surface; uint64_t bitmap=0; }; std::map foreign_targets; uint64_t foreign_dc(uint64_t window); + // Tells the other processes of the prefix what they may ask of a window: styles, parent, visibility, class and title. + void publish_traits(uint64_t window); + // A top-level window of another process of the prefix with this class (any when empty) and, when asked, this title; 0 when none. + uint64_t foreign_top_level(const std::u16string &class_name, bool titled, const std::u16string &title) const; std::map foreign_frames; // Windows that received a frame from another process with something other than black on it. std::set foreign_content; diff --git a/testdata/native_loader/cross_window_client.c b/testdata/native_loader/cross_window_client.c new file mode 100644 index 0000000..f4eb2fb --- /dev/null +++ b/testdata/native_loader/cross_window_client.c @@ -0,0 +1,90 @@ +// The windows of one process are visible to another of the same prefix: a child finds its parent's window by class and title, reads +// its title and visibility, sees it in EnumWindows and learns which process owns it. +#include + +static const wchar_t class_name[] = L"RavenCrossWindow"; +static const wchar_t title[] = L"Cross window probe é"; + +struct search { HWND wanted; int seen; }; + +static BOOL CALLBACK visit(HWND window, LPARAM parameter) +{ + struct search *search = (struct search *)parameter; + if (window == search->wanted) + search->seen = 1; + return TRUE; +} + +static int same(const wchar_t *left, const wchar_t *right) +{ + return lstrcmpW(left, right) == 0; +} + +static int child(DWORD parent) +{ + HWND window = FindWindowW(class_name, title); + if (!window) + return 10; + if (FindWindowW(class_name, NULL) != window || FindWindowW(NULL, title) != window || FindWindowA("RavenCrossWindow", NULL) != window) + return 11; + if (FindWindowW(class_name, L"Another title")) + return 12; + wchar_t text[64]; + if (GetWindowTextLengthW(window) != lstrlenW(title) || GetWindowTextW(window, text, 64) != lstrlenW(title) || !same(text, title)) + return 13; + if (GetWindowTextW(window, text, 6) != 5 || text[5] || text[0] != L'C') + return 14; + if (GetClassNameW(window, text, 64) != lstrlenW(class_name) || !same(text, class_name)) + return 15; + if (!IsWindow(window) || !IsWindowVisible(window)) + return 16; + DWORD owner = 0; + GetWindowThreadProcessId(window, &owner); + if (owner != parent) + return 17; + struct search search = {window, 0}; + if (!EnumWindows(visit, (LPARAM)&search) || !search.seen) + return 18; + return 0; +} + +int raven_entry(void) +{ + const wchar_t *line = GetCommandLineW(); + const wchar_t *marker = line; + for (; *marker; ++marker) + if (marker[0] == L'/' && marker[1] == L'c' && marker[2] == L':') { + DWORD parent = 0; + for (marker += 3; *marker >= L'0' && *marker <= L'9'; ++marker) + parent = parent * 10 + (DWORD)(*marker - L'0'); + return child(parent); + } + + WNDCLASSW definition = {0}; + definition.lpfnWndProc = DefWindowProcW; + definition.hInstance = GetModuleHandleW(NULL); + definition.lpszClassName = class_name; + if (!RegisterClassW(&definition)) + return 1; + HWND window = CreateWindowExW(0, class_name, title, WS_OVERLAPPEDWINDOW, 10, 10, 200, 120, NULL, NULL, definition.hInstance, NULL); + if (!window) + return 2; + ShowWindow(window, SW_SHOWNORMAL); + + wchar_t path[MAX_PATH], command[MAX_PATH + 64]; + if (!GetModuleFileNameW(NULL, path, MAX_PATH)) + return 3; + wsprintfW(command, L"\"%s\" /c:%lu", path, GetCurrentProcessId()); + STARTUPINFOW startup = {sizeof(startup)}; + PROCESS_INFORMATION information; + if (!CreateProcessW(path, command, NULL, NULL, FALSE, 0, NULL, NULL, &startup, &information)) + return 4; + if (WaitForSingleObject(information.hProcess, 20000) != WAIT_OBJECT_0) + return 5; + DWORD code = 6; + GetExitCodeProcess(information.hProcess, &code); + CloseHandle(information.hThread); + CloseHandle(information.hProcess); + DestroyWindow(window); + return (int)code; +}