Provide fresh Wine review snapshots and 32-bit Edit hooks
Expose visible Wine window text to the matching Go reader's independent keypad review commands, and extend standard Edit feedback to 32-bit programs such as MUSHclient without requiring 32-bit Linux GLib/Winelib libraries. Add lifetime-aware instance/window/control identities and bounded capture of standard Edit text, button/static captions, native ListBox/ListView items, and usable MSAA text. Preserve focus, caret, selection, scrolling, and the clipboard. Exclude hidden/password content before retrieval, retain both rune and UTF-16 offsets, clip visible ranges, and reject changing or oversized sources rather than returning incomplete text as a full snapshot. Export CaptureWindow on the version-1 WineReview D-Bus interface. Register with the current reader service owner and publish copied focus metadata. Authorize capture only for the current same-user owner. Use a dedicated GLib dispatcher and separate COM worker with a 500 ms deadline, one active job and one pending job, bounded payloads, and explicit stale/busy/limited/ unavailable replies. Keep menu and focus forwarding responsive when an MSAA provider stalls; retain ordinary feedback with older readers. Read the actual X11 geometry associated with Wine HWNDs using bounded XCB queries and __wine_x11_whole_window. Translate virtual-desktop coordinates and recheck physical geometry before returning captured text. Link XCB in the helper/tests and declare libxcb in the Arch package dependencies. Build edit_hook.c into the 32-bit DLL and attach both menu and input hooks to the foreground 32-bit UI thread. Release both hooks on transitions or attachment failure. Keep Scintilla-specific hooks confined to 64-bit builds. Expand Edit integration checks with read-only multiline navigation and unsuccessful deletion, plus 32-bit editor/Windows Forms lifecycle support. Add 32-bit/64-bit native capture fixtures, wire-protocol integration checks, and design/implementation documents. Document exact schema, capture limits, installation dependencies, compatibility, and live acceptance procedures. Validation during commit preparation: - git diff --check passed. - cmake --build build --parallel passed. - CTest could not start because xvfb-run is unavailable in this environment. - Further tests were skipped at the user's request; capture, protocol, and menu/editor integration tests were not run during commit preparation. Live speech and physical braille checks remain necessary, including review without moving the native caret, incoming output, plugin dialogs, and return to Linux/Fenrir. Custom painted output requires an accessible provider; RichEdit/Scintilla capture, semantic object review, Say All, typing echo, and native Wine braille routing remain unsupported by this protocol.
This commit is contained in:
1 parent
872388a59b
commit
a8cb350467
21 files changed
+2401
-38
No files matched your search
+47
-4
@@ -1,5 +1,32 @@
|
||||
# Wine control integration
|
||||
|
||||
## Wine flat-review capture
|
||||
|
||||
Build the helper and run `ctest --test-dir build --output-on-failure` on a
|
||||
private display. The review fixture covers both architectures: visible Edit
|
||||
text, Unicode offsets before the viewport, wrapping/blank lines, an empty
|
||||
focus anchor, hidden/password exclusion, password transitions before copying,
|
||||
clipping, same-length Edit/ListBox/ListView races, native ListView cells,
|
||||
negative coordinates, source limits and unchanged focus/selection.
|
||||
|
||||
Run `python3 tests/review_integration.py --build-dir build` and its `--32-bit`
|
||||
variant sequentially. These exercise the exact wire types, actual-owner
|
||||
authorization, deadline/busy behavior, blocked-provider focus feedback,
|
||||
shutdown and older-reader compatibility. The matching reader's
|
||||
`tests/wine_review_x11.py` drives physical XTest keycodes through its real
|
||||
command router, D-Bus registry and speech/braille queues. Run its Go test with
|
||||
`CTHULHU_WINE_REVIEW_BUILD=/absolute/build` and
|
||||
`go test ./internal/app -run NativeWineReviewPhysicalKeys -count=1 -v` on a
|
||||
private session/display; add `CTHULHU_WINE_REVIEW_32=1` for a 32-bit application.
|
||||
|
||||
Recorded output proves queued text and input consumption, not audible speech
|
||||
or physical braille. In MUSHclient, focus the plugin's output text window and
|
||||
check current/previous/next line, words, characters, spelling/phonetics and
|
||||
window boundaries. Confirm review does not move the native caret; then confirm
|
||||
ordinary arrows and newly arriving game output still work. Check plugin
|
||||
dialogs and return to Linux/Fenrir. Custom painted output needs the output
|
||||
plugin; this protocol supplies no native braille routing or Say All.
|
||||
|
||||
Build the project, then run `python3 tests/menu_integration.py --build-dir build`
|
||||
from the repository root in a graphical session. Dependencies are Wine,
|
||||
MinGW's x86_64 and i686 C compilers, PyGObject, and `dbus-run-session`.
|
||||
@@ -35,8 +62,8 @@ and that normal control reading resumes afterward. Test multiple Wine prefixes
|
||||
and reader shutdown separately. Automated event recording does not establish
|
||||
live listening or physical braille acceptance.
|
||||
|
||||
32-bit hook support, entirely custom menus without `WM_MENUSELECT`, and full
|
||||
object review are outside the current implementation.
|
||||
Entirely custom menus without `WM_MENUSELECT` and full object review are outside
|
||||
the current implementation.
|
||||
|
||||
## BARD Express managed menus
|
||||
|
||||
@@ -83,6 +110,10 @@ a private X display:
|
||||
python3 tests/menu_integration.py --build-dir build --fixture edit
|
||||
python3 tests/menu_integration.py --build-dir build --fixture edit --late-helper
|
||||
python3 tests/menu_integration.py --build-dir build --fixture edit --restart-helper
|
||||
python3 tests/menu_integration.py --build-dir build --fixture edit --32-bit
|
||||
python3 tests/menu_integration.py --build-dir build --fixture edit --32-bit --late-helper
|
||||
python3 tests/menu_integration.py --build-dir build --fixture edit --32-bit --restart-helper
|
||||
python3 tests/menu_integration.py --build-dir build --fixture winforms --32-bit
|
||||
python3 tests/menu_integration.py --build-dir build --fixture notepad
|
||||
```
|
||||
|
||||
@@ -90,7 +121,9 @@ The standard Edit fixture checks focus, characters/words/lines, incremental
|
||||
selection and unselection, Backspace/Delete/Clear, read-only controls, password
|
||||
exclusion, surrogate pairs, rapid repeated letters, menu return at the caret,
|
||||
short replacements, CRLF deletion, long-line truncation, Undo snapshots, and
|
||||
Ctrl+End line reading. Lifecycle variants attach after the control has focus or
|
||||
Ctrl+End line reading, and Up/Down navigation in a read-only multiline output
|
||||
control. Both 32-bit and 64-bit fixtures use the same assertions. Lifecycle
|
||||
variants attach after the control has focus or
|
||||
restart the helper without closing it. Every text action must have an established
|
||||
focus source, matching the Go reader's filtering contract.
|
||||
|
||||
@@ -100,6 +133,16 @@ checks resulting document text as well as recorded accessibility output. These
|
||||
tests use a recording D-Bus service; they do not run the full reader or prove
|
||||
audible speech, typing echo, physical keys, or a physical braille display.
|
||||
|
||||
For live MUSHclient acceptance, install the helper and reopen MUSHclient so the
|
||||
updated 32-bit DLL can load. In the command field, check Left/Right and
|
||||
Ctrl+Left/Right feedback without submitting a command. Use Ctrl+Tab to reach
|
||||
the read-only output notepad supplied by the Aardwolf review-buffer plugin;
|
||||
check Up/Down line reading and Left/Right characters there, then return to
|
||||
the command field. Check plugin menus and dialogs still work, and repeat after
|
||||
restarting the reader. These hooks provide caret feedback; independent review
|
||||
commands use the optional flat-review protocol described above. Native braille
|
||||
routing into Wine text remains unsupported.
|
||||
|
||||
The earlier 2026-10-10 helper fixtures and Go package tests did not establish
|
||||
that Go accepted editor `text` events. The reader follow-up adds that support,
|
||||
focused name/state handling, repeated-action and password/focus tests, and a
|
||||
@@ -222,7 +265,7 @@ synthetic accessibility events, button roles, return to the checked row,
|
||||
and empty-list state reset. The existing 32-bit plain-list regression also
|
||||
passed. These are private D-Bus recordings; audible GoldWave speech and
|
||||
physical braille remain a live acceptance check after installing the helper
|
||||
and restarting Cthulhu. The fix does not add 32-bit menu or editor hooks.
|
||||
and restarting Cthulhu. List queries are independent of the menu/editor hooks.
|
||||
|
||||
## Native tabs, trees and named clients
|
||||
|
||||
|
||||
@@ -119,6 +119,18 @@ static LRESULT CALLBACK window_proc(HWND window, UINT message, WPARAM wp, LPARAM
|
||||
case 30:
|
||||
key(VK_END, TRUE, FALSE); // whole restored line
|
||||
break;
|
||||
case 31:
|
||||
SetWindowTextW(editor, L"readonly first\r\nreadonly second");
|
||||
SendMessageW(editor, EM_SETSEL, 0, 0);
|
||||
SendMessageW(editor, EM_SETREADONLY, TRUE, 0);
|
||||
SetFocus(password); SetFocus(editor);
|
||||
break;
|
||||
case 32: key(VK_DOWN, FALSE, FALSE); break;
|
||||
case 33: key(VK_UP, FALSE, FALSE); break;
|
||||
case 34:
|
||||
key(VK_DELETE, FALSE, FALSE);
|
||||
if (GetWindowTextLengthW(editor) != 31) ExitProcess(2);
|
||||
break;
|
||||
default:
|
||||
DestroyWindow(window); break;
|
||||
}
|
||||
|
||||
@@ -24,8 +24,8 @@ parser.add_argument("--32-bit", dest="is32Bit", action="store_true")
|
||||
parser.add_argument("--late-helper", action="store_true")
|
||||
parser.add_argument("--restart-helper", action="store_true")
|
||||
args = parser.parse_args()
|
||||
if args.is32Bit and args.fixture not in ("menu", "list", "delphi", "controls"):
|
||||
parser.error("32-bit checks currently require --fixture menu, list, delphi or controls")
|
||||
if args.is32Bit and args.fixture not in ("menu", "edit", "winforms", "list", "delphi", "controls"):
|
||||
parser.error("32-bit checks currently require --fixture menu, edit, winforms, list, delphi or controls")
|
||||
if (args.late_helper or args.restart_helper) and args.fixture not in ("edit", "winforms", "scintilla"):
|
||||
parser.error("helper lifecycle checks require --fixture edit, winforms or scintilla")
|
||||
if args.fixture == "scintilla" and (not args.notepadpp_exe or not args.notepadpp_exe.is_file()):
|
||||
@@ -278,6 +278,9 @@ with tempfile.TemporaryDirectory(prefix="wine-access-menus-") as temporary:
|
||||
assert sum(e[2] == "new line deleted" and "kind=4" in e[5] for e in text_events) == 1, "CRLF Delete feedback missing"
|
||||
assert any(e[4] == "x" * 1022 for e in text_events), "truncated surrogate discarded line"
|
||||
assert "d" in spoken and "undo restored" in spoken, "Undo left stale text snapshot"
|
||||
for line in ("readonly first", "readonly second"):
|
||||
assert any(e[1] == "text" and e[2] == line for e in text_events), f"read-only arrow feedback missing: {line}"
|
||||
assert "r deleted" not in spoken, "read-only document reported an unsuccessful deletion"
|
||||
if args.fixture == "winforms":
|
||||
assert any(e[2] == "Email" and e[3] == "edit" and e[4] == "alpha beta"
|
||||
for e in text_events), "managed email label/focus missing"
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
// SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
#include <windows.h>
|
||||
#include <oleacc.h>
|
||||
#include <cstdio>
|
||||
#include <algorithm>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#if __has_include("../review_capture.h")
|
||||
#include "../review_capture.h"
|
||||
#define HAS_CAPTURE 1
|
||||
#endif
|
||||
|
||||
static bool check(bool value, const char *message) {
|
||||
if (!value) std::fprintf(stderr, "FAIL: %s\n", message);
|
||||
return value;
|
||||
}
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
if (argc != 3 || std::string(argv[1]) != "--fixture") return 2;
|
||||
std::string dos = argv[2];
|
||||
if (dos.starts_with('/')) dos = "Z:" + dos;
|
||||
std::replace(dos.begin(), dos.end(), '/', '\\');
|
||||
std::string quoted = "\"" + dos + "\"";
|
||||
int units = MultiByteToWideChar(CP_UTF8, 0, quoted.c_str(), -1, nullptr, 0);
|
||||
std::vector<WCHAR> command(units);
|
||||
MultiByteToWideChar(CP_UTF8, 0, quoted.c_str(), -1, command.data(), units);
|
||||
STARTUPINFOW startup{}; startup.cb = sizeof(startup);
|
||||
PROCESS_INFORMATION process{};
|
||||
if (!CreateProcessW(nullptr, command.data(), nullptr, nullptr, FALSE, 0,
|
||||
nullptr, nullptr, &startup, &process)) {
|
||||
std::fprintf(stderr, "FAIL: fixture starts (%s, Windows error %lu)\n", argv[2], static_cast<unsigned long>(GetLastError()));
|
||||
return 2;
|
||||
}
|
||||
HWND window = nullptr;
|
||||
ULONGLONG deadline = GetTickCount64() + 10000;
|
||||
while (!(window = FindWindowW(L"WineAccessReviewFixture", nullptr)) && GetTickCount64() < deadline) Sleep(20);
|
||||
bool ok = check(window != nullptr, "fixture becomes ready");
|
||||
if (window) {
|
||||
Sleep(100);
|
||||
#ifndef HAS_CAPTURE
|
||||
ok = check(false, "review capture unavailable");
|
||||
#else
|
||||
CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED);
|
||||
std::fprintf(stderr, "capture test: COM ready\n");
|
||||
ReviewIdentityStore store;
|
||||
auto capture = [&]() {
|
||||
auto focus = store.observe_foreground();
|
||||
std::fprintf(stderr, "capture test: focus %s\n", focus.focus.c_str());
|
||||
return capture_review_window(store, focus, GetTickCount64() + 500);
|
||||
};
|
||||
auto joined = [](const ReviewCapture &data) {
|
||||
std::string all;
|
||||
for (const auto &node : data.nodes) {
|
||||
all += node.name;
|
||||
for (const auto &range : node.ranges) all += range.text + "\n";
|
||||
}
|
||||
return all;
|
||||
};
|
||||
auto data = capture();
|
||||
std::fprintf(stderr, "capture test: initial %s\n", data.status.c_str());
|
||||
ok &= check(data.status == "ok", "initial capture succeeds");
|
||||
const auto text = joined(data);
|
||||
ok &= check(text.find("alpha beta") != std::string::npos, "visible Edit text");
|
||||
ok &= check(text.find("output second") != std::string::npos, "read-only output text");
|
||||
ok &= check(text.find("Open & close") != std::string::npos, "caption mnemonics and literal ampersand");
|
||||
ok &= check(text.find("native & item") != std::string::npos, "native MSAA list items retain literal ampersands");
|
||||
ok &= check(text.find("private-review-secret") == std::string::npos && text.find("hidden-review-secret") == std::string::npos,
|
||||
"password and hidden contents excluded");
|
||||
ok &= check(SendMessageW(window, WM_APP + 11, 0, 0), "review preserves focus and selection");
|
||||
for (int mode : {1, 2, 3, 4, 5, 6, 7, 8, 9, 14, 15, 17, 18, 21, 22, 23, 24, 25}) {
|
||||
std::fprintf(stderr, "capture test: mode %d\n", mode);
|
||||
SendMessageW(window, WM_APP + 10, mode, 0);
|
||||
auto result = capture();
|
||||
const auto value = joined(result);
|
||||
if (mode == 7) ok &= check(result.status == "limited" && result.nodes.empty(), "source limit rejects partial capture");
|
||||
else if (mode == 9) ok &= check(result.status == "stale" && result.nodes.empty(), "same-length update during capture rejects mixed data");
|
||||
else if (mode == 14 || mode == 15 || mode == 18 || mode == 23) ok &= check(result.status == "stale" && result.nodes.empty(), "protection/list/viewport transition rejects old capture");
|
||||
else if (mode == 21) ok &= check(result.status != "ok" && result.nodes.empty(), "delayed native call respects capture deadline");
|
||||
else ok &= check(result.status == "ok", "configured capture succeeds");
|
||||
ok &= check(value.find("private-review-secret") == std::string::npos && value.find("hidden-review-secret") == std::string::npos,
|
||||
"all captures exclude protected/hidden text");
|
||||
if (mode == 1) {
|
||||
bool found = false;
|
||||
for (const auto &node : result.nodes) for (const auto &range : node.ranges)
|
||||
if (range.text == "beta") {
|
||||
found = range.start == 8 && range.end == 12 && range.utf16_start == 9 && range.utf16_end == 13;
|
||||
}
|
||||
ok &= check(found, "surrogate before viewport retains absolute rune offsets");
|
||||
}
|
||||
if (mode == 2) {
|
||||
int wrapped = 0; bool blank = false;
|
||||
for (const auto &node : result.nodes) for (const auto &range : node.ranges) {
|
||||
if (range.text.find("wrapped") != std::string::npos) ++wrapped;
|
||||
if (range.text == "\n") blank = true;
|
||||
}
|
||||
ok &= check(wrapped > 1 && blank, "visual wraps and blank lines");
|
||||
}
|
||||
if (mode == 3) ok &= check(value.find("output second") != std::string::npos && !result.source.focus.empty(), "empty command remains review anchor");
|
||||
if (mode == 5) ok &= check(value.find("alpha beta") == std::string::npos, "partly clipped text is excluded");
|
||||
if (mode == 6) ok &= check(value.find("omega zeta") != std::string::npos && value.find("alpha beta") == std::string::npos, "same-length replacement refreshes text");
|
||||
if (mode == 8) ok &= check(result.source.bounds.left < 0, "negative screen coordinates remain valid");
|
||||
if (mode == 14) ok &= check(SendMessageW(window,WM_APP + 14,0,0), "password transition is excluded before copying text");
|
||||
if (mode == 17) ok &= check(value.find("table & item") != std::string::npos && value.find("ready") != std::string::npos, "visible native ListView cells retain text and ampersands");
|
||||
if (mode == 25) ok &= check(value.find("literal & label") != std::string::npos, "SS_NOPREFIX captions retain literal ampersands");
|
||||
if (mode == 21) {
|
||||
Sleep(750);
|
||||
ok &= check(SendMessageW(window,WM_APP + 15,0,0), "remote buffer lives until the delayed target returns");
|
||||
}
|
||||
if (mode == 22 || mode == 24) {
|
||||
bool visible=false;
|
||||
auto field=store.identity(GetDlgItem(window,mode == 22 ? 109 : 110));
|
||||
for (const auto &node:result.nodes) if (node.id == field) for (const auto &range:node.ranges)
|
||||
if (range.utf16_start > 0 && !range.text.empty()) visible=true;
|
||||
if (!visible) {
|
||||
HWND control=GetDlgItem(window,mode == 22 ? 109 : 110);
|
||||
std::fprintf(stderr,"scroll diagnostic first=%lld scroll=%d lines=%lld style=%llx\n",
|
||||
static_cast<long long>(SendMessageW(control,EM_GETFIRSTVISIBLELINE,0,0)),GetScrollPos(control,SB_HORZ),
|
||||
static_cast<long long>(SendMessageW(control,EM_GETLINECOUNT,0,0)),static_cast<unsigned long long>(GetWindowLongPtrW(control,GWL_STYLE)));
|
||||
RECT format{}; SendMessageW(control,EM_GETRECT,0,reinterpret_cast<LPARAM>(&format));
|
||||
auto length=SendMessageW(control,WM_GETTEXTLENGTH,0,0);
|
||||
std::fprintf(stderr,"scroll length=%lld positions=%llx/%llx format=%d,%d,%d,%d\n",static_cast<long long>(length),
|
||||
static_cast<unsigned long long>(SendMessageW(control,EM_POSFROMCHAR,0,0)),static_cast<unsigned long long>(SendMessageW(control,EM_POSFROMCHAR,length-1,0)),
|
||||
format.left,format.top,format.right,format.bottom);
|
||||
for (const auto &node:result.nodes) if (node.id == field) for (const auto &range:node.ranges)
|
||||
std::fprintf(stderr,"scroll range %llu %llu %s\n",static_cast<unsigned long long>(range.utf16_start),static_cast<unsigned long long>(range.utf16_end),range.text.c_str());
|
||||
}
|
||||
ok &= check(visible, "horizontally scrolled Edit retains visible text and absolute offsets");
|
||||
}
|
||||
if (mode != 4) ok &= check(SendMessageW(window, WM_APP + 11, 0, 0), "capture leaves caret and focus unchanged");
|
||||
}
|
||||
CoUninitialize();
|
||||
#endif
|
||||
PostMessageW(window, WM_CLOSE, 0, 0);
|
||||
}
|
||||
if (WaitForSingleObject(process.hProcess, 3000) != WAIT_OBJECT_0) TerminateProcess(process.hProcess, 2);
|
||||
CloseHandle(process.hThread); CloseHandle(process.hProcess);
|
||||
if (ok) std::puts("PASS: native visible Wine review capture and independent caret");
|
||||
return ok ? 0 : 1;
|
||||
}
|
||||
@@ -0,0 +1,254 @@
|
||||
// SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
#include <windows.h>
|
||||
#include <commctrl.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#define REVIEW_CONFIG (WM_APP + 10)
|
||||
#define REVIEW_CHECK (WM_APP + 11)
|
||||
#define REVIEW_CHECK_RIGHT (WM_APP + 12)
|
||||
#define REVIEW_CHECK_STRICT (WM_APP + 13)
|
||||
#define REVIEW_PRIVACY_CHECK (WM_APP + 14)
|
||||
#define REVIEW_REMOTE_CHECK (WM_APP + 15)
|
||||
static HWND editor, output, password, hidden;
|
||||
static WCHAR *large;
|
||||
static WNDPROC original_edit;
|
||||
static BOOL replace_during_read;
|
||||
static BOOL stall_accessible;
|
||||
static BOOL secondary;
|
||||
static unsigned int key_down, key_up;
|
||||
static const WCHAR *expected_text = L"alpha beta\r\nsecond line";
|
||||
static BOOL protect_during_length, replace_list_during_read;
|
||||
static unsigned int protected_text_reads;
|
||||
static WNDPROC original_list;
|
||||
static WNDPROC original_table;
|
||||
static BOOL replace_table_during_read;
|
||||
static BOOL delay_table_read, freed_table_memory, scroll_during_position;
|
||||
static LRESULT CALLBACK table_proc(HWND window, UINT message, WPARAM wp, LPARAM lp) {
|
||||
if (message == LVM_GETITEMTEXTW && delay_table_read) {
|
||||
delay_table_read=FALSE;
|
||||
Sleep(650);
|
||||
MEMORY_BASIC_INFORMATION memory={0};
|
||||
if (!VirtualQuery((void*)lp,&memory,sizeof(memory)) || memory.State != MEM_COMMIT) {
|
||||
freed_table_memory=TRUE;
|
||||
return 0; // Observe the unsafe lifetime without dereferencing freed data.
|
||||
}
|
||||
}
|
||||
LRESULT result=CallWindowProcW(original_table,window,message,wp,lp);
|
||||
if (message == LVM_GETITEMTEXTW && replace_table_during_read) {
|
||||
replace_table_during_read=FALSE;
|
||||
LVITEMW item={0}; item.pszText=L"other & item";
|
||||
SendMessageW(window,LVM_SETITEMTEXTW,0,(LPARAM)&item);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
static LRESULT CALLBACK list_proc(HWND window, UINT message, WPARAM wp, LPARAM lp) {
|
||||
LRESULT result = CallWindowProcW(original_list,window,message,wp,lp);
|
||||
if (message == LB_GETTEXT && replace_list_during_read) {
|
||||
replace_list_during_read = FALSE;
|
||||
SendMessageW(window,LB_DELETESTRING,0,0);
|
||||
SendMessageW(window,LB_INSERTSTRING,0,(LPARAM)L"update & item");
|
||||
}
|
||||
return result;
|
||||
}
|
||||
static LRESULT CALLBACK stall_proc(HWND window, UINT message, WPARAM wp, LPARAM lp) {
|
||||
if (message == WM_GETOBJECT && stall_accessible) {
|
||||
stall_accessible = FALSE;
|
||||
Sleep(2000);
|
||||
}
|
||||
return DefWindowProcW(window,message,wp,lp);
|
||||
}
|
||||
static LRESULT CALLBACK edit_proc(HWND window, UINT message, WPARAM wp, LPARAM lp) {
|
||||
if (message == WM_GETTEXT && (GetWindowLongPtrW(window,GWL_STYLE) & ES_PASSWORD)) ++protected_text_reads;
|
||||
if (message == WM_KEYDOWN) ++key_down;
|
||||
if (message == WM_KEYUP) ++key_up;
|
||||
LRESULT result = CallWindowProcW(original_edit, window, message, wp, lp);
|
||||
if (message == WM_GETTEXTLENGTH && protect_during_length) {
|
||||
protect_during_length = FALSE;
|
||||
SendMessageW(window,EM_SETPASSWORDCHAR,L'*',0);
|
||||
}
|
||||
if (message == WM_GETTEXT && replace_during_read) {
|
||||
replace_during_read = FALSE;
|
||||
SetWindowTextW(window, L"omega zeta\r\nsecond line");
|
||||
}
|
||||
if (message == EM_POSFROMCHAR && scroll_during_position) {
|
||||
scroll_during_position=FALSE;
|
||||
SendMessageW(window,EM_LINESCROLL,0,1);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
static LRESULT CALLBACK window_proc(HWND window, UINT message, WPARAM wp, LPARAM lp) {
|
||||
(void)lp;
|
||||
if (message == WM_TIMER && secondary) {
|
||||
KillTimer(window,1);
|
||||
SetForegroundWindow(window);
|
||||
SetFocus(GetDlgItem(window,105));
|
||||
return 0;
|
||||
}
|
||||
if (message == REVIEW_CONFIG) {
|
||||
SendMessageW(editor,EM_SETPASSWORDCHAR,0,0);
|
||||
protect_during_length = replace_list_during_read = FALSE;
|
||||
replace_table_during_read=FALSE;
|
||||
delay_table_read=freed_table_memory=scroll_during_position=FALSE;
|
||||
protected_text_reads = 0;
|
||||
key_down = key_up = 0;
|
||||
expected_text = L"alpha beta\r\nsecond line";
|
||||
ShowWindow(GetDlgItem(window,106),SW_SHOW);
|
||||
ShowWindow(GetDlgItem(window,107),SW_HIDE);
|
||||
ShowWindow(GetDlgItem(window,109),SW_HIDE);
|
||||
ShowWindow(GetDlgItem(window,110),SW_HIDE);
|
||||
ShowWindow(GetDlgItem(window,111),SW_HIDE);
|
||||
SetWindowTextW(editor, L"alpha beta\r\nsecond line");
|
||||
SetWindowTextW(output, L"output first\r\noutput second");
|
||||
SendMessageW(editor, EM_SETREADONLY, FALSE, 0);
|
||||
SendMessageW(editor, EM_SETSEL, 0, 0);
|
||||
MoveWindow(editor, 20, 35, 320, 90, TRUE);
|
||||
switch (wp) {
|
||||
case 1: // surrogate before first displayed row, absolute offsets
|
||||
SetWindowTextW(editor, L"\U0001f600alpha\r\nbeta\r\nlast");
|
||||
SendMessageW(editor, EM_LINESCROLL, 0, 1);
|
||||
break;
|
||||
case 2:
|
||||
SetWindowTextW(editor, L"wrapped words repeated wrapped words repeated wrapped words repeated wrapped words\r\n\r\nlast");
|
||||
MoveWindow(editor, 20, 35, 150, 150, TRUE);
|
||||
break;
|
||||
case 3: SetWindowTextW(editor, L""); break;
|
||||
case 4:
|
||||
SetForegroundWindow(window);
|
||||
SetFocus(password);
|
||||
return 1;
|
||||
case 5:
|
||||
MoveWindow(editor, 500, 35, 320, 90, TRUE);
|
||||
break;
|
||||
case 6: expected_text = L"omega zeta\r\nsecond line"; SetWindowTextW(editor, expected_text); break;
|
||||
case 7:
|
||||
large = malloc(1000002 * sizeof(WCHAR));
|
||||
if (!large) ExitProcess(3);
|
||||
// Avoid quadratic native wrapping work in the fixture itself.
|
||||
for (int i = 0; i < 1000001; ++i) large[i] = i % 32 == 31 ? L'\n' : L'x';
|
||||
large[1000001] = 0;
|
||||
SendMessageW(editor, EM_LIMITTEXT, 2000000, 0);
|
||||
SetWindowTextW(editor, large);
|
||||
free(large); large = NULL;
|
||||
break;
|
||||
case 8: SetWindowPos(window, NULL, -20, -20, 0, 0, SWP_NOSIZE | SWP_NOZORDER); break;
|
||||
case 9: replace_during_read = TRUE; break;
|
||||
case 10: stall_accessible = TRUE; break;
|
||||
case 11:
|
||||
SetForegroundWindow(window);
|
||||
SetFocus(output);
|
||||
return 1;
|
||||
case 12: ShowWindow(GetDlgItem(window,106),SW_HIDE); break;
|
||||
case 14: protect_during_length = TRUE; break;
|
||||
case 15: replace_list_during_read = TRUE; break;
|
||||
case 17: ShowWindow(GetDlgItem(window,107),SW_SHOW); break;
|
||||
case 18: ShowWindow(GetDlgItem(window,107),SW_SHOW); replace_table_during_read=TRUE; break;
|
||||
case 21: ShowWindow(GetDlgItem(window,107),SW_SHOW); delay_table_read=TRUE; break;
|
||||
case 22:
|
||||
ShowWindow(GetDlgItem(window,109),SW_SHOW);
|
||||
SendMessageW(GetDlgItem(window,109),EM_SETSEL,INT_MAX,INT_MAX);
|
||||
SendMessageW(GetDlgItem(window,109),EM_SCROLLCARET,0,0);
|
||||
break;
|
||||
case 23:
|
||||
SetWindowTextW(editor,L"first\r\nsecond\r\nthird\r\nfourth\r\nfifth\r\nsixth\r\nseventh\r\neighth\r\nninth\r\ntenth");
|
||||
scroll_during_position=TRUE;
|
||||
break;
|
||||
case 24:
|
||||
ShowWindow(GetDlgItem(window,110),SW_SHOW);
|
||||
SendMessageW(GetDlgItem(window,110),EM_SETSEL,INT_MAX,INT_MAX);
|
||||
SendMessageW(GetDlgItem(window,110),EM_SCROLLCARET,0,0);
|
||||
break;
|
||||
case 25: ShowWindow(GetDlgItem(window,111),SW_SHOW); break;
|
||||
}
|
||||
SetForegroundWindow(window);
|
||||
SetFocus(editor);
|
||||
return 1;
|
||||
}
|
||||
if (message == REVIEW_PRIVACY_CHECK) return protected_text_reads == 0;
|
||||
if (message == REVIEW_REMOTE_CHECK) return !freed_table_memory;
|
||||
if (message == REVIEW_CHECK || message == REVIEW_CHECK_RIGHT || message == REVIEW_CHECK_STRICT) {
|
||||
DWORD start = 0, end = 0;
|
||||
SendMessageW(editor, EM_GETSEL, (WPARAM)&start, (LPARAM)&end);
|
||||
if (message == REVIEW_CHECK) return start == 0 && end == 0 && GetFocus() == editor;
|
||||
WCHAR text[128], output_text[128];
|
||||
GetWindowTextW(editor,text,128); GetWindowTextW(output,output_text,128);
|
||||
if (message == REVIEW_CHECK_RIGHT)
|
||||
return start == 1 && end == 1 && key_down == 1 && key_up == 1 && GetFocus() == editor && lstrcmpW(text,expected_text) == 0;
|
||||
return start == 0 && end == 0 && GetFocus() == editor && key_down == 0 && key_up == 0 &&
|
||||
lstrcmpW(text,expected_text) == 0 && lstrcmpW(output_text,L"output first\r\noutput second") == 0;
|
||||
}
|
||||
if (message == WM_CLOSE) { DestroyWindow(window); return 0; }
|
||||
if (message == WM_DESTROY) { PostQuitMessage(0); return 0; }
|
||||
return DefWindowProcW(window, message, wp, lp);
|
||||
}
|
||||
|
||||
int WINAPI WinMain(HINSTANCE instance, HINSTANCE previous, LPSTR args, int show) {
|
||||
(void)previous; (void)show;
|
||||
secondary=strcmp(args,"--secondary")==0;
|
||||
if (strncmp(args,"--configure ",12)==0 || strcmp(args,"--check")==0 || strcmp(args,"--check-strict")==0 || strcmp(args,"--check-right")==0 || strcmp(args,"--check-remote")==0 || strcmp(args,"--close")==0) {
|
||||
HWND target=FindWindowW(L"WineAccessReviewFixture",NULL);
|
||||
if (!target) return 3;
|
||||
if (strcmp(args,"--close")==0) { PostMessageW(target,WM_CLOSE,0,0); return 0; }
|
||||
DWORD_PTR result=0;
|
||||
UINT message=strcmp(args,"--check")==0 ? REVIEW_CHECK : strcmp(args,"--check-strict")==0 ? REVIEW_CHECK_STRICT : strcmp(args,"--check-right")==0 ? REVIEW_CHECK_RIGHT : strcmp(args,"--check-remote")==0 ? REVIEW_REMOTE_CHECK : REVIEW_CONFIG;
|
||||
WPARAM mode=message==REVIEW_CONFIG ? (WPARAM)atoi(args+12) : 0;
|
||||
if (!SendMessageTimeoutW(target,message,mode,0,SMTO_ABORTIFHUNG,3000,&result)) return 4;
|
||||
return result ? 0 : 5;
|
||||
}
|
||||
WNDCLASSW cls = {0};
|
||||
cls.hInstance = instance; cls.lpfnWndProc = window_proc;
|
||||
cls.lpszClassName = secondary ? L"WineAccessReviewSecondary" : L"WineAccessReviewFixture";
|
||||
cls.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1);
|
||||
RegisterClassW(&cls);
|
||||
WNDCLASSW stall=cls; stall.lpszClassName=L"WineReviewStall"; stall.lpfnWndProc=stall_proc;
|
||||
RegisterClassW(&stall);
|
||||
HWND window = CreateWindowW(cls.lpszClassName, secondary ? L"Wine secondary fixture" : L"Wine review fixture",
|
||||
WS_OVERLAPPEDWINDOW, 100, 100, 560, 500, NULL, NULL, instance, NULL);
|
||||
CreateWindowW(L"Static", L"&Command:", WS_CHILD | WS_VISIBLE, 20, 10, 200, 20,
|
||||
window, NULL, instance, NULL);
|
||||
editor = CreateWindowW(L"Edit", L"", WS_CHILD | WS_VISIBLE | WS_TABSTOP | WS_BORDER |
|
||||
ES_MULTILINE | ES_AUTOVSCROLL | WS_VSCROLL, 20, 35, 320, 90, window, (HMENU)101, instance, NULL);
|
||||
original_edit = (WNDPROC)SetWindowLongPtrW(editor, GWLP_WNDPROC, (LONG_PTR)edit_proc);
|
||||
output = CreateWindowW(L"Edit", L"output first\r\noutput second", WS_CHILD | WS_VISIBLE |
|
||||
ES_MULTILINE | ES_READONLY | WS_BORDER, 20, 205, 320, 80, window, (HMENU)102, instance, NULL);
|
||||
password = CreateWindowW(L"Edit", L"private-review-secret", WS_CHILD | WS_VISIBLE |
|
||||
ES_PASSWORD | WS_BORDER, 20, 320, 200, 25, window, (HMENU)103, instance, NULL);
|
||||
hidden = CreateWindowW(L"Static", L"hidden-review-secret", WS_CHILD, 20, 350, 200, 25,
|
||||
window, (HMENU)104, instance, NULL);
|
||||
CreateWindowW(L"Button", L"&Open && close", WS_CHILD | WS_VISIBLE, 20, 390, 160, 25,
|
||||
window, (HMENU)105, instance, NULL);
|
||||
HWND list = CreateWindowW(L"ListBox", L"", WS_CHILD | WS_VISIBLE | LBS_NOTIFY,
|
||||
350, 35, 150, 70, window, (HMENU)106, instance, NULL);
|
||||
original_list = (WNDPROC)SetWindowLongPtrW(list,GWLP_WNDPROC,(LONG_PTR)list_proc);
|
||||
SendMessageW(list, LB_ADDSTRING, 0, (LPARAM)L"native & item");
|
||||
SendMessageW(list, LB_ADDSTRING, 0, (LPARAM)L"another item");
|
||||
INITCOMMONCONTROLSEX controls = {sizeof(controls), ICC_LISTVIEW_CLASSES};
|
||||
InitCommonControlsEx(&controls);
|
||||
HWND table = CreateWindowW(WC_LISTVIEWW,L"",WS_CHILD|LVS_REPORT,
|
||||
350,200,170,100,window,(HMENU)107,instance,NULL);
|
||||
LVCOLUMNW column = {0}; column.mask=LVCF_TEXT|LVCF_WIDTH; column.cx=100; column.pszText=L"Plugin";
|
||||
SendMessageW(table,LVM_INSERTCOLUMNW,0,(LPARAM)&column);
|
||||
column.cx=60; column.pszText=L"State";
|
||||
SendMessageW(table,LVM_INSERTCOLUMNW,1,(LPARAM)&column);
|
||||
LVITEMW item={0}; item.mask=LVIF_TEXT; item.pszText=L"table & item";
|
||||
SendMessageW(table,LVM_INSERTITEMW,0,(LPARAM)&item);
|
||||
item.iSubItem=1; item.pszText=L"ready";
|
||||
SendMessageW(table,LVM_SETITEMTEXTW,0,(LPARAM)&item);
|
||||
original_table=(WNDPROC)SetWindowLongPtrW(table,GWLP_WNDPROC,(LONG_PTR)table_proc);
|
||||
CreateWindowW(L"Edit",L"abcdefghijklmnopqrstuvwxyz abcdefghijklmnopqrstuvwxyz visible suffix",
|
||||
WS_CHILD|ES_AUTOHSCROLL|WS_BORDER,20,130,150,25,window,(HMENU)109,instance,NULL);
|
||||
CreateWindowW(L"Edit",L"abcdefghijklmnopqrstuvwxyz abcdefghijklmnopqrstuvwxyz visible suffix",
|
||||
WS_CHILD|ES_MULTILINE|ES_AUTOHSCROLL|WS_HSCROLL|WS_BORDER,20,165,150,50,window,(HMENU)110,instance,NULL);
|
||||
CreateWindowW(L"Static",L"literal & label",WS_CHILD|SS_NOPREFIX,
|
||||
20,365,200,20,window,(HMENU)111,instance,NULL);
|
||||
CreateWindowW(stall.lpszClassName,L"",WS_CHILD|WS_VISIBLE,350,150,150,20,window,NULL,instance,NULL);
|
||||
ShowWindow(window, SW_SHOW);
|
||||
SendMessageW(window, REVIEW_CONFIG, 0, 0);
|
||||
if (secondary) SetTimer(window,1,400,NULL);
|
||||
MSG message;
|
||||
while (GetMessageW(&message, NULL, 0, 0) > 0) {
|
||||
TranslateMessage(&message); DispatchMessageW(&message);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,184 @@
|
||||
"""Read-only review protocol checks in an isolated Wine prefix/session bus."""
|
||||
import argparse
|
||||
import os
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import time
|
||||
from gi.repository import Gio, GLib
|
||||
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('--build-dir', default='build')
|
||||
parser.add_argument('--32-bit', dest='is32', action='store_true')
|
||||
parser.add_argument('--session', action='store_true')
|
||||
args = parser.parse_args()
|
||||
build = Path(args.build_dir).resolve()
|
||||
if not args.session:
|
||||
sys.exit(subprocess.call(['dbus-run-session', '--', sys.executable, __file__,
|
||||
'--session', '--build-dir', str(build), *(['--32-bit'] if args.is32 else [])]))
|
||||
iface = 'org.stormux.Cthulhu1.Service'
|
||||
path = '/org/stormux/Cthulhu1/Service'
|
||||
capture_iface = 'org.stormux.Cthulhu1.WineReview'
|
||||
capture_path = '/org/stormux/Cthulhu1/WineReview'
|
||||
|
||||
def interface_xml(review=True):
|
||||
methods = {'PresentAccessibleEvent': ['s']*7+['i','i','s']}
|
||||
if review:
|
||||
methods.update(RegisterWineReviewProvider=['o','u'], PresentWineReviewFocus=['s','s','s','t','s','s','b','(iiii)'])
|
||||
return '<node><interface name="'+iface+'">'+''.join(
|
||||
'<method name="'+name+'">'+''.join('<arg type="'+kind+'" direction="in"/>' for kind in kinds)+
|
||||
'<arg type="b" direction="out"/></method>' for name,kinds in methods.items())+'</interface></node>'
|
||||
|
||||
providers, focuses, events = {}, {}, []
|
||||
def record(conn, sender, object_path, interface, method, params, invocation):
|
||||
data = params.unpack()
|
||||
accepted = True
|
||||
if method == 'RegisterWineReviewProvider':
|
||||
accepted = data == (capture_path, 1)
|
||||
if accepted: providers[sender] = data
|
||||
elif method == 'PresentWineReviewFocus':
|
||||
accepted = sender in providers
|
||||
if accepted: focuses[sender] = data
|
||||
else: events.append(data)
|
||||
invocation.return_value(GLib.Variant('(b)', (accepted,)))
|
||||
|
||||
connection = Gio.bus_get_sync(Gio.BusType.SESSION, None)
|
||||
registration = connection.register_object(path, Gio.DBusNodeInfo.new_for_xml(interface_xml()).interfaces[0], record, None, None)
|
||||
connection.call_sync('org.freedesktop.DBus','/org/freedesktop/DBus','org.freedesktop.DBus','RequestName',
|
||||
GLib.Variant('(su)',(iface,0)),None,Gio.DBusCallFlags.NONE,1000,None)
|
||||
|
||||
def pump_until(predicate, seconds=5):
|
||||
end = time.monotonic()+seconds
|
||||
while not predicate() and time.monotonic()<end:
|
||||
while GLib.MainContext.default().iteration(False): pass
|
||||
time.sleep(.005)
|
||||
assert predicate(), f'private review protocol condition timed out; providers={providers}; helper={helper.poll()}; app={app.poll()}'
|
||||
|
||||
def request(conn, sender, focus):
|
||||
result = []
|
||||
started = time.monotonic()
|
||||
def finished(source, pending):
|
||||
try: result.append((source.call_finish(pending), time.monotonic()-started))
|
||||
except GLib.Error as error: result.append(error)
|
||||
conn.call(sender,capture_path,capture_iface,'CaptureWindow',GLib.Variant('(sst)',focus[1:4]),
|
||||
GLib.VariantType.new('(a{sv})'),Gio.DBusCallFlags.NONE,1500,None,finished)
|
||||
return result
|
||||
|
||||
with tempfile.TemporaryDirectory(prefix='wine-review-wire-') as temporary:
|
||||
env = os.environ.copy()
|
||||
env.update(WINEPREFIX=str(Path(temporary)/'prefix'),WINEDEBUG=os.environ.get('WINE_ACCESS_TEST_DEBUG','-all'),WINEDLLOVERRIDES='mscoree,mshtml=',CTHULHU_WINE_ACCESS_MANAGED='1')
|
||||
fixture = build/('review-fixture32.exe' if args.is32 else 'review-fixture64.exe')
|
||||
helper, app, secondary = None, None, None
|
||||
def configure(mode):
|
||||
subprocess.run(['wine',str(fixture),'--configure',str(mode)],env=env,check=True,timeout=5)
|
||||
while GLib.MainContext.default().iteration(False): pass
|
||||
def capture():
|
||||
end=time.monotonic()+.15
|
||||
while time.monotonic()<end:
|
||||
while GLib.MainContext.default().iteration(False): pass
|
||||
time.sleep(.005)
|
||||
sender, focus = next((s,f) for s,f in focuses.items() if f[4]=='Wine review fixture')
|
||||
result = request(connection,sender,focus)
|
||||
pump_until(lambda: bool(result),2)
|
||||
assert not isinstance(result[0],GLib.Error),f'{result[0]}; helper={helper.poll()}'
|
||||
return result[0]
|
||||
try:
|
||||
subprocess.run(['wineboot','-u'],env=env,check=True,timeout=45)
|
||||
helper = subprocess.Popen([str(build/'cthulhu-wine-access.exe'),'--no-controller'],env=env)
|
||||
app = subprocess.Popen(['wine',str(fixture)],env=env)
|
||||
pump_until(lambda: any(f[4]=='Wine review fixture' for f in focuses.values()),8)
|
||||
reply, elapsed = capture()
|
||||
fields = reply.unpack()[0]
|
||||
assert fields['status']=='ok' and fields['version']==1, fields
|
||||
assert elapsed<.8 and fields['nodes']
|
||||
wire = reply.get_child_value(0)
|
||||
assert wire.lookup_value('version',GLib.VariantType.new('u')) is not None
|
||||
assert wire.lookup_value('revision',GLib.VariantType.new('t')) is not None
|
||||
assert wire.lookup_value('nodes',GLib.VariantType.new('aa{sv}')) is not None
|
||||
nodes=wire.lookup_value('nodes',GLib.VariantType.new('aa{sv}'))
|
||||
for index in range(nodes.n_children()):
|
||||
node=nodes.get_child_value(index)
|
||||
for key,kind in {'id':'s','parent':'s','role':'s','name':'s','protected':'b','visible':'b',
|
||||
'text_known':'b','caret_known':'b','caret':'t','caret_utf16':'t',
|
||||
'bounds':'(iiii)','ranges':'a(tttts(iiii))'}.items():
|
||||
assert node.lookup_value(key,GLib.VariantType.new(kind)) is not None,(key,kind)
|
||||
strings = str(fields)
|
||||
assert 'alpha beta' in strings and 'output second' in strings and 'native & item' in strings
|
||||
assert 'private-review-secret' not in strings and 'hidden-review-secret' not in strings
|
||||
subprocess.run(['wine',str(fixture),'--check'],env=env,check=True,timeout=5)
|
||||
print('PASS: test_capture_wire_contract')
|
||||
other = Gio.DBusConnection.new_for_address_sync(os.environ['DBUS_SESSION_BUS_ADDRESS'],
|
||||
Gio.DBusConnectionFlags.AUTHENTICATION_CLIENT|Gio.DBusConnectionFlags.MESSAGE_BUS_CONNECTION,None,None)
|
||||
sender,focus = next((s,f) for s,f in focuses.items() if f[4]=='Wine review fixture')
|
||||
wrong = request(other,sender,focus); pump_until(lambda:bool(wrong),2)
|
||||
assert isinstance(wrong[0],GLib.Error) and 'AccessDenied' in str(wrong[0])
|
||||
other.close_sync(None)
|
||||
print('PASS: test_wrong_caller')
|
||||
configure(9)
|
||||
reply,_ = capture(); data=reply.unpack()[0]
|
||||
assert data['status'] in ('ok','stale')
|
||||
if data['status']=='stale': assert not data['nodes']
|
||||
else: assert 'omega zeta' in str(data) and 'alpha beta' not in str(data)
|
||||
print('PASS: test_same_length_capture_race')
|
||||
configure(21)
|
||||
reply,elapsed=capture(); data=reply.unpack()[0]
|
||||
assert data['status']=='unavailable' and not data['nodes'] and elapsed<.8,(data,elapsed)
|
||||
subprocess.run(['wine',str(fixture),'--check-remote'],env=env,check=True,timeout=5)
|
||||
print('PASS: test_native_buffer_lifetime_after_deadline')
|
||||
# Exiting the helper during a custom message must not invalidate the
|
||||
# application's remote allocation while its window procedure runs.
|
||||
configure(21)
|
||||
sender,focus=next((s,f) for s,f in focuses.items() if f[4]=='Wine review fixture')
|
||||
request(connection,sender,focus)
|
||||
end=time.monotonic()+.2
|
||||
while time.monotonic()<end:
|
||||
while GLib.MainContext.default().iteration(False): pass
|
||||
time.sleep(.005)
|
||||
helper.terminate(); helper.wait(timeout=3)
|
||||
subprocess.run(['wine',str(fixture),'--check-remote'],env=env,check=True,timeout=5)
|
||||
print('PASS: test_native_buffer_lifetime_after_helper_stop')
|
||||
helper=subprocess.Popen([str(build/'cthulhu-wine-access.exe'),'--no-controller'],env=env)
|
||||
pump_until(lambda:any(s != sender and f[4]=='Wine review fixture' for s,f in focuses.items()),5)
|
||||
focuses.pop(sender,None)
|
||||
configure(7)
|
||||
reply,_ = capture(); data=reply.unpack()[0]
|
||||
assert data['status']=='limited' and not data['nodes']
|
||||
configure(10)
|
||||
sender,focus = next((s,f) for s,f in focuses.items() if f[4]=='Wine review fixture')
|
||||
requests = [request(connection,sender,focus) for _ in range(5)]
|
||||
secondary = subprocess.Popen(['wine',str(fixture),'--secondary'],env=env)
|
||||
pump_until(lambda:all(requests),2)
|
||||
statuses = [result[0][0].unpack()[0]['status'] for result in requests]
|
||||
assert 'busy' in statuses, statuses
|
||||
assert 'unavailable' in statuses, statuses
|
||||
for result in requests:
|
||||
assert result[0][1]<.8, result[0][1]
|
||||
assert not result[0][0].unpack()[0]['nodes']
|
||||
print('PASS: test_busy_capture; test_capture_deadline')
|
||||
pump_until(lambda:any(event[9]=='Wine secondary fixture' and event[1]=='focus' for event in events),1)
|
||||
print('PASS: test_stalled_capture_keeps_focus_speech')
|
||||
started=time.monotonic(); helper.terminate(); helper.wait(timeout=3)
|
||||
assert time.monotonic()-started<3
|
||||
print('PASS: test_stop_during_capture')
|
||||
secondary.terminate(); secondary.wait(timeout=3)
|
||||
time.sleep(2)
|
||||
connection.unregister_object(registration)
|
||||
registration = connection.register_object(path,Gio.DBusNodeInfo.new_for_xml(interface_xml(False)).interfaces[0],record,None,None)
|
||||
events.clear()
|
||||
helper = subprocess.Popen([str(build/'cthulhu-wine-access.exe'),'--no-controller'],env=env)
|
||||
end=time.monotonic()+1
|
||||
while time.monotonic()<end:
|
||||
while GLib.MainContext.default().iteration(False): pass
|
||||
time.sleep(.005)
|
||||
configure(11)
|
||||
pump_until(lambda:bool(events),5)
|
||||
assert helper.poll() is None
|
||||
print('PASS: test_old_reader')
|
||||
finally:
|
||||
for process in (app,helper,secondary):
|
||||
if process is not None and process.poll() is None:
|
||||
process.terminate()
|
||||
try: process.wait(timeout=3)
|
||||
except subprocess.TimeoutExpired: process.kill(); process.wait(timeout=3)
|
||||
subprocess.run(['wineserver','-k'],env=env,timeout=5)
|
||||
Reference in new issue
Block a user