From 01dea9ca104be8cf5a8c8141d534aa192d0038a4 Mon Sep 17 00:00:00 2001 From: serenity4 Date: Sat, 11 Nov 2023 15:12:16 +0100 Subject: [PATCH] Implement record & replay functionality --- Project.toml | 2 +- src/XCB.jl | 4 ++- src/events.jl | 8 +++--- src/testing.jl | 67 +++++++++++++++++++++++++++++++++++++++++++++--- src/window.jl | 5 ++-- test/runtests.jl | 37 +++++++++++++++++--------- 6 files changed, 101 insertions(+), 22 deletions(-) diff --git a/Project.toml b/Project.toml index 369830a..e176da9 100644 --- a/Project.toml +++ b/Project.toml @@ -15,7 +15,7 @@ Xorg_xcb_util_jll = "2def613f-5ad1-5310-b15b-b15d46f528f5" BitMasks = "0.1" DocStringExtensions = "0.8, 0.9" Reexport = "1" -WindowAbstractions = "0.7" +WindowAbstractions = "0.7.1" XKeyboard = "0.1" Xorg_libxcb_jll = "1.13" Xorg_xcb_util_jll = "0.4" diff --git a/src/XCB.jl b/src/XCB.jl index 5e1c873..e2c53b1 100644 --- a/src/XCB.jl +++ b/src/XCB.jl @@ -38,7 +38,9 @@ import WindowAbstractions: set_title, KeyEvent, ModifierState, Event, - EventQueue + EventQueue, + save_history, + replay_history include("LibXCB.jl") @reexport using .LibXCB diff --git a/src/events.jl b/src/events.jl index 9879574..285c935 100644 --- a/src/events.jl +++ b/src/events.jl @@ -84,11 +84,13 @@ Event(wm::XWindowManager, win::XCBWindow, event::xcb_focus_in_event_t, t) = Event(response_type(event) == XCB_FOCUS_IN ? WINDOW_GAINED_FOCUS : WINDOW_LOST_FOCUS, nothing, (0.0, 0.0), t, win) function is_delete_request(event::xcb_client_message_event_t, win::XCBWindow) - ed_8 = Int.(event.data.data8) - event_data32_1 = ed_8[1] + ed_8[2] * 2^8 + ed_8[3] * 2^16 + ed_8[4] * 2^24 - event_data32_1 == win.delete_request + data = deserialize_delete_request_data(event.data.data8) + data == win.delete_request end +serialize_delete_request_data(delete_request::xcb_atom_t) = ntuple(i -> i ≤ 4 ? UInt8((delete_request << 8(4 - i)) >> 24) : 0x00, 20) +deserialize_delete_request_data(bytes) = xcb_atom_t(Int(bytes[1]) + Int(bytes[2]) << 8 + Int(bytes[3]) << 16 + Int(bytes[4]) << 24) + function poll_for_events!(queue::EventQueue{XWindowManager}) event = xcb_poll_for_event(queue.wm.conn) event == C_NULL && return false diff --git a/src/testing.jl b/src/testing.jl index 7a08446..6b30a18 100644 --- a/src/testing.jl +++ b/src/testing.jl @@ -23,6 +23,9 @@ function response_type_xcb(event::Event) event.type == POINTER_EXITED && return XCB_LEAVE_NOTIFY event.type == WINDOW_RESIZED && return XCB_CONFIGURE_NOTIFY event.type == WINDOW_EXPOSED && return XCB_EXPOSE + event.type == WINDOW_GAINED_FOCUS && return XCB_FOCUS_IN + event.type == WINDOW_LOST_FOCUS && return XCB_FOCUS_OUT + event.type == WINDOW_CLOSED && return XCB_CLIENT_MESSAGE error("No response type corresponding to $(event.type)") end @@ -33,6 +36,8 @@ function event_type_xcb(event::Event) event.type in POINTER_EVENT && return xcb_enter_notify_event_t event.type == WINDOW_RESIZED && return xcb_configure_notify_event_t event.type == WINDOW_EXPOSED && return xcb_expose_event_t + event.type in WINDOW_GAINED_FOCUS | WINDOW_LOST_FOCUS && return xcb_focus_in_event_t + event.type == WINDOW_CLOSED && return xcb_client_message_event_t error("No event type corresponding to $(event.type)") end @@ -55,9 +60,14 @@ end function event_xcb(wm::XWindowManager, e::Event) T = event_type_xcb(e) - T === xcb_expose_event_t && return T(response_type_xcb(e), 0, 0, e.win.id, e.location..., extent(e.win)..., 0, 0) - T === xcb_configure_notify_event_t && return T(response_type_xcb(e), 0, 0, e.win.id, e.win.id, 0, e.location..., extent(e.win)..., 0, 0, 0) - T(response_type_xcb(e), detail_xcb(wm, e), 0, e.time, e.win.parent_id, e.win.id, 0, 0, 0, e.location..., state_xcb(e), true, false) + wx, wy = extent(e.win) + x, y = (wx, wy) .* e.location + x, y = round(Int16, x), round(Int16, y) + T === xcb_expose_event_t && return T(response_type_xcb(e), 0, 0, e.win.id, x, y, wx, wy, 0, (0, 0)) + T === xcb_configure_notify_event_t && return T(response_type_xcb(e), 0, 0, e.win.id, e.win.id, 0, x, y, wx, wy, 0, 0, 0) + T === xcb_focus_in_event_t && return T(response_type_xcb(e), detail_xcb(wm, e), 0, e.win.id, 0, (0, 0, 0)) + e.type == WINDOW_CLOSED && return T(response_type_xcb(e), 32, 0, e.win.id, 0x00000183, xcb_client_message_data_t(serialize_delete_request_data(e.win.delete_request))) + T(response_type_xcb(e), detail_xcb(wm, e), 0, 0, e.win.parent_id, e.win.id, 0, 0, 0, x, y, state_xcb(e), true, false) end send_event(wm::XWindowManager, e::Event) = send_event(e.win, event_xcb(wm, e)) @@ -77,3 +87,54 @@ function send_event(wm::XWindowManager, win::XCBWindow, event_type::EventType, d end send_event(wm::XWindowManager, win::XCBWindow) = (event_type, data = nothing; location = (0.0, 0.0)) -> send_event(wm, win, event_type, data; location) + +struct WindowRef <: AbstractWindow + number::Int64 +end + +function save_history(wm::XWindowManager, queue::EventQueue{XWindowManager,XCBWindow}) + events = Event{WindowRef}[] + windows = XCBWindow[] + for event in queue.history + i = findfirst(==(event.win), windows) + isnothing(i) && push!(windows, event.win) + winref = WindowRef(something(i, lastindex(windows))) + push!(events, Event(event.type, event.data, event.location, event.time, winref)) + end + events +end + +# FIXME: Events will be triggered multiple times if an event triggers another. How should we tackle that? +function replay_history(wm::XWindowManager, events::AbstractVector{Event{WindowRef}}) + windows = Dict{WindowRef,XCBWindow}() + all_windows = xcb_window_t[] + replay_time = nothing + for event in events + win = get!(windows, event.win) do + # Assume that window IDs will be ordered chronologically. + union!(all_windows, keys(wm.windows)) + @assert issorted(all_windows) "Window IDs do not appear to be sorted chronologically" + i = event.win.number + wm.windows[all_windows[i]] + end + wait_for(event.time - something(replay_time, event.time)) + replay_time = event.time + event = Event(event.type, event.data, event.location, time(), win) + send_event(wm, event) + end +end + +function wait_for(Δt) + start = time() + + # Sleep if we need to wait for a while, to avoid holding onto CPU resources. + sleep_time = max(Δt - 0.002, 0) + if sleep_time > 0 + sleep(sleep_time) + end + + # Busy wait. + while time() < start + Δt + yield() + end +end diff --git a/src/window.jl b/src/window.jl index 78c8c11..28265f2 100644 --- a/src/window.jl +++ b/src/window.jl @@ -79,8 +79,9 @@ end function extent(win::XCBWindow) geometry_cookie = xcb_get_geometry(win.conn, win.id) geometry_reply = xcb_get_geometry_reply(win.conn, geometry_cookie, C_NULL) - geometry_reply == C_NULL && throw(InvalidWindow(win)) - getproperty.(Ref(unsafe_load(geometry_reply)), (:width, :height)) + geometry_reply == C_NULL && return (zero(UInt16), zero(UInt16)) + data = unsafe_load(geometry_reply) + data.width, data.height end function resize(win::XCBWindow, extent) diff --git a/test/runtests.jl b/test/runtests.jl index 960bede..809e42c 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1,8 +1,8 @@ using XCB using Test -function main(wm) - for event in EventQueue(wm) +function main(wm, queue = EventQueue(wm)) + for event in queue if event.type == WINDOW_CLOSED close(wm, event.win) elseif event.type == KEY_PRESSED @@ -53,11 +53,6 @@ function on_pressed_key(wm, event) write(io, String(wm.keymap)) end end - if matches(key"f", event) - send = send_event(wm, win) - @info "Faking input: sending key AD01 to quit (requires an english keyboard layout to be translated to the relevant symbol 'q')" - return send(KEY_PRESSED, KeyEvent(wm.keymap, PhysicalKey(wm.keymap, :AD01), NO_MODIFIERS)) - end (; gc) = win set_attributes(gc, [XCB.XCB_GC_FOREGROUND], [rand(1:16_777_215)]) @@ -74,12 +69,14 @@ function test() ctx = GraphicsContext(win, attributes=[XCB.XCB_GC_FOREGROUND, XCB.XCB_GC_GRAPHICS_EXPOSURES], values=[screen.black_pixel, 0]) attach_graphics_context!(win, ctx) send = send_event(wm, win) + queue = EventQueue(wm; record_history = true) if interactive - main(wm) + main(wm, queue) + isfile("keymap.c") && rm("keymap.c") else @info "Running window asynchronously" - task = @async main(wm) + task = @async main(wm, queue) @info "Sending fake inputs" send(BUTTON_PRESSED, MouseEvent(BUTTON_LEFT, BUTTON_NONE)) send(BUTTON_RELEASED, MouseEvent(BUTTON_LEFT, BUTTON_LEFT)) @@ -92,17 +89,33 @@ function test() send(KEY_RELEASED, KeyEvent(wm.keymap, PhysicalKey(wm.keymap, :AC09))) send(KEY_PRESSED, KeyEvent(wm.keymap, PhysicalKey(wm.keymap, :AC02))) send(KEY_RELEASED, KeyEvent(wm.keymap, PhysicalKey(wm.keymap, :AC02))) - send(KEY_PRESSED, KeyEvent(wm.keymap, PhysicalKey(wm.keymap, :AC04))) - send(KEY_RELEASED, KeyEvent(wm.keymap, PhysicalKey(wm.keymap, :AC04))) + sleep(0.5) + @info "Sending WINDOW_CLOSED event" + send(WINDOW_CLOSED) @info "Waiting for window to close" wait(task) @test !istaskfailed(task) @test isfile("keymap.c") + isfile("keymap.c") && rm("keymap.c") end + + events = save_history(wm, queue) + + @info "Replaying events..." + wm = XWindowManager() + screen = current_screen(wm) + win = XCBWindow(wm; screen, x=0, y=1000, border_width=50, window_title="XCB window", icon_title="XCB", attributes=[XCB.XCB_CW_BACK_PIXEL], values=[screen.black_pixel]) + ctx = GraphicsContext(win, attributes=[XCB.XCB_GC_FOREGROUND, XCB.XCB_GC_GRAPHICS_EXPOSURES], values=[screen.black_pixel, 0]) + attach_graphics_context!(win, ctx) + task = @async main(wm) + replay_history(wm, events) + wait(task) + @test !istaskfailed(task) isfile("keymap.c") && rm("keymap.c") + nothing end @testset "XCB.jl" begin - @test isnothing(test()) + test() end;