mirror of
https://github.com/baketnk/frame-yap.git
synced 2026-10-06 02:00:06 +02:00
Add opt-in fail-closed Xwayland auto insert
This commit is contained in:
1 parent
19a3db7667
commit
c5e925cb48
27 files changed
+588
-45
No files matched your search
@@ -29,12 +29,14 @@ int main(int argc, char** argv) {
|
||||
assert(load_config(path).buttons.empty());
|
||||
assert(!load_config(path).experimental_input_priority);
|
||||
assert(!load_config(path).advanced_debug);
|
||||
assert(!load_config(path).auto_insert);
|
||||
assert(load_config(path).wrist.width == .30f);
|
||||
auto example = load_config(std::filesystem::path(argv[1]) / "config.example.json");
|
||||
assert(example.buttons.at("ptt") == "/user/hand/right/input/x");
|
||||
assert(example.font.empty());
|
||||
assert(!example.experimental_input_priority);
|
||||
assert(!example.advanced_debug);
|
||||
assert(!example.auto_insert);
|
||||
assert(example.wrist.y == .18f && example.wrist.z == .089f);
|
||||
std::filesystem::create_directories(path.parent_path());
|
||||
assert(save_advanced_debug(path, true));
|
||||
@@ -42,6 +44,10 @@ int main(int argc, char** argv) {
|
||||
assert(get(path).find("\"advanced_debug\":true") != std::string::npos);
|
||||
assert(save_advanced_debug(path, false));
|
||||
assert(!load_config(path).advanced_debug);
|
||||
assert(save_auto_insert(path, true));
|
||||
assert(load_config(path).auto_insert);
|
||||
assert(save_auto_insert(path, false));
|
||||
assert(!load_config(path).auto_insert);
|
||||
for (const auto* invalid : {R"({"advanced_debug":"true"})", R"({"advanced_debug":0})",
|
||||
R"({"advanced_debug":null})", R"({"advanced_debug":[]})"}) {
|
||||
put(path, invalid);
|
||||
@@ -69,6 +75,7 @@ int main(int argc, char** argv) {
|
||||
auto link = dir / "linked-config";
|
||||
std::filesystem::create_symlink(path, link);
|
||||
assert(!save_advanced_debug(link, false));
|
||||
assert(!save_auto_insert(link, true));
|
||||
assert(get(path) == before);
|
||||
std::filesystem::remove(link);
|
||||
put(path, R"({"font":")" + std::string(4090, 'x') + R"("})");
|
||||
@@ -79,6 +86,13 @@ int main(int argc, char** argv) {
|
||||
assert(experimental.experimental_input_priority && experimental.advanced_debug);
|
||||
// A priority request must preserve the user's existing action manifest/bindings.
|
||||
assert(action_manifest(argv[1], experimental) == std::filesystem::absolute(std::filesystem::path(argv[1]) / "actions.json"));
|
||||
put(path, R"({"auto_insert":"on"})");
|
||||
fails([&] { load_config(path); });
|
||||
assert(!save_auto_insert(path, true));
|
||||
put(path, R"({"auto_insert":false,"font":"kept"})");
|
||||
assert(save_auto_insert(path, true));
|
||||
assert(load_config(path).auto_insert);
|
||||
assert(get(path) == R"({"auto_insert":true,"font":"kept"})");
|
||||
put(path, R"({"input_priority":"normal"})");
|
||||
assert(!load_config(path).experimental_input_priority);
|
||||
for (const auto* invalid : {R"({"input_priority":true})", R"({"input_priority":16777216})",
|
||||
|
||||
@@ -75,6 +75,18 @@ void delivery_checks() {
|
||||
CHECK(deliver_enter(s, fake.factory()) == DeliveryResult::EnterQueued);
|
||||
CHECK((fake.events == std::vector<std::string>{"text:preserve ", "enter"}));
|
||||
}
|
||||
{
|
||||
auto s = review("focus lost during lease"); FakeDelivery fake;
|
||||
auto acquire = fake.factory();
|
||||
const DeliveryFactory invalidated_after_acquisition = [&]() -> std::unique_ptr<DeliveryLease> {
|
||||
auto lease = acquire();
|
||||
throw std::runtime_error("focus changed while connecting to IME");
|
||||
};
|
||||
try { (void)deliver_insert(s, invalidated_after_acquisition); CHECK(false); }
|
||||
catch (const std::runtime_error&) {}
|
||||
CHECK(s.state() == State::Review && s.text() == "focus lost during lease");
|
||||
CHECK(fake.events.empty() && fake.active_leases == 0);
|
||||
}
|
||||
{
|
||||
auto s = review("uncertain"); FakeDelivery fake; fake.fail_text = true;
|
||||
CHECK(deliver_enter(s, fake.factory()) == DeliveryResult::TextUncertain);
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
#include "focus_guard.hpp"
|
||||
|
||||
#include <cstdlib>
|
||||
#include <iostream>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
|
||||
using frameyap::FocusGuard;
|
||||
#define CHECK(condition) do { if (!(condition)) throw std::runtime_error( \
|
||||
std::string("line ") + std::to_string(__LINE__) + ": " #condition); } while (false)
|
||||
|
||||
class DisplaySetting {
|
||||
public:
|
||||
DisplaySetting() {
|
||||
if (const char* current = std::getenv("DISPLAY")) {
|
||||
previous_ = current;
|
||||
had_previous_ = true;
|
||||
}
|
||||
if (setenv("DISPLAY", "frameyap-no-such-display:9876", 1) != 0)
|
||||
throw std::runtime_error("could not set isolated DISPLAY");
|
||||
}
|
||||
~DisplaySetting() {
|
||||
if (had_previous_) setenv("DISPLAY", previous_.c_str(), 1);
|
||||
else unsetenv("DISPLAY");
|
||||
}
|
||||
DisplaySetting(const DisplaySetting&) = delete;
|
||||
DisplaySetting& operator=(const DisplaySetting&) = delete;
|
||||
private:
|
||||
std::string previous_;
|
||||
bool had_previous_ = false;
|
||||
};
|
||||
|
||||
int main() {
|
||||
try {
|
||||
DisplaySetting isolated_display;
|
||||
FocusGuard guard; // Construction must not connect to X.
|
||||
CHECK(!guard.valid());
|
||||
CHECK(!guard.arm()); // No server: fail closed.
|
||||
CHECK(!guard.valid());
|
||||
CHECK(!guard.arm()); // No reconnect/rearm attempt.
|
||||
guard.invalidate();
|
||||
CHECK(!guard.valid());
|
||||
FocusGuard explicitly_invalidated;
|
||||
explicitly_invalidated.invalidate();
|
||||
CHECK(!explicitly_invalidated.arm());
|
||||
CHECK(!explicitly_invalidated.valid());
|
||||
std::cout << "focus_guard fail-closed policy checks passed\n";
|
||||
return 0;
|
||||
} catch (const std::exception& e) {
|
||||
std::cerr << "focus_guard_test: " << e.what() << '\n';
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
#include "focus_guard.hpp"
|
||||
#include <xcb/xcb.h>
|
||||
#include <cassert>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <iostream>
|
||||
#include <memory>
|
||||
|
||||
using namespace frameyap;
|
||||
namespace {
|
||||
struct Free { void operator()(void* p) const { std::free(p); } };
|
||||
template<class T> using Reply = std::unique_ptr<T, Free>;
|
||||
xcb_atom_t atom(xcb_connection_t* c, const char* name) {
|
||||
auto cookie = xcb_intern_atom(c, 0, std::strlen(name), name);
|
||||
Reply<xcb_intern_atom_reply_t> reply(xcb_intern_atom_reply(c, cookie, nullptr));
|
||||
assert(reply && reply->atom);
|
||||
return reply->atom;
|
||||
}
|
||||
void focus(xcb_connection_t* c, xcb_window_t root, xcb_window_t window,
|
||||
xcb_atom_t active, xcb_atom_t gamescope) {
|
||||
xcb_change_property(c, XCB_PROP_MODE_REPLACE, root, active, XCB_ATOM_WINDOW, 32, 1, &window);
|
||||
xcb_change_property(c, XCB_PROP_MODE_REPLACE, root, gamescope, XCB_ATOM_CARDINAL, 32, 1, &window);
|
||||
xcb_set_input_focus(c, XCB_INPUT_FOCUS_POINTER_ROOT, window, XCB_CURRENT_TIME);
|
||||
auto cookie = xcb_get_input_focus(c);
|
||||
Reply<xcb_get_input_focus_reply_t> reply(xcb_get_input_focus_reply(c, cookie, nullptr));
|
||||
assert(reply && reply->focus == window);
|
||||
}
|
||||
}
|
||||
int main() {
|
||||
int index = 0;
|
||||
xcb_connection_t* c = xcb_connect(nullptr, &index);
|
||||
assert(c && !xcb_connection_has_error(c));
|
||||
auto screens = xcb_setup_roots_iterator(xcb_get_setup(c));
|
||||
for (int i = 0; i < index; ++i) xcb_screen_next(&screens);
|
||||
assert(screens.rem);
|
||||
const auto root = screens.data->root;
|
||||
const auto active = atom(c, "_NET_ACTIVE_WINDOW"), gamescope = atom(c, "GAMESCOPE_FOCUSED_WINDOW");
|
||||
const auto a = xcb_generate_id(c), b = xcb_generate_id(c);
|
||||
xcb_create_window(c, XCB_COPY_FROM_PARENT, a, root, 0, 0, 160, 100, 0,
|
||||
XCB_WINDOW_CLASS_INPUT_OUTPUT, screens.data->root_visual, 0, nullptr);
|
||||
xcb_create_window(c, XCB_COPY_FROM_PARENT, b, root, 180, 0, 160, 100, 0,
|
||||
XCB_WINDOW_CLASS_INPUT_OUTPUT, screens.data->root_visual, 0, nullptr);
|
||||
xcb_map_window(c, a); xcb_map_window(c, b);
|
||||
focus(c, root, a, active, gamescope);
|
||||
FocusGuard stable;
|
||||
assert(stable.arm() && stable.valid());
|
||||
focus(c, root, b, active, gamescope);
|
||||
focus(c, root, a, active, gamescope);
|
||||
assert(!stable.valid()); // loss/regain of the same ID cannot authorize delivery
|
||||
assert(!stable.arm());
|
||||
FocusGuard new_capture;
|
||||
assert(new_capture.arm() && new_capture.valid());
|
||||
xcb_change_property(c, XCB_PROP_MODE_REPLACE, root, active, XCB_ATOM_WINDOW, 32, 1, &b);
|
||||
auto cookie = xcb_get_input_focus(c);
|
||||
Reply<xcb_get_input_focus_reply_t> reply(xcb_get_input_focus_reply(c, cookie, nullptr));
|
||||
assert(reply);
|
||||
assert(!new_capture.valid()); // compositor/X disagreement fails closed
|
||||
focus(c, root, a, active, gamescope);
|
||||
FocusGuard destroyed;
|
||||
assert(destroyed.arm());
|
||||
xcb_destroy_window(c, a);
|
||||
cookie = xcb_get_input_focus(c);
|
||||
reply.reset(xcb_get_input_focus_reply(c, cookie, nullptr));
|
||||
assert(reply);
|
||||
assert(!destroyed.valid());
|
||||
xcb_disconnect(c);
|
||||
std::cout << "focus guard XCB transitions passed\n";
|
||||
}
|
||||
@@ -12,7 +12,7 @@ SurfaceEvent click(PanelSurface& surface, float x, float y, unsigned cursor = 0)
|
||||
return surface.pointer_up(cursor, x, y);
|
||||
}
|
||||
void no_action(const SurfaceEvent& event) {
|
||||
assert(!event.action && !event.mount && !event.lasers_anytime && !event.advanced_debug &&
|
||||
assert(!event.action && !event.mount && !event.lasers_anytime && !event.advanced_debug && !event.auto_insert &&
|
||||
!event.recenter && !event.open_bindings);
|
||||
}
|
||||
void snapshot(PanelSurface& surface, const std::string& path) {
|
||||
@@ -126,6 +126,13 @@ int main(int argc, char** argv) {
|
||||
laser = click(surface, 680, 420);
|
||||
assert(laser.lasers_anytime == false && !laser.action && !laser.mount);
|
||||
surface.set_lasers_anytime(false); assert(surface.render(p));
|
||||
auto automatic = click(surface, 200, 474);
|
||||
assert(automatic.auto_insert == true && !automatic.action);
|
||||
assert(!surface.render(p)); // request alone has no effect
|
||||
surface.set_auto_insert(true); assert(surface.render(p));
|
||||
automatic = click(surface, 200, 474);
|
||||
assert(automatic.auto_insert == false);
|
||||
surface.set_auto_insert(false); assert(surface.render(p));
|
||||
auto debug = click(surface, 680, 474);
|
||||
assert(debug.advanced_debug == true && !debug.action && !debug.mount && !debug.lasers_anytime);
|
||||
assert(!surface.render(p)); // an event is only a request; caller sets the accepted value
|
||||
@@ -151,6 +158,7 @@ int main(int argc, char** argv) {
|
||||
no_action(click(surface, 680, 260)); // mount controls not active on Review
|
||||
no_action(click(surface, 680, 420)); // laser toggle only exists on Settings
|
||||
no_action(click(surface, 680, 474)); // debug toggle only exists on Settings
|
||||
no_action(click(surface, 200, 474)); // auto insert only exists on Settings
|
||||
|
||||
// Bindings opens SteamVR directly, from either tab, without replacing review.
|
||||
surface.pointer_down(1, 480, 610);
|
||||
|
||||
@@ -74,6 +74,7 @@ class InstallTests(unittest.TestCase):
|
||||
config = self.home / ".config/frameyap/config.json"
|
||||
self.assertEqual(json.loads(config.read_text()), installer.CONFIG_DEFAULTS)
|
||||
self.assertIs(json.loads(config.read_text())["advanced_debug"], False)
|
||||
self.assertIs(json.loads(config.read_text())["auto_insert"], False)
|
||||
self.assertEqual(list(config.parent.glob("config.json.backup-*")), [])
|
||||
self.assertIn("PYTHONDONTWRITEBYTECODE=1", launcher.read_text())
|
||||
self.assertTrue(os.access(root / "versions/v1/runtime/bin/helper", os.X_OK))
|
||||
@@ -127,12 +128,14 @@ class InstallTests(unittest.TestCase):
|
||||
self.assertEqual(fixed["buttons"]["cancel"], "/user/hand/right/input/b")
|
||||
self.assertEqual(fixed["input_priority"], "normal")
|
||||
self.assertIs(fixed["advanced_debug"], False)
|
||||
self.assertIs(fixed["auto_insert"], False)
|
||||
self.assertEqual(fixed["wrist"], installer.CONFIG_DEFAULTS["wrist"])
|
||||
backups = list(config.parent.glob("config.json.backup-*"))
|
||||
self.assertEqual(len(backups), 1)
|
||||
self.assertEqual(backups[0].read_bytes(), original)
|
||||
fixed["input_priority"] = "experimental"
|
||||
fixed["advanced_debug"] = True
|
||||
fixed["auto_insert"] = True
|
||||
fixed["buttons"]["enter"] = "" # intentional disabling survives upgrades
|
||||
fixed["wrist"]["y"] = 0.2
|
||||
compact = json.dumps(fixed, separators=(",", ":")).encode()
|
||||
@@ -140,6 +143,7 @@ class InstallTests(unittest.TestCase):
|
||||
self.install("v1", archive, digest)
|
||||
self.assertEqual(config.read_bytes(), compact)
|
||||
self.assertIs(json.loads(config.read_text())["advanced_debug"], True)
|
||||
self.assertIs(json.loads(config.read_text())["auto_insert"], True)
|
||||
self.assertEqual(len(list(config.parent.glob("config.json.backup-*"))), 1)
|
||||
fixed["advanced_debug"] = False
|
||||
compact_off = json.dumps(fixed, separators=(",", ":")).encode()
|
||||
@@ -156,6 +160,7 @@ class InstallTests(unittest.TestCase):
|
||||
self.assertEqual(fixed["buttons"], installer.CONFIG_DEFAULTS["buttons"]) # colliding paths reset
|
||||
self.assertEqual(fixed["input_priority"], "normal")
|
||||
self.assertIs(fixed["advanced_debug"], False)
|
||||
self.assertIs(fixed["auto_insert"], False)
|
||||
self.assertEqual(fixed["wrist"]["x"], 0.04)
|
||||
self.assertEqual(fixed["wrist"]["y"], 0.18)
|
||||
self.assertEqual(fixed["wrist"]["width"], 0.30)
|
||||
@@ -193,6 +198,17 @@ class InstallTests(unittest.TestCase):
|
||||
self.assertEqual(json.loads(config.read_text())["font"], "/custom/font.ttf")
|
||||
self.assertIn(original, [p.read_bytes() for p in config.parent.glob("config.json.backup-*")])
|
||||
|
||||
def test_auto_insert_boolean_repair_backs_up_invalid_values(self):
|
||||
archive, digest = self.package("v1")
|
||||
config = self.home / ".config/frameyap/config.json"
|
||||
config.parent.mkdir(parents=True)
|
||||
for invalid in ("true", 1, None, [], {}):
|
||||
original = json.dumps({"auto_insert": invalid, "font": "/custom/font.ttf"}).encode()
|
||||
config.write_bytes(original)
|
||||
self.install("v1", archive, digest)
|
||||
self.assertIs(json.loads(config.read_text())["auto_insert"], False)
|
||||
self.assertIn(original, [p.read_bytes() for p in config.parent.glob("config.json.backup-*")])
|
||||
|
||||
def test_digest_and_same_version_mismatch_leave_previous(self):
|
||||
a, h = self.package("v1")
|
||||
self.install("v1", a, h)
|
||||
|
||||
Reference in new issue
Block a user