/** * @file src/quit_hotkey.cpp * @brief One answer to "what combination" and "may we watch for it", for both platforms. * * The two triggers are as different as they can be: Windows reserves the combination with the * OS and is told when it is pressed, Linux reads the shared keyboard itself and works it out * event by event. That difference is real and this file does not try to paper over it. What it * does is refuse to let the difference spread: the combination is parsed once here, and the * question of when the host is allowed to be watching is answered once here, so neither platform * gets its own opinion about either. * * should_watch() is deliberately the only gate, and deliberately asks for the session count with * no cleanup - session_count_no_cleanup() rather than session_count() - because a trigger making * a scheduling decision of its own must not enter session teardown in order to make it. * * On Linux the watcher is driven by a thread that polls that one answer and opens or closes the * shared keyboards accordingly, so the host holds nothing open while it has nothing to do. That * thread OWNS its watcher rather than reaching for a shared one, and that is not a style choice: * a namespace-scope thread is destroyed after the function-local statics it would call into, so a * shared watcher here would be joined back into an object that had already been destroyed. Owning * it means the watcher is destroyed inside the thread, before the thread finishes, and there is no * destruction order left to get wrong. * * The thread is started the first time a combination is applied rather than at some startup point * a caller has to remember, because a startup point a caller has to remember is a startup point * somebody eventually forgets. */ #include "src/quit_hotkey.h" #include #include #include #include "src/logging.h" #include "src/quit_hotkey_parse.h" #include "src/rtsp.h" #ifdef _WIN32 #include "src/platform/windows/hotkey_manager.h" #else #include "src/platform/linux/quit_hotkey_device.h" #include "src/platform/linux/quit_hotkey_linux.h" #include "src/quit_request.h" #endif namespace quit_hotkey { bool should_watch() { // The single gate. Both triggers ask this and nothing else, so they cannot disagree about // when the host is allowed to be listening. return rtsp_stream::session_count_no_cleanup() > 0; } } // namespace quit_hotkey #ifdef _WIN32 namespace quit_hotkey { namespace { // MOD_ALT, MOD_CONTROL, MOD_SHIFT and the VK_F1 base, from . Repeated rather than // included, for the same reason the Linux file repeats its evdev codes: these are ABI, and // this file has no business pulling in windows.h to state four numbers. constexpr unsigned int kModAlt = 0x0001; constexpr unsigned int kModControl = 0x0002; constexpr unsigned int kModShift = 0x0004; constexpr int kVirtualKeyF1 = 0x70; int virtual_key_for(const parse_result_t &parsed) { if (parsed.function_key >= 1 && parsed.function_key <= 24) { return kVirtualKeyF1 + (parsed.function_key - 1); } // For a letter or a digit the virtual-key code IS the character's own value, so no table // is needed here - unlike evdev, where the letters are deliberately not in alphabetical // order and a table is the only honest way to state them. return static_cast(parsed.key); } unsigned int modifiers_for(const parse_result_t &parsed) { unsigned int modifiers = 0; if (parsed.alt) { modifiers |= kModAlt; } if (parsed.ctrl) { modifiers |= kModControl; } if (parsed.shift) { modifiers |= kModShift; } return modifiers; } } // namespace void apply_config(const std::string &value) { const auto parsed = parse(value); if (!parsed.ok) { BOOST_LOG(warning) << "input_quit_hotkey " << parsed.error << " - keeping the previous setting."; return; } const int vk = virtual_key_for(parsed); if (vk <= 0) { BOOST_LOG(warning) << "input_quit_hotkey names no key Windows has a code for - keeping the previous setting."; return; } // Handing this over while idle is correct rather than a mistake: the hotkey thread holds the // reservation only while should_watch() is true, so what is stored here is a wish, not a grab. platf::hotkey::update_quit_hotkey(vk, modifiers_for(parsed)); } } // namespace quit_hotkey #else namespace quit_hotkey { namespace { /// How often the gate asks whether it should be watching, and how finely it slices that wait /// so a shutdown is prompt rather than up to half a second late. constexpr int kGatePollMs = 500; constexpr int kGateSliceMs = 50; // Declared before g_gate deliberately: namespace-scope objects are destroyed in reverse order // of construction, so this outlives the thread that locks it. std::mutex g_mutex; resolved_combo_t g_resolved{}; bool g_have_combo = false; std::jthread g_gate; /// The action, shared with the Windows trigger. Written once so neither platform can end /// something different from the other. void on_combo() { if (!quit_request::end_client_holding_shared_devices("quit hotkey")) { BOOST_LOG(info) << "Quit combo pressed, but no client could be resolved; no stream was ended."; } } /// The combination to watch for, if one has been understood yet. Copied out under the lock so /// the gate never holds the mutex while it is doing anything slow. bool current_combo(resolved_combo_t &out) { std::lock_guard lock{g_mutex}; if (!g_have_combo) { return false; } out = g_resolved; return true; } void gate_main(std::stop_token stop) { // Owned by this thread, not shared - see the file header. A shared watcher would be // destroyed before this thread is joined back into it. watcher_t watcher{&on_combo}; bool watching = false; // Said once per state, so a session with nothing shared in does not fill the log with the // same sentence twice a second. bool said_waiting = false; while (!stop.stop_requested()) { resolved_combo_t combo{}; if (!watching && current_combo(combo) && should_watch()) { const auto opened = watcher.start(combo); watching = opened > 0; if (opened == 0) { if (!said_waiting) { BOOST_LOG(info) << "Quit combo: a session is live but no keyboard has been shared in yet; waiting for one."; said_waiting = true; } } else { said_waiting = false; BOOST_LOG(info) << "Quit combo: watching " << opened << " shared keyboard(s)."; } } else if (watching && !should_watch()) { watcher.stop(); watching = false; said_waiting = false; BOOST_LOG(info) << "Quit combo: no session is live; stopped reading the shared keyboard."; } for (int waited = 0; waited < kGatePollMs && !stop.stop_requested(); waited += kGateSliceMs) { std::this_thread::sleep_for(std::chrono::milliseconds(kGateSliceMs)); } } watcher.stop(); } } // namespace void apply_config(const std::string &value) { const auto parsed = parse(value); if (!parsed.ok) { BOOST_LOG(warning) << "input_quit_hotkey " << parsed.error << " - keeping the previous setting."; return; } const auto resolved = resolve(to_combo(parsed)); if (!resolved) { BOOST_LOG(warning) << "input_quit_hotkey names a key this build has no code for - keeping the previous setting."; return; } { std::lock_guard lock{g_mutex}; g_resolved = *resolved; g_have_combo = true; } // Started on the first combination rather than at a named startup point, so there is no // ordering to get right and nothing to forget. A later change to the setting takes effect // from the next gate pass, which is at most half a second and only matters mid-session. if (!g_gate.joinable()) { g_gate = std::jthread{gate_main}; } } } // namespace quit_hotkey #endif