Skip to content

Commit

Permalink
organize, reimplement saved hosts, & disable stacktrace on macOS
Browse files Browse the repository at this point in the history
  • Loading branch information
radj307 committed Mar 2, 2024
1 parent ae1f88b commit 7665c96
Show file tree
Hide file tree
Showing 6 changed files with 323 additions and 162 deletions.
155 changes: 136 additions & 19 deletions ARRCON/ARRCON.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
#include "config.hpp"
#include "helpers/print_input_prompt.h"
#include "helpers/bukkit-colors.h"
#include "helpers/FileLocator.hpp"

// 307lib
#include <opt3.hpp> //< for commandline argument parser & manager
Expand Down Expand Up @@ -41,10 +42,10 @@ struct print_help {
<< " -H, --host <Host> RCON Server IP/Hostname." << '\n'//" (Default: \"" /*<< Global.DEFAULT_TARGET.hostname*/ << "\")" << '\n'
<< " -P, --port <Port> RCON Server Port." << '\n'//" (Default: \"" /*<< Global.DEFAULT_TARGET.port*/ << "\")" << '\n'
<< " -p, --pass <Pass> RCON Server Password." << '\n'
//<< " -S, --saved <Host> Use a saved host's connection information, if it isn't overridden by arguments." << '\n'
//<< " --save-host <H> Create a new saved host named \"<H>\" using the current [Host/Port/Pass] value(s)." << '\n'
//<< " --remove-host <H> Remove an existing saved host named \"<H>\" from the list, then exit." << '\n'
//<< " -l, --list-hosts Show a list of all saved hosts, then exit." << '\n'
<< " -S, --saved <Host> Use a saved host's connection information, if it isn't overridden by arguments." << '\n'
<< " --save-host <H> Create a new saved host named \"<H>\" using the current [Host/Port/Pass] value(s)." << '\n'
<< " --remove-host <H> Remove an existing saved host named \"<H>\" from the list, then exit." << '\n'
<< " -l, --list-hosts Show a list of all saved hosts, then exit." << '\n'
<< '\n'
<< "OPTIONS:\n"
<< " -h, --help Show this help display, then exits." << '\n'
Expand Down Expand Up @@ -77,17 +78,25 @@ int main(const int argc, char** argv)
} catch (std::exception const& ex) {
std::cerr << csync.get_fatal() << ex.what() << std::endl;

#ifndef OS_MAC //< see exceptions.hpp for more information on MacOS stacktrace compat

// try getting a stacktrace for this exception and print it
if (const auto* trace = boost::get_error_info<traced_exception>(ex))
std::cerr << "Stacktrace:\n" << *trace;

#endif

return 1;
} catch (...) {
std::cerr << csync.get_fatal() << "An undefined error occurred!" << std::endl;
return 1;
}
}

static constexpr char const* const DEFAULT_TARGET_HOST{ "127.0.0.1" };
static constexpr char const* const DEFAULT_TARGET_PORT{ "27015" };
static constexpr char const* const DEFAULT_TARGET_PASS{ "" };

int main_impl(const int argc, char** argv)
{
const opt3::ArgManager args{ argc, argv,
Expand All @@ -105,11 +114,11 @@ int main_impl(const int argc, char** argv)

// get the executable's location & name
const auto& [programPath, programName] { env::PATH().resolve_split(argv[0]) };
config::Locator locator{ programPath, std::filesystem::path{ programName }.replace_extension() };
FileLocator locator{ programPath, std::filesystem::path{ programName }.replace_extension() };

/// setup the log
// log file stream
std::ofstream logfs(locator.from_extension(".log"));
std::ofstream logfs{ locator.from_extension(".log") };
// log manager object
Logger logManager{ logfs.rdbuf() };

Expand All @@ -134,27 +143,135 @@ int main_impl(const int argc, char** argv)
// -n|--no-color
csync.setEnabled(!args.check_any<opt3::Flag, opt3::Option>('n', "no-color"));

// TODO: Move this elsewhere vvv
static constexpr char const* const DEFAULT_TARGET_HOST{ "127.0.0.1" };
static constexpr char const* const DEFAULT_TARGET_PORT{ "27015" };
static constexpr char const* const DEFAULT_TARGET_PASS{ "" };
/// Select a target server & operate on the hosts file
const auto hostsfile_path{ locator.from_extension(".hosts") };
std::optional<config::SavedHosts> hostsfile;

// --rm-host|--remove-host
if (const auto& arg_removeHost{ args.getv_any<opt3::Option>("rm-host", "remove-host") }; arg_removeHost.has_value()) {
if (!std::filesystem::exists(hostsfile_path))
throw make_exception("The hosts file hasn't been created yet. (Use \"--save-host\" to create one)");

// load the hosts file
ini::INI ini(hostsfile_path);

// remove the specified entry
if (const auto it{ ini.find(arg_removeHost.value()) }; it != ini.end())
ini.erase(it);
else
throw make_exception("The specified saved host \"", arg_removeHost.value(), "\" doesn't exist! (Use \"--list-hosts\" to see a list of saved hosts.)");

// save the hosts file
if (ini.write(hostsfile_path)) {
std::cout << "Successfully removed \"" << csync(color::yellow) << arg_removeHost.value() << csync() << "\" from the hosts list.\n";
return 0;
}
else throw make_exception("Failed to save hosts file to ", hostsfile_path, '!');
}
// --list-hosts
else if (args.check_any<opt3::Option>("list-hosts", "list-host")) {
if (!std::filesystem::exists(hostsfile_path))
throw make_exception("The hosts file hasn't been created yet. (Use \"--save-host\" to create one)");

// load the hosts file
if (!hostsfile.has_value())
hostsfile = config::SavedHosts(hostsfile_path);

if (hostsfile->empty())
throw make_exception("The hosts file doesn't have any entries yet. (Use \"--save-host\" to create one)");

// if quiet was specified, get the length of the longest saved host name
size_t longestNameLength{};
if (quiet) {
for (const auto& [name, _] : *hostsfile) {
if (name.size() > longestNameLength)
longestNameLength = name.size();
}
}

// print out the hosts list
for (const auto& [name, info] : *hostsfile) {
if (!quiet) {
std::cout
<< csync(color::yellow) << name << csync() << '\n'
<< " Hostname: \"" << info.host << "\"\n"
<< " Port: \"" << info.port << "\"\n"
;
}
else {
std::cout
<< csync(color::yellow) << name << csync()
<< indent(longestNameLength + 2, name.size())
<< "( " << info.host << ':' << info.port << " )\n"
;
}
}

return 0;
}

/// get the target connection info:
net::rcon::target_info target{ DEFAULT_TARGET_HOST, DEFAULT_TARGET_PORT, DEFAULT_TARGET_PASS };

// -S|--saved|--server
if (const auto& arg_saved{ args.getv_any<opt3::Flag, opt3::Option>('S', "saved", "server") }; arg_saved.has_value()) {
if (!std::filesystem::exists(hostsfile_path))
throw make_exception("The hosts file hasn't been created yet. (Use \"--save-host\" to create one)");

// load the hosts file
if (!hostsfile.has_value())
hostsfile = config::SavedHosts(hostsfile_path);

// get the target server info
const std::string target_host{ args.getv_any<opt3::Flag, opt3::Option>('H', "host", "hostname").value_or(DEFAULT_TARGET_HOST) };
const std::string target_port{ args.getv_any<opt3::Flag, opt3::Option>('P', "port").value_or(DEFAULT_TARGET_PORT) };
const std::string target_pass{ args.getv_any<opt3::Flag, opt3::Option>('p', "pass", "password").value_or(DEFAULT_TARGET_PASS) };
// try getting the specified saved target's info
if (const auto savedTarget{ hostsfile->get_host(arg_saved.value()) }; savedTarget.has_value()) {
target = savedTarget.value();
}
else throw make_exception("The specified saved host \"", arg_saved.value(), "\" doesn't exist! (Use \"--list-hosts\" to see a list of saved hosts.)");
}
// -H|--host|--hostname
if (const auto& arg_hostname{ args.getv_any<opt3::Flag, opt3::Option>('H', "host", "hostname") }; arg_hostname.has_value())
target.host = arg_hostname.value();
// -P|--port
if (const auto& arg_port{ args.getv_any<opt3::Flag, opt3::Option>('P', "port") }; arg_port.has_value())
target.port = arg_port.value();
// -p|--pass|--password
if (const auto& arg_password{ args.getv_any<opt3::Flag, opt3::Option>('p', "pass", "password") }; arg_password.has_value())
target.pass = arg_password.value();

// --save-host
if (const auto& arg_saveHost{ args.getv_any<opt3::Option>("save-host") }; arg_saveHost.has_value()) {
// load the hosts file
if (!hostsfile.has_value()) {
hostsfile = std::filesystem::exists(hostsfile_path)
? config::SavedHosts(hostsfile_path)
: config::SavedHosts();
}

// TODO: Improve feedback when target already exists, maybe prompt the user if changes are going to be made
// set the target
(*hostsfile)[arg_saveHost.value()] = target;

// write to disk
ini::INI ini;
hostsfile->export_to(ini);
if (ini.write(hostsfile_path)) {
std::cout << "Successfully added \"" << csync(color::yellow) << arg_saveHost.value() << csync() << "\" to the hosts list.\n";
return 0;
}
else throw make_exception("Failed to save hosts file to ", hostsfile_path, '!');
}

// validate & log the target host information
std::clog << MessageHeader(LogLevel::Info) << "Target Host: \"" << target_host << ':' << target_port << '\"' << std::endl;
std::clog << MessageHeader(LogLevel::Info) << "Target Host: \"" << target.host << ':' << target.port << '\"' << std::endl;

// initialize and connect the client
net::rcon::RconClient client{ target_host, target_port };
net::rcon::RconClient client{ target.host, target.port };

// -t|--timeout
client.set_timeout(args.castgetv_any<int, opt3::Flag, opt3::Option>([](auto&& arg) { return str::stoi(std::forward<decltype(arg)>(arg)); }, 't', "timeout").value_or(3000));

// authenticate with the server
if (!client.authenticate(target_pass)) {
if (!client.authenticate(target.pass)) {
throw make_exception("Authentication failed due to incorrect password!");
}

Expand Down Expand Up @@ -196,7 +313,7 @@ int main_impl(const int argc, char** argv)

if (!quiet) {
if (!disablePromptAndEcho) // print the shell prompt
print_input_prompt(std::cout, target_host, csync);
print_input_prompt(std::cout, target.host, csync);
// echo the command
std::cout << command << '\n';
}
Expand All @@ -220,7 +337,7 @@ int main_impl(const int argc, char** argv)
// interactive mode input loop
while (true) {
if (!quiet && !disablePromptAndEcho) // print the shell prompt
print_input_prompt(std::cout, target_host, csync);
print_input_prompt(std::cout, target.host, csync);

// get user input
std::string str;
Expand Down
2 changes: 0 additions & 2 deletions ARRCON/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
# ARRCON/ARRCON
cmake_minimum_required (VERSION 3.22)

file(GLOB_RECURSE HEADERS
RELATIVE "${CMAKE_CURRENT_SOURCE_DIR}"
CONFIGURE_DEPENDS
Expand Down
118 changes: 76 additions & 42 deletions ARRCON/config.hpp
Original file line number Diff line number Diff line change
@@ -1,63 +1,97 @@
#pragma once
// 307lib
#include <env.hpp>
#include "logging.hpp"
#include "net/target_info.hpp"

// 307lib::filelib
#include <simpleINI.hpp> //< for ini::INI

// STL
#include <filesystem> //< for std::filesystem::path
#include <simpleINI.hpp>//< for ini::INI
#include <filesystem> //< for std::filesystem::path
#include <map> //< for std::map

namespace config {
inline constexpr const auto HEADER_APPERANCE{ "appearance" };
inline constexpr const auto HEADER_TIMING{ "timing" };
inline constexpr const auto HEADER_TARGET{ "target" };
inline constexpr const auto HEADER_MISC{ "miscellaneous" };

/**
* @class Locator
* @brief Used to locate ARRCON's config files.
*/
class Locator {
std::filesystem::path program_location;
std::string name_no_ext;
std::filesystem::path env_path;
std::filesystem::path home_path;
class SavedHosts {
using target_info = net::rcon::target_info;
using map = std::map<std::string, target_info>;

map hosts;

public:
Locator(const std::filesystem::path& program_dir, const std::string& program_name_no_extension) :
program_location{ program_dir },
name_no_ext{ program_name_no_extension },
env_path{ env::getvar(name_no_ext + "_CONFIG_DIR").value_or("") },
home_path{ env::get_home() }
SavedHosts() = default;
SavedHosts(ini::INI const& ini)
{
import_from(ini);
}
SavedHosts(std::filesystem::path const& path) : SavedHosts(ini::INI(path)) {}

auto begin() const { return hosts.begin(); }
auto end() const { return hosts.end(); }
bool empty() const noexcept { return hosts.empty(); }
size_t size() const noexcept { return hosts.size(); }

void import_from(ini::INI const& ini)
{
if (ini.contains("")) {
// warn about global keys
const auto globalKeysCount{ ini.at("").size() };
std::clog << MessageHeader(LogLevel::Warning) << "Hosts file contains " << globalKeysCount << " key" << (globalKeysCount == 1 ? "" : "s") << " that aren't associated with a saved host!" << std::endl;
}

// enumerate entries
for (const auto& [entryKey, entryContent] : ini) {
// enumerate key-value pairs
for (const auto& [key, value] : entryContent) {
const std::string keyLower{ str::tolower(key) };

if (str::equalsAny<false>(keyLower, "sHost")) {
hosts[entryKey].host = value;

std::clog << MessageHeader(LogLevel::Trace) << '[' << entryKey << ']' << " Imported hostname \"" << value << '\"' << std::endl;
}
else if (str::equalsAny<false>(keyLower, "sPort")) {
hosts[entryKey].port = value;

std::clog << MessageHeader(LogLevel::Trace) << '[' << entryKey << ']' << " Imported port \"" << value << '\"' << std::endl;
}
else if (str::equalsAny<false>(keyLower, "sPass")) {
hosts[entryKey].pass = value;

std::clog << MessageHeader(LogLevel::Trace) << '[' << entryKey << ']' << " Imported password \"" << std::string(value.size(), '*') << '\"' << std::endl;
}
else {
std::clog << MessageHeader(LogLevel::Warning) << '[' << entryKey << ']' << " Skipped unrecognized key \"" << key << "\"" << std::endl;
}
}
}
}
Locator(const std::filesystem::path& program_dir, const std::filesystem::path& program_name_no_extension) :
Locator(program_dir, program_name_no_extension.generic_string())
void export_to(ini::INI& ini) const
{
for (const auto& [name, info] : hosts) {
ini[name] = ini::Section{
std::make_pair("sHost", info.host),
std::make_pair("sPort", info.port),
std::make_pair("sPass", info.pass),
};

std::clog << MessageHeader(LogLevel::Trace) << '[' << name << ']' << " was exported successfully." << std::endl;
}
}

/**
* @brief Retrieves the target location of the given file extension appended to the program name. (Excluding extension, if applicable.)
* @param ext The file extension of the target file.
* @returns std::filesystem::path
*\n This is NOT guaranteed to exist! If no valid config file was found, the .config directory in the user's home directory is returned.
*/
std::filesystem::path from_extension(const std::string& ext) const
std::optional<target_info> get_host(std::string const& name) const
{
if (ext.empty())
throw make_exception("Empty extension passed to Locator::from_extension()!");
std::string target{ name_no_ext + ((ext.front() != '.') ? ("." + ext) : ext) };
std::filesystem::path path;
// 1: check the environment
if (!env_path.empty()) {
path = env_path / target;
return path;
if (const auto& it{ hosts.find(name) }; it != hosts.end()) {
return it->second;
}
// 2: check the program directory. (support portable versions by checking this before the user's home dir)
if (path = program_location / target; std::filesystem::exists(path))
return path;
// 3: user's home directory:
path = home_path / ".config" / name_no_ext / target;
return path; // return even if it doesn't exist
else return std::nullopt;
}

auto& operator[](std::string const& name)
{
return hosts[name];
}
};
}
Loading

0 comments on commit 7665c96

Please sign in to comment.