Implement Socket Server Game Logic Backend

Convert CameEmulator child side to GameServer
Convert gameEmulator parent side into MySteam somewhat

Define basic protocol for client-server communication. This protocol
is added as actions in. types/Actions.h. These still need to be
finalized and implemented in yajl.

Add back method of retrieving cached owned games and
get_user_steamId3 for now because the new way is far too slow.

Improve control flow over the GameEmulator implementation.
Less inherent and more explicit behavior.

Simplify buffer logic to just use NULL terminators.
Then JSON requests can be sent as plaintext json strings.

Add some NULL checks.
This commit is contained in:
William Pierce 2019-07-21 01:18:14 -07:00
parent b8acaa6308
commit 769c645d96
16 changed files with 479 additions and 60 deletions

View File

@ -48,7 +48,11 @@
"type_traits": "cpp",
"tuple": "cpp",
"typeinfo": "cpp",
"utility": "cpp"
"utility": "cpp",
"bit": "cpp",
"map": "cpp",
"string": "cpp",
"algorithm": "cpp"
},
"C_Cpp.intelliSenseEngine": "Tag Parser"
}

View File

@ -1,10 +1,18 @@
#include "GameServerManager.h"
#include "MyGameServer.h"
// TODO: shouldn't really need MySteam inside here
#include "MySteam.h"
#include "globals.h"
#include <signal.h>
#include <iostream>
#include <unistd.h>
void handle_sigint_gameserv(int signum) {
g_steam->quit_game();
exit(0);
}
MyClientSocket*
GameServerManager::quick_server_create(AppId_t appid)
{
@ -16,7 +24,6 @@ GameServerManager::quick_server_create(AppId_t appid)
server.run();
// The server will stop running when the client sends a quit request.
exit(EXIT_SUCCESS);
}
else if (pid == -1) {
@ -26,7 +33,9 @@ GameServerManager::quick_server_create(AppId_t appid)
else {
// Parent process
// TODO watch out for zombie processes
// TODO: don't use signal; use sigaction
signal(SIGCHLD, SIG_IGN);
signal(SIGINT, handle_sigint_gameserv);
return new MyClientSocket(appid);
}
}

View File

@ -1,6 +1,12 @@
#pragma once
#include "sockets/MyClientSocket.h"
// This class is more the old GameEmulator class
// It handles
// - Creation of GameServer which acts like a Steam game
// - Creation of a client socket which the main window uses to
// communicate with that server
class GameServerManager
{
private:

View File

@ -1,4 +1,5 @@
#include "MyGameServer.h"
#include <iostream>
MyGameServer::MyGameServer(AppId_t appid) : m_appid(appid), m_socket(appid)
{
@ -9,6 +10,13 @@ void
MyGameServer::run()
{
setenv("SteamAppId", std::to_string(m_appid).c_str(), 1);
SteamAPI_Init();
if (!SteamAPI_Init()) {
std::cerr << "An error occurred launching the Steam API. Aborting." << std::endl;
std::cerr << "Make sure you are trying to run an app you own, and running with lauch.sh" << std::endl;
exit(EXIT_FAILURE);
}
m_socket.run_server();
SteamAPI_Shutdown();
}

View File

@ -4,6 +4,7 @@
#include <algorithm>
#include <dirent.h>
#include "types/Game.h"
#include "types/Actions.h"
#include "SteamAppDAO.h"
#include "GameEmulator.h"
#include "common/functions.h"
@ -47,14 +48,18 @@ MySteam::launch_game(AppId_t appID) {
// //TODO if
// emulator->init_app(appID);
if (m_ipc_socket != nullptr)
{
if (m_ipc_socket != nullptr) {
std::cerr << "I will not launch the game as one is already running" << std::endl;
return false;
}
m_ipc_socket = m_server_manager.quick_server_create(appID);
if (m_ipc_socket == nullptr) {
std::cerr << "Failed to get connection to game" << std::endl;
return false;
}
return true;
}
// => launch_game
@ -67,10 +72,15 @@ bool
MySteam::quit_game() {
// GameEmulator* emulator = GameEmulator::get_instance();
// return emulator->kill_running_app();
m_ipc_socket->kill_server();
delete m_ipc_socket;
m_ipc_socket = nullptr;
return true;
if (m_ipc_socket != nullptr) {
m_ipc_socket->kill_server();
delete m_ipc_socket;
m_ipc_socket = nullptr;
return true;
} else {
return false;
}
}
// => quit_game
@ -83,6 +93,41 @@ MySteam::quit_game() {
*/
void
MySteam::refresh_owned_apps() {
//TODO at bottom of function
const std::string path_to_cache_dir(MySteam::get_steam_install_path() + "/appcache/stats/");
DIR* dirp = opendir(path_to_cache_dir.c_str());
struct dirent * dp;
std::string filename;
const std::string prefix("UserGameStats_" + MySteam::get_user_steamId3() + "_");
const std::string input_scheme_c(prefix + "%lu.bin");
Game_t game;
unsigned long app_id;
SteamAppDAO* appDAO = SteamAppDAO::get_instance();
// The whole update will really occur only once in a while, no worries
appDAO->update_name_database();
m_all_subscribed_apps.clear();
while ((dp = readdir(dirp)) != NULL) {
filename = dp->d_name;
if(filename.rfind(prefix, 0) == 0) {
if(sscanf(dp->d_name, input_scheme_c.c_str(), &app_id) == 1) {
game.app_id = app_id;
game.app_name = appDAO->get_app_name(app_id);
m_all_subscribed_apps.push_back(game);
}
}
}
std::sort(m_all_subscribed_apps.begin(), m_all_subscribed_apps.end(), comp_app_name);
closedir(dirp);
/*
// TODO: Scanning through all apps using app_is_owned is far too slow
// It takes minutes for SAM to start up if this code is enabled
// Implement something to speed this code up before re-enabling it
Game_t game;
SteamAppDAO* appDAO = SteamAppDAO::get_instance();
@ -103,9 +148,52 @@ MySteam::refresh_owned_apps() {
}
std::sort(m_all_subscribed_apps.begin(), m_all_subscribed_apps.end(), comp_app_name);
*/
}
// => refresh_owned_apps
/**
* Could parse /home/user/.local/share/Steam/config/loginusers.vdf, but wrong id type
* Parses STEAM/logs/parental_log.txt, hoping those logs can't be disabled
* Return the most recently logged in user id
* Returns empty string on error
*/
std::string
MySteam::get_user_steamId3() {
static const std::string file_path(MySteam::get_steam_install_path() + "/logs/parental_log.txt");
std::ifstream input(file_path, std::ios::in);
std::string word;
if(!input) {
std::cerr << "Could not open " << file_path << std::endl;
std::cerr << "Make sure you have a default steam installation, and that logging is not disabled." << std::endl;
input.close();
exit(EXIT_FAILURE);
}
//We're done setting up and checking, let's parse this file
bool next_is_id = false;
std::string latest_id = "";
while(input >> word) {
if(word == "ID:") {
next_is_id = true;
continue;
}
if(next_is_id) {
latest_id = word;
next_is_id = false;
}
}
input.close();
return latest_id;
}
// => get_user_steamId3
/**
* Tries to locate the steam folder in multiple locations,
@ -160,6 +248,26 @@ MySteam::refresh_icons() {
}
// => refresh_icons
std::vector<Achievement_t>
MySteam::get_achievements() {
if (m_ipc_socket == nullptr) {
std::cerr << "Connection to game is broken" << std::endl;
exit(0);
}
m_ipc_socket->request_response(GET_ACHIEVEMENTS_STR);
//TODO: process reponse achievements
//This type might not end up being a std::vector<Achievement_t> type,
//it may just conform to the JSON types
std::vector<Achievement_t> yes;
yes.clear();
return yes;
}
/**
* Adds an achievement to the list of achievements to unlock/lock
*/

View File

@ -1,5 +1,6 @@
#pragma once
#include "types/Game.h"
#include "types/Achievement.h"
#include "GameServerManager.h"
#include "sockets/MyClientSocket.h"
#include <string>
@ -18,6 +19,14 @@ public:
*/
static MySteam* get_instance();
/**
* Returns the steamId3 of the last user who logged in on the
* machine. Make sure all logs are enabled, or this may result
* in an error.
*/
static std::string get_user_steamId3();
/**
* Returns the absolute path to the steam installation folder.
* This is not failsafe and may require some tweaking to add
@ -73,6 +82,16 @@ public:
*/
std::vector<Game_t> get_all_games_with_stats() { return m_all_subscribed_apps; };
/**
* Get achievements of the launched app
*
* For now use an Achievement_t for ease of extension
* to count-based achievements
*
* TODO: maybe don't name this the same as GameServer::get_achievements?
*/
std::vector<Achievement_t> get_achievements();
/**
* Adds a modification to be done on the launched app.
*/

View File

@ -5,6 +5,7 @@
#include <cstring>
#include <algorithm>
#include <cctype>
#include <iostream>
pid_t create_process()
{
@ -19,9 +20,29 @@ pid_t create_process()
void read_count(int fd, void *buf, size_t count)
{
size_t bytes_read;
for (size_t i = 0; i < count; i += bytes_read) {
bytes_read = read(fd, (void*)((char*)buf+i), count-i);
//bytes read per call
ssize_t bytes;
for (size_t i = 0; i < count; i += bytes) {
errno = 0;
bytes = read(fd, (void*)((char*)buf+i), count-i);
if ((bytes == -1) || (bytes == 0 && errno > 0)) {
std::cerr << "Read pipe encountered fatal error." << std::endl;
exit(EXIT_FAILURE);
}
}
}
void write_count(int fd, void *buf, size_t count)
{
//bytes written per call
ssize_t bytes;
for (size_t i = 0; i < count; i += bytes) {
errno = 0;
bytes = write(fd, (void*)((char*)buf+i), count-i);
if ((bytes == -1) || (bytes == 0 && errno > 0)) {
std::cerr << "Write pipe encountered fatal error." << std::endl;
exit(EXIT_FAILURE);
}
}
}

View File

@ -8,11 +8,14 @@
pid_t create_process();
/**
* Wrapper for read() to actually read count bytes
* instead of reading up to count bytes
* Wrapper for read/write to actually read/write count
* bytes instead of reading/writing up to count bytes,
* or fail hard on error
*/
void read_count(int fd, void *buf, size_t count);
void write_count(int fd, void *buf, size_t count);
/**
* Feed it a path to a file name, will return whether the file
* exists or not on the current machine

View File

@ -96,7 +96,16 @@ extern "C"
if( appId != 0 ) {
g_main_gui->switch_to_stats_page();
g_steam->launch_game(appId);
// get_achievements from launched child
std::vector<Achievement_t> ach_list = g_steam->get_achievements();
//TODO: populate achievements in GUI (rip out update view from GameEmulator)
/*
for(unsigned i = 0; i < m_achievement_count; i++) {
g_main_gui->add_to_achievement_list(m_achievement_list[i]);
}
g_main_gui->confirm_stats_list();
*/
} else {
std::cerr << "An error occurred figuring out which app to launch.. You can report this to the developer." << std::endl;
}

View File

@ -1,4 +1,5 @@
#include "MyClientSocket.h"
#include "../types/Actions.h"
#include <thread>
#include <chrono>
@ -47,6 +48,8 @@ MyClientSocket::request_response(std::string request)
connect_to_server();
send_message(request);
std::string ret = receive_message();
std::cerr << "client receieved" << ret << std::endl;
// need to parse this in the case of GET_ACHIEVEMENTS
disconnect();
return ret;
}
@ -60,5 +63,5 @@ MyClientSocket::disconnect()
void
MyClientSocket::kill_server()
{
request_response(END_OF_SERVICE);
request_response(QUIT_GAME_STR);
}

View File

@ -1,9 +1,164 @@
#include "MyGameSocket.h"
#include "../types/Actions.h"
MyGameSocket::MyGameSocket(AppId_t appid) :
MyServerSocket(appid),
m_CallbackUserStatsReceived( this, &MyGameSocket::OnUserStatsReceived )
{
}
std::string
MyGameSocket::process_request(std::string request) {
// Logic goes here TODO
SteamAPI_RunCallbacks();
return "{my json encoded response}";
//TODO: yajl parse the JSON-encoded request string (then get request type?) or are strings good enough?
//SAM_ACTION request_type = get_request_type(request)
std::string ret;
// switch (request_type) {
if (request == GET_ACHIEVEMENTS_STR) {
//case GET_ACHIEVEMENTS:
//this requires an async callback
std::vector<Achievement_t> achievements = get_achievements();
// Steam api is launched in this context, other possible imlementation: game_utils->get_achievements()
//ret = JSON::achievement_vector(achievements);
//break;
} else if (request == STORE_ACHIEVEMENTS_STR) {
//case STORE_ACHIEVEMENTS:
// TODO: achievement ID string, lock or relock boolean
//std::vector<std::pair<string, bool>> operations = JSON::parse_achievement_array(request);
//long term TODO: extend to stats
//process_achievements(operations); // or game_utils->process_achievements(..)
//ret = SAM_ACK_BUT_IN_JSON;
//break;
} else if (request == QUIT_GAME_STR) {
//case QUIT_GAME:
SteamAPI_Shutdown();
} else {
//default:
std::cerr << "Invalid command" << std::endl;
ret.clear();
}
return ret;
}
std::vector<Achievement_t>
MyGameSocket::get_achievements() {
m_stats_callback_received = false;
ISteamUserStats *stats_api = SteamUserStats();
if (!stats_api->RequestCurrentStats()) {
std::cerr << "ERROR: User not logged in, exiting" << std::endl;
exit(EXIT_FAILURE);
}
while (!m_stats_callback_received) {
// for debugging how long steam callbacks take
//std::cerr << "waiting for callback" << std::endl;
std::this_thread::sleep_for(std::chrono::milliseconds(500));
SteamAPI_RunCallbacks();
}
return m_achievement_list;
}
void
MyGameSocket::OnUserStatsReceived(UserStatsReceived_t *callback) {
// Check if we received the values for the correct app
if (std::string(getenv("SteamAppId")) == std::to_string(callback->m_nGameID)) {
if ( k_EResultOK == callback->m_eResult ) {
ISteamUserStats *stats_api = SteamUserStats();
// ==============================
// RETRIEVE IDS
// ==============================
const unsigned num_ach = stats_api->GetNumAchievements();
if (num_ach == 0) {
std::cerr << "No achievements for current game" << std::endl;
}
m_achievement_list.clear();
m_achievement_list.reserve(num_ach);
for (unsigned i = 0; i < num_ach ; i++) {
// TODO: strncpy is slow, because it fills the remaining space with NULLs
// This is last stage optimisation but, could have used strcpy, or sprintf,
// making sure strings are NULL terminated
// see "man strncpy" for a possible implementation
strncpy(
m_achievement_list[i].id,
stats_api->GetAchievementName(i),
MAX_ACHIEVEMENT_ID_LENGTH);
strncpy(
m_achievement_list[i].name,
stats_api->GetAchievementDisplayAttribute(m_achievement_list[i].id, "name"),
MAX_ACHIEVEMENT_NAME_LENGTH);
strncpy(
m_achievement_list[i].desc,
stats_api->GetAchievementDisplayAttribute(m_achievement_list[i].id, "desc"),
MAX_ACHIEVEMENT_DESC_LENGTH);
// TODO
// https://partner.steamgames.com/doc/api/ISteamUserStats#RequestGlobalAchievementPercentages
//stats_api->GetAchievementAchievedPercent(m_achievement_list[i].id, &(m_achievement_list[i].global_achieved_rate));
m_achievement_list[i].global_achieved_rate = 0;
stats_api->GetAchievement(m_achievement_list[i].id, &(m_achievement_list[i].achieved));
m_achievement_list[i].hidden = (bool)strcmp(stats_api->GetAchievementDisplayAttribute( m_achievement_list[i].id, "hidden" ), "0");
m_achievement_list[i].icon_handle = stats_api->GetAchievementIcon( m_achievement_list[i].id );
}
m_stats_callback_received = true;
} else {
std::cerr << "Received stats for the game, but an error occurrred." << std::endl;
}
} else {
std::cerr << "Received stats for wrong game" << std::endl;
}
}
void
MyGameSocket::process_achievements(std::vector<std::pair<std::string, bool>> changes) {
//Untested
ISteamUserStats *stats_api = SteamUserStats();
for (unsigned i = 0; i < changes.size(); i++)
{
const char* achievement_id = changes[i].first.c_str();
if (changes[i].second) {
// We want to unlock an achievement
if (!stats_api->SetAchievement(achievement_id)) {
std::cerr << "Unlocking achievement " << achievement_id << " failed " << std::endl;
} else {
std::cerr << "Unlocked achievement " << achievement_id << std::endl;
}
} else {
// We want to relock an achievement
if (!stats_api->ClearAchievement(achievement_id)) {
std::cerr << "Relocking achievement " << achievement_id << " failed" << std::endl;
} else {
std::cerr << "Relocked achievement " << achievement_id << std::endl;
}
}
//TODO: stats
}
// Auto-commit after we receive everything
if (!stats_api->StoreStats()) {
std::cerr << "Committing changes failed" << std::endl;
}
}

View File

@ -1,10 +1,32 @@
#pragma once
#include "MyServerSocket.h"
#include "../types/Achievement.h"
#include "../../steam/steam_api.h"
#include <iostream>
#include <string>
#include <utility>
#include <vector>
#include <thread>
#include <atomic>
class MyGameSocket : public MyServerSocket
{
public:
std::string process_request(std::string request);
using MyServerSocket::MyServerSocket;
std::vector<Achievement_t> get_achievements(void);
void process_achievements(std::vector<std::pair<std::string, bool>> changes);
MyGameSocket(AppId_t appid);
/**
* Steam API callback to handle the received stats and achievements
*/
STEAM_CALLBACK( MyGameSocket, OnUserStatsReceived, UserStatsReceived_t, m_CallbackUserStatsReceived );
private:
std::atomic_bool m_stats_callback_received;
std::vector<Achievement_t> m_achievement_list;
};

View File

@ -1,6 +1,8 @@
#include "MyServerSocket.h"
#include "../common/functions.h"
#include "../types/Actions.h"
#include <iostream>
MyServerSocket::MyServerSocket(AppId_t appid) : MySocket(appid)
@ -10,7 +12,7 @@ MyServerSocket::MyServerSocket(AppId_t appid) : MySocket(appid)
{
std::cerr << "It looks like the server before me did not shutdown properly." << std::endl;
if(unlink(m_socket_path.c_str()) < 0) {
std::cout << "Something is wrong. Are you the right user? Exitting." << std::endl;
std::cout << "Something is wrong. Are you the right user? Exiting." << std::endl;
exit(EXIT_FAILURE);
}
}
@ -18,7 +20,7 @@ MyServerSocket::MyServerSocket(AppId_t appid) : MySocket(appid)
errno = 0;
if ((m_socket_fd = socket(AF_UNIX, SOCK_SEQPACKET, 0)) == -1)
{
std::cerr << "Could not create the server socket. Exitting. Code: " << errno << std::endl;
std::cerr << "Could not create the server socket. Exiting. Code: " << errno << std::endl;
exit(EXIT_FAILURE);
}
@ -34,7 +36,7 @@ MyServerSocket::MyServerSocket(AppId_t appid) : MySocket(appid)
if (listen(m_socket_fd, 20) < 0)
{
std::cerr << "Unable to listen to the socket. Exitting." << std::endl;
std::cerr << "Unable to listen to the socket. Exiting." << std::endl;
exit(EXIT_FAILURE);
}
@ -59,16 +61,24 @@ MyServerSocket::run_server()
for (;;) {
/* Wait for incoming connection. */
data_socket = accept(m_socket_fd, NULL, NULL);
std::cerr << "Received connection" << std:: endl;
if (data_socket == -1) {
std::cerr << "Server failed to accept. Exitting." << std::endl;
std::cerr << "Server failed to accept. Exiting." << std::endl;
exit(EXIT_FAILURE);
}
// Read all the client's request
std::string request = receive_message(data_socket);
if (request == END_OF_SERVICE)
std::cerr << "Server received request: " << request << std:: endl;
send_message(data_socket, process_request(request));
if (request == QUIT_GAME_STR)
{
std::cout << "shutting down" << request << std:: endl;
send_message(data_socket, "SAM_ACK");
close(data_socket);
close(m_socket_fd);
@ -76,8 +86,6 @@ MyServerSocket::run_server()
break;
}
send_message(data_socket, process_request(request));
/* Close socket. */
close(data_socket);
}

View File

@ -1,6 +1,8 @@
#include "MySocket.h"
#include "../globals.h"
#include "../common/functions.h"
#include <iostream>
MySocket::MySocket(AppId_t appid) : m_appid(appid), m_socket_fd(-1)
@ -23,25 +25,24 @@ std::string
MySocket::receive_message(const int fd)
{
std::string ret("");
char buffer[BUFFER_SIZE];
char buffer[BUFFER_SIZE+1];
/* Ensure buffer is 0-terminated. */
buffer[BUFFER_SIZE] = '\0';
for (;;) {
/* Wait for next data packet. */
if (read(fd, buffer, BUFFER_SIZE) == -1) {
std::cerr << "Socket could not read." << std::endl;
exit(EXIT_FAILURE);
}
read_count(fd, buffer, BUFFER_SIZE);
//std::cerr << " received packet: " << buffer << std::endl;
ret += std::string(buffer);
/* Ensure buffer is 0-terminated. */
buffer[BUFFER_SIZE - 1] = 0;
if (!strncmp(buffer, END_OF_MESSAGE, BUFFER_SIZE)) {
if (strlen(buffer) < BUFFER_SIZE) {
// Got a NULL in the actual buffer, so must be the end
// of the string
break;
}
ret += std::string(buffer);
}
//std::cerr << "PID: " << getpid() << " received message: " << ret << std::endl;
return ret;
}
@ -54,29 +55,19 @@ MySocket::send_message(const std::string message)
void
MySocket::send_message(const int fd, const std::string message)
{
char buffer[BUFFER_SIZE];
char buffer[BUFFER_SIZE+1]; //+1 for printing chunks
buffer[BUFFER_SIZE] = '\0';
unsigned iterator = 0;
//std::cerr << "PID: " << getpid() << " sending message: " << message << std::endl;
for (;;) {
strncpy(buffer, message.substr(iterator, BUFFER_SIZE - 1).c_str(), BUFFER_SIZE - 1);
strncpy(buffer, message.substr(iterator, BUFFER_SIZE).c_str(), BUFFER_SIZE);
//std::cerr << " sending packet: " << buffer << std::endl;
write_count(fd, buffer, BUFFER_SIZE);
std::cerr << getpid() << " sending packet: " << buffer << std::endl;
if (write(fd, buffer, strlen(buffer) + 1) == -1) {
std::cerr << getpid() << ": there was an error writing the message." << std::endl;
exit(EXIT_FAILURE);
}
iterator += BUFFER_SIZE - 1;
if (iterator > message.size())
{
// Close the request
strcpy(buffer, END_OF_MESSAGE);
if (write(fd, buffer, strlen(buffer) + 1) == -1) {
std::cerr << "There was an error closing the message." << std::endl;
exit(EXIT_FAILURE);
}
iterator += BUFFER_SIZE;
if (iterator > message.size()) {
break;
}
}

View File

@ -6,8 +6,9 @@
#include <sys/un.h>
#define BUFFER_SIZE 12
#define END_OF_MESSAGE "SAM_STOP"
#define END_OF_SERVICE "SAM_QUIT"
// Strings are the fundamental unit of transmission,
// so just use NULL terminator as end of message
class MySocket
{

52
src/types/Actions.h Normal file
View File

@ -0,0 +1,52 @@
// Message format shall be
// all GET format
#define GET_ACHIEVEMENTS_STR "GET_ACHIEVEMENTS"
#define STORE_ACHIEVEMENTS_STR "STORE_ACHIEVEMENTS"
#define QUIT_GAME_STR "QUIT_GAME"
// TODO: Add SAM_START as an action to this too?
// Would require more reorg of code structure
enum SAM_ACTION {
GET_ACHIEVEMENTS,
STORE_ACHIEVEMENTS,
QUIT_GAME,
INVALID
};
// support for the full achievement type shall be added
// for now just implement achieved/not achieved
/* JSON format shall be
messages are sent as plaintext strings
messages are delimited by the usual string NULL terminator
get all achievements for active game
{
GET_ACHIEVEMENTS_STR
}
response
{
int NUM_ACH //maybe not necessary if we have .length function?
[{
str ACH_NAME
bool ACHIEVED
}]
}
store a list of achievement changes
{
int NUM_ACH //maybe not necessary if we have .length function?
[{
str ACH_NAME
bool ACHIEVED
}]
}
quit active game
{
SAM_QUIT_STR
}
*/