/** * @file src/session_history.cpp * @brief Public facade for SQLite-backed session history persistence. */ // standard includes #include #include #include // local includes #include "session_history.h" #include "config.h" #include "host_stats.h" #include "logging.h" #include "rtsp.h" #include "stream.h" #include "session_history_sampler.h" #include "session_history_storage.h" #include "session_history_storage_diagnostics.h" #include "session_history_writer.h" #ifdef _WIN32 #include "platform/windows/display.h" #endif namespace session_history { namespace { std::mutex g_lifecycle_mutex; std::string g_history_db_path; writer::settings_t current_writer_settings() { writer::settings_t settings; settings.enabled = config::sunshine.session_history_enabled; settings.ttl_days = std::max(config::sunshine.session_history_ttl_days, 0); settings.max_db_size_bytes = config::sunshine.session_history_db_size_limit_mb > 0 ? static_cast(config::sunshine.session_history_db_size_limit_mb) * 1024ull * 1024ull : 0ull; return settings; } bool history_available() { return writer::is_enabled() && writer::is_available(); } void log_storage_diagnostic(storage::diagnostics::level_e level, const std::string &message) { switch (level) { case storage::diagnostics::level_e::debug: BOOST_LOG(debug) << message; break; case storage::diagnostics::level_e::info: BOOST_LOG(info) << message; break; case storage::diagnostics::level_e::warning: BOOST_LOG(warning) << message; break; case storage::diagnostics::level_e::error: BOOST_LOG(error) << message; break; } } } // namespace void init(const std::string &db_path) { std::scoped_lock lk {g_lifecycle_mutex}; storage::diagnostics::set_sink(log_storage_diagnostic); g_history_db_path = db_path; writer::update_settings(current_writer_settings()); writer::init(g_history_db_path); if (writer::is_available()) { sampler::init(); } } void shutdown() { std::scoped_lock lk {g_lifecycle_mutex}; BOOST_LOG(info) << "session_history: shutting down"; sampler::shutdown(); writer::shutdown(); g_history_db_path.clear(); BOOST_LOG(info) << "session_history: shut down"; } void reload_settings() { std::scoped_lock lk {g_lifecycle_mutex}; const auto settings = current_writer_settings(); writer::update_settings(settings); if (!settings.enabled) { sampler::shutdown(); writer::shutdown(); return; } if (!g_history_db_path.empty()) { writer::init(g_history_db_path); } if (writer::is_available()) { sampler::init(); (void) writer::enqueue_prune(); } } void begin_session(const session_metadata_t &metadata) { if (!history_available()) { return; } BOOST_LOG(info) << "session_history: begin_session uuid=" << metadata.uuid << " protocol=" << metadata.protocol; session_metadata_t enriched = metadata; if (enriched.host_cpu_model.empty() || enriched.host_gpu_model.empty()) { const auto &info = host_stats::info(); if (enriched.host_cpu_model.empty()) { enriched.host_cpu_model = info.cpu_model; } if (enriched.host_gpu_model.empty()) { enriched.host_gpu_model = info.gpu_model; } } #ifdef _WIN32 if (enriched.stream_gpu_model.empty()) { enriched.stream_gpu_model = platf::dxgi::current_display_adapter_name(); } #endif enriched.codec = stream::canonical_codec_name(enriched.codec); sampler::register_session(enriched); if (!writer::enqueue_begin(enriched)) { BOOST_LOG(error) << "session_history: failed to queue begin_session for uuid=" << enriched.uuid; } session_event_t event; event.session_uuid = enriched.uuid; event.timestamp_unix = storage::now_unix(); event.event_type = "stream_started"; if (!writer::enqueue_event(event)) { BOOST_LOG(error) << "session_history: failed to queue stream_started event for uuid=" << enriched.uuid; } } void end_session(const std::string &uuid) { sampler::unregister_session(uuid); if (!history_available()) { return; } BOOST_LOG(info) << "session_history: end_session uuid=" << uuid; session_event_t event; event.session_uuid = uuid; event.timestamp_unix = storage::now_unix(); event.event_type = "stream_ended"; if (!writer::enqueue_event(event)) { BOOST_LOG(error) << "session_history: failed to queue stream_ended event for uuid=" << uuid; } if (!writer::enqueue_end(uuid)) { BOOST_LOG(error) << "session_history: failed to queue end_session for uuid=" << uuid; } (void) writer::enqueue_prune(); } void record_event(const std::string &uuid, const std::string &event_type, const std::string &payload) { if (!history_available()) { return; } session_event_t event; event.session_uuid = uuid; event.timestamp_unix = storage::now_unix(); event.event_type = event_type; event.payload = payload; if (!writer::enqueue_event(event)) { BOOST_LOG(error) << "session_history: failed to queue event '" << event_type << "' for uuid=" << uuid; } } std::vector list_sessions(int limit, int offset) { return writer::list_sessions(limit, offset); } std::optional get_session_detail(const std::string &uuid, bool include_all) { return writer::get_session_detail(uuid, include_all); } std::vector get_active_sessions() { return sampler::get_active_sessions(); } history_status_t get_history_status() { return writer::get_status(); } delete_result_e delete_session(const std::string &uuid) { if (!history_available()) { return delete_result_e::unavailable; } if (sampler::is_active(uuid)) { return delete_result_e::active_session; } return writer::delete_session(uuid); } #ifdef SUNSHINE_TESTS bool record_sample_for_tests(const session_sample_t &sample) { return writer::enqueue_sample(sample); } bool set_session_end_time_for_tests(const std::string &uuid, double end_time_unix) { return writer::set_session_end_time_for_tests(uuid, end_time_unix); } void configure_retention_for_tests(bool enabled, int ttl_days, std::uint64_t max_db_size_bytes) { writer::settings_t settings; settings.enabled = enabled; settings.ttl_days = ttl_days; settings.max_db_size_bytes = max_db_size_bytes; writer::update_settings(settings); } bool prune_now_for_tests() { return writer::prune_now_for_tests(); } bool force_write_failure_for_tests() { return writer::force_write_failure_for_tests(); } void configure_queue_limits_for_tests( std::size_t priority_limit, std::size_t regular_limit, std::size_t sample_limit, std::size_t sample_batch_size) { writer::configure_queue_limits_for_tests(priority_limit, regular_limit, sample_limit, sample_batch_size); } void reset_queue_limits_for_tests() { writer::reset_queue_limits_for_tests(); } #endif } // namespace session_history