server/src/quit_hotkey.cpp
Rama main · réplica de lectura del repositorio original.
/**
* @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 <chrono>
#include <mutex>
#include <thread>
#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 <winuser.h>. 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<int>(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