server/src/lutris_integration.cpp
Rama main · réplica de lectura del repositorio original.
#include "lutris_integration.h"
#if defined(__linux__)
#include "provider_scan_protocol.h"
#endif
#include <algorithm>
#include <array>
#include <cctype>
#include <cerrno>
#include <chrono>
#include <cstdlib>
#include <memory>
#include <mutex>
#include <optional>
#include <sqlite3.h>
#include <string_view>
#if defined(__linux__)
#include <fcntl.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#endif
namespace platf::lutris {
namespace {
struct database_deleter {
void operator()(sqlite3 *database) const {
if (database) sqlite3_close(database);
}
};
struct statement_deleter {
void operator()(sqlite3_stmt *statement) const {
if (statement) sqlite3_finalize(statement);
}
};
using database_ptr = std::unique_ptr<sqlite3, database_deleter>;
using statement_ptr = std::unique_ptr<sqlite3_stmt, statement_deleter>;
std::string text(sqlite3_stmt *statement, int column) {
const auto *value = sqlite3_column_text(statement, column);
return value ? reinterpret_cast<const char *>(value) : std::string {};
}
std::string lower(std::string value) {
std::transform(value.begin(), value.end(), value.begin(), [](unsigned char character) {
return static_cast<char>(std::tolower(character));
});
return value;
}
std::filesystem::path first_existing(const std::filesystem::path &base,
const std::string &slug,
std::initializer_list<std::string_view> extensions) {
for (const auto extension : extensions) {
auto candidate = base / (slug + std::string(extension));
std::error_code error;
if (std::filesystem::is_regular_file(candidate, error)) return candidate;
}
return {};
}
std::filesystem::path discover_artwork(const std::filesystem::path &lutris_data,
const std::string &slug) {
if (slug.empty()) return {};
return first_existing(lutris_data / "coverart", slug, {".png", ".jpg", ".jpeg", ".webp"});
}
std::filesystem::path discover_icon(const std::filesystem::path &lutris_data,
const std::string &slug) {
if (slug.empty()) return {};
auto data_home = lutris_data.parent_path();
for (const auto size : {"256x256", "128x128", "64x64", "48x48"}) {
auto image = first_existing(data_home / "icons/hicolor" / size / "apps",
"lutris_" + slug, {".png"});
if (!image.empty()) return image;
}
return {};
}
void hash_bytes(std::uint64_t &hash, std::string_view value) {
constexpr std::uint64_t prime = 1099511628211ULL;
for (const auto byte : value) {
hash ^= static_cast<unsigned char>(byte);
hash *= prime;
}
hash ^= 0xff;
hash *= prime;
}
void hash_path(std::uint64_t &hash, const std::filesystem::path &path) {
hash_bytes(hash, path.generic_string());
if (path.empty()) return;
std::error_code error;
const auto size = std::filesystem::file_size(path, error);
hash_bytes(hash, error ? "missing" : std::to_string(size));
error.clear();
const auto modified = std::filesystem::last_write_time(path, error);
hash_bytes(hash, error ? "unstatable" : std::to_string(modified.time_since_epoch().count()));
}
#if defined(__linux__)
bool machine_host_mode() {
const auto *value = std::getenv("ARTLIGHT_MACHINE_HOST");
return value && *value;
}
std::shared_ptr<const provider_scan::lutris_catalog_t> machine_lutris_catalog() {
static std::mutex mutex;
static std::shared_ptr<const provider_scan::lutris_catalog_t> cached;
static std::chrono::steady_clock::time_point refreshed_at {};
static bool initialized = false;
constexpr auto cache_lifetime = std::chrono::seconds {2};
const auto now = std::chrono::steady_clock::now();
std::lock_guard lock {mutex};
if (!initialized || now - refreshed_at >= cache_lifetime) {
auto scanned = provider_scan::scan_lutris_session();
cached = scanned ? std::make_shared<const provider_scan::lutris_catalog_t>(std::move(*scanned)) : nullptr;
refreshed_at = now;
initialized = true;
}
return cached;
}
std::filesystem::path find_lutris_executable() {
const auto *path_value = std::getenv("PATH");
if (!path_value || !*path_value) return {};
std::string_view paths {path_value};
while (true) {
const auto separator = paths.find(':');
const auto directory = paths.substr(0, separator);
const auto candidate = (directory.empty() ? std::filesystem::current_path() : std::filesystem::path(directory)) / "lutris";
if (access(candidate.c_str(), X_OK) == 0) return candidate;
if (separator == std::string_view::npos) break;
paths.remove_prefix(separator + 1);
}
return {};
}
#endif
} // namespace
bool game_t::steam_backed() const {
return lower(runner) == "steam" || lower(service) == "steam";
}
std::filesystem::path default_database_path() {
#if defined(__linux__)
if (machine_host_mode()) return {};
const auto *xdg = std::getenv("XDG_DATA_HOME");
if (xdg && *xdg) {
const auto candidate = std::filesystem::path(xdg) / "lutris/pga.db";
std::error_code error;
if (std::filesystem::is_regular_file(candidate, error)) return candidate;
}
const auto *home = std::getenv("HOME");
if (home && *home) {
for (const auto &relative : {
std::filesystem::path(".local/share/lutris/pga.db"),
std::filesystem::path(".var/app/net.lutris.Lutris/data/lutris/pga.db")}) {
const auto candidate = std::filesystem::path(home) / relative;
std::error_code error;
if (std::filesystem::is_regular_file(candidate, error)) return candidate;
}
}
#endif
return {};
}
bool database_available() {
#if defined(__linux__)
if (machine_host_mode()) {
const auto catalog = machine_lutris_catalog();
return catalog && catalog->database_available;
}
#endif
return !default_database_path().empty();
}
bool discovery_ready() {
#if defined(__linux__)
if (machine_host_mode()) return static_cast<bool>(machine_lutris_catalog());
#endif
return true;
}
std::vector<game_t> discover(const std::filesystem::path &requested_database) {
#if defined(__linux__)
if (machine_host_mode()) {
const auto catalog = machine_lutris_catalog();
return catalog && catalog->database_available ? catalog->games : std::vector<game_t> {};
}
#endif
const auto database_path = requested_database.empty() ? default_database_path() : requested_database;
if (database_path.empty()) return {};
// SQLite expects UTF-8, including when native filesystem paths use UTF-16.
const auto database_path_utf8 = database_path.u8string();
sqlite3 *raw_database = nullptr;
if (sqlite3_open_v2(reinterpret_cast<const char *>(database_path_utf8.c_str()), &raw_database,
SQLITE_OPEN_READONLY | SQLITE_OPEN_FULLMUTEX, nullptr) != SQLITE_OK) {
if (raw_database) sqlite3_close(raw_database);
return {};
}
database_ptr database {raw_database};
sqlite3_busy_timeout(database.get(), 1000);
constexpr const char *query =
"SELECT id, name, slug, runner, platform, directory, configpath, "
"service, service_id, lastplayed, playtime "
"FROM games WHERE installed = 1 AND configpath IS NOT NULL AND configpath <> '' ORDER BY id";
sqlite3_stmt *raw_statement = nullptr;
if (sqlite3_prepare_v2(database.get(), query, -1, &raw_statement, nullptr) != SQLITE_OK) return {};
statement_ptr statement {raw_statement};
std::vector<game_t> games;
const auto lutris_data = database_path.parent_path();
while (sqlite3_step(statement.get()) == SQLITE_ROW) {
game_t game;
game.id = sqlite3_column_int64(statement.get(), 0);
if (game.id <= 0) continue;
game.name = text(statement.get(), 1);
game.slug = text(statement.get(), 2);
game.runner = text(statement.get(), 3);
game.platform = text(statement.get(), 4);
game.directory = text(statement.get(), 5);
game.config_path = text(statement.get(), 6);
game.service = text(statement.get(), 7);
game.service_id = text(statement.get(), 8);
game.last_played = sqlite3_column_int64(statement.get(), 9);
game.playtime_seconds = sqlite3_column_double(statement.get(), 10);
game.artwork_path = discover_artwork(lutris_data, game.slug);
game.icon_path = discover_icon(lutris_data, game.slug);
game.stable_id = game.steam_backed() && !game.service_id.empty() ?
"steam:" + game.service_id :
"lutris:" + std::to_string(game.id);
games.push_back(std::move(game));
}
return games;
}
std::uint64_t source_fingerprint(const std::vector<game_t> &games) {
std::uint64_t hash = 1469598103934665603ULL;
for (const auto &game : games) {
hash_bytes(hash, std::to_string(game.id));
hash_bytes(hash, game.name);
hash_bytes(hash, game.runner);
hash_bytes(hash, game.service_id);
hash_bytes(hash, game.directory.generic_string());
hash_bytes(hash, game.config_path);
hash_bytes(hash, game.session_artwork_revision);
hash_path(hash, game.artwork_path);
hash_path(hash, game.icon_path);
}
return hash;
}
std::string launch_uri(std::int64_t id) {
return id > 0 ? "lutris:rungameid/" + std::to_string(id) : std::string {};
}
bool executable_available() {
#if defined(__linux__)
if (machine_host_mode()) {
const auto catalog = machine_lutris_catalog();
return catalog && catalog->executable_available;
}
return !find_lutris_executable().empty();
#else
return false;
#endif
}
bool launch(std::int64_t id) {
#if defined(__linux__)
const auto uri = launch_uri(id);
if (uri.empty()) return false;
const bool machine_host = machine_host_mode();
const auto executable = machine_host ? std::filesystem::path {} : find_lutris_executable();
if (!machine_host && executable.empty()) return false;
int error_pipe[2] {-1, -1};
if (pipe(error_pipe) != 0 || fcntl(error_pipe[1], F_SETFD, FD_CLOEXEC) == -1) {
if (error_pipe[0] >= 0) close(error_pipe[0]);
if (error_pipe[1] >= 0) close(error_pipe[1]);
return false;
}
const auto child = fork();
if (child < 0) {
close(error_pipe[0]);
close(error_pipe[1]);
return false;
}
if (child == 0) {
close(error_pipe[0]);
const auto grandchild = fork();
if (grandchild > 0) _exit(0);
if (grandchild < 0) {
const int error = errno;
const auto ignored = write(error_pipe[1], &error, sizeof(error));
(void) ignored;
_exit(127);
}
setsid();
if (machine_host) {
const auto id_string = std::to_string(id);
execl("/usr/libexec/vibeshine/artlight-session-exec", "artlight-session-exec", "lutris", id_string.c_str(), static_cast<char *>(nullptr));
} else {
execl(executable.c_str(), executable.c_str(), uri.c_str(), static_cast<char *>(nullptr));
}
const int error = errno;
const auto ignored = write(error_pipe[1], &error, sizeof(error));
(void) ignored;
_exit(127);
}
close(error_pipe[1]);
(void) waitpid(child, nullptr, 0);
int error = 0;
const auto bytes = read(error_pipe[0], &error, sizeof(error));
close(error_pipe[0]);
return bytes == 0;
#else
(void) id;
return false;
#endif
}
} // namespace platf::lutris