Mercurial > irccd
changeset 348:24b1709093e7
Irccdctl: resurrect
author | David Demelier <markand@malikania.fr> |
---|---|
date | Sun, 13 Nov 2016 14:16:58 +0100 |
parents | ec43b9ac3df7 |
children | 56475d6c1c30 |
files | irccd/main.cpp irccdctl/CMakeLists.txt irccdctl/alias.cpp irccdctl/alias.hpp irccdctl/cli.cpp irccdctl/main.cpp |
diffstat | 6 files changed, 783 insertions(+), 9 deletions(-) [+] |
line wrap: on
line diff
--- a/irccd/main.cpp Sun Nov 13 10:41:28 2016 +0100 +++ b/irccd/main.cpp Sun Nov 13 14:16:58 2016 +0100 @@ -275,6 +275,8 @@ instance = std::make_unique<Irccd>(); instance->commands().add(std::make_unique<command::PluginConfigCommand>()); + instance->commands().add(std::make_unique<command::PluginInfoCommand>()); + instance->commands().add(std::make_unique<command::PluginListCommand>()); instance->commands().add(std::make_unique<command::PluginReloadCommand>()); instance->commands().add(std::make_unique<command::PluginUnloadCommand>()); instance->commands().add(std::make_unique<command::ServerChannelModeCommand>());
--- a/irccdctl/CMakeLists.txt Sun Nov 13 10:41:28 2016 +0100 +++ b/irccdctl/CMakeLists.txt Sun Nov 13 14:16:58 2016 +0100 @@ -20,6 +20,8 @@ set( SOURCES ${irccdctl_SOURCE_DIR}/CMakeLists.txt + ${irccdctl_SOURCE_DIR}/alias.cpp + ${irccdctl_SOURCE_DIR}/alias.hpp ${irccdctl_SOURCE_DIR}/cli.cpp ${irccdctl_SOURCE_DIR}/cli.hpp ${irccdctl_SOURCE_DIR}/main.cpp
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/irccdctl/alias.cpp Sun Nov 13 14:16:58 2016 +0100 @@ -0,0 +1,60 @@ +/* + * alias.cpp -- create irccdctl aliases + * + * Copyright (c) 2013-2016 David Demelier <markand@malikania.fr> + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#include <cassert> +#include <regex> + +#include "alias.hpp" + +namespace irccd { + +AliasArg::AliasArg(std::string value) +{ + assert(!value.empty()); + + if ((m_isPlaceholder = std::regex_match(value, std::regex("^%\\d+$")))) + m_value = value.substr(1); + else + m_value = std::move(value); +} + +unsigned AliasArg::index() const noexcept +{ + assert(isPlaceholder()); + + return std::stoi(m_value); +} + +const std::string &AliasArg::value() const noexcept +{ + assert(!isPlaceholder()); + + return m_value; +} + +std::ostream &operator<<(std::ostream &out, const AliasArg &arg) +{ + if (arg.m_isPlaceholder) + out << "%" << arg.m_value; + else + out << arg.m_value; + + return out; +} + +} // !irccd
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/irccdctl/alias.hpp Sun Nov 13 14:16:58 2016 +0100 @@ -0,0 +1,174 @@ +/* + * alias.hpp -- create irccdctl aliases + * + * Copyright (c) 2013-2016 David Demelier <markand@malikania.fr> + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#ifndef IRCCD_ALIAS_HPP +#define IRCCD_ALIAS_HPP + +/** + * \file alias.hpp + * \brief Create irccdctl aliases. + */ + +#include <ostream> +#include <string> +#include <vector> + +#include "sysconfig.hpp" + +namespace irccd { + +/** + * \class AliasArg + * \brief Describe an alias argument. + * + * When the user specify arguments, it can precise an applied argument or a + * placeholder that will be substituted during command line invocation. + * + * Placeholders are placed using %n where n is an integer starting from 0. + */ +class AliasArg { +private: + std::string m_value; + bool m_isPlaceholder; + +public: + /** + * Construct an argument. + * + * \pre value must not be empty + * \param value the value + */ + IRCCD_EXPORT AliasArg(std::string value); + + /** + * Check if the argument is a placeholder. + * + * \return true if the argument is a placeholder + */ + inline bool isPlaceholder() const noexcept + { + return m_isPlaceholder; + } + + /** + * Get the placeholder index (e.g %0 returns 0) + * + * \pre isPlaceholder() must return true + * \return the position + */ + IRCCD_EXPORT unsigned index() const noexcept; + + /** + * Get the real value. + * + * \pre isPlaceholder() must return false + * \return the value + */ + IRCCD_EXPORT const std::string &value() const noexcept; + + /** + * Output the alias to the stream. + * + * \param out the output stream + * \return out + */ + IRCCD_EXPORT friend std::ostream &operator<<(std::ostream &out, const AliasArg &); +}; + +/** + * \class AliasCommand + * \brief Describe a user-provided alias command. + * + * An alias command is just a command with a set of applied or placeholders + * arguments. + */ +class AliasCommand { +private: + std::string m_command; + std::vector<AliasArg> m_args; + +public: + /** + * Create an alias command. + * + * \param command the command + * \param args the arguments + */ + inline AliasCommand(std::string command, std::vector<AliasArg> args = {}) noexcept + : m_command(std::move(command)) + , m_args(std::move(args)) + { + } + + /** + * Get the command to execute. + * + * \return the command name + */ + inline const std::string &command() const noexcept + { + return m_command; + } + + /** + * Get the arguments. + * + * \return the arguments + */ + inline const std::vector<AliasArg> &args() const noexcept + { + return m_args; + } +}; + +/** + * \class Alias + * \brief A set of commands to execute with their arguments. + * + * An alias is a composition of AliasCommand, typically, the user is able to set + * an alias that execute a list of specified commands in order they are defined. + */ +class Alias : public std::vector<AliasCommand> { +private: + std::string m_name; + +public: + /** + * Create an alias. + * + * \param name the alias name + */ + inline Alias(std::string name) noexcept + : m_name(std::move(name)) + { + } + + /** + * Get the alias name. + * + * \return the name + */ + inline const std::string &name() const noexcept + { + return m_name; + } +}; + +} // !irccd + +#endif // !IRCCD_ALIAS_HPP
--- a/irccdctl/cli.cpp Sun Nov 13 10:41:28 2016 +0100 +++ b/irccdctl/cli.cpp Sun Nov 13 14:16:58 2016 +0100 @@ -71,8 +71,6 @@ irccdctl.client().onMessage.disconnect(id); - std::cout << msg << std::endl; - if (!msg.is_object()) throw std::runtime_error("no response received"); if (util::json::getString(msg, "command") != m_name)
--- a/irccdctl/main.cpp Sun Nov 13 10:41:28 2016 +0100 +++ b/irccdctl/main.cpp Sun Nov 13 14:16:58 2016 +0100 @@ -16,21 +16,559 @@ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. */ +#include <unordered_map> + +#include <format.h> + +#include "alias.hpp" +#include "cli.hpp" #include "client.hpp" +#include "elapsed-timer.hpp" +#include "fs.hpp" +#include "ini.hpp" #include "irccdctl.hpp" -#include "cli.hpp" +#include "logger.hpp" +#include "options.hpp" +#include "path.hpp" +#include "system.hpp" +#include "util.hpp" +using namespace std::string_literals; +using namespace fmt::literals; using namespace irccd; -int main(int, char **) +namespace { + +std::vector<std::unique_ptr<Cli>> commands; +std::unordered_map<std::string, Alias> aliases; +std::unique_ptr<Client> client; +std::unique_ptr<Irccdctl> irccdctl; +net::Address address; + +void usage() +{ + bool first = true; + + for (const auto &cmd : commands) { + log::warning() << (first ? "usage: " : " ") << sys::programName() << " " + << cmd->usage() << std::endl; + first = false; + } + + std::exit(1); +} + +void help() +{ + log::warning() << "usage: " << sys::programName() << " [options...] <command> [command-options...] [command-args...]\n\n"; + log::warning() << "General options:\n"; + log::warning() << "\t-c, --config file\tspecify the configuration file\n"; + log::warning() << "\t--help\t\t\tshow this help\n"; + log::warning() << "\t-t, --type type\t\tspecify connection type\n"; + log::warning() << "\t-v, --verbose\t\tbe verbose\n\n"; + log::warning() << "Available options for type ip and ipv6 (-t, --type):\n"; + log::warning() << "\t-h, --host address\tconnect to the specified address\n"; + log::warning() << "\t-p, --port port\t\tuse the specified port number\n\n"; + log::warning() << "Available options for type unix (-t, --type):\n"; + log::warning() << "\t-P, --path file\t\tconnect to the specified socket file\n\n"; + log::warning() << "Available commands:\n"; + + for (const auto &cmd : commands) + log::warning() << "\t" << std::left << std::setw(32) + << cmd->name() << cmd->summary() << std::endl; + + log::warning() << "\nFor more information on a command, type " << sys::programName() << " help <command>" << std::endl; + std::exit(1); +} + +/* + * Configuration file parsing. + * ------------------------------------------------------------------- + */ + +/* + * readConnectIp + * ------------------------------------------------------------------- + * + * Extract IP connection information from the config file. + * + * [connect] + * type = "ip" + * host = "ip or hostname" + * port = "port number or service" + * domain = "ipv4 or ipv6" (Optional, default: ipv4) + * ssl = true | false + */ +void readConnectIp(const ini::Section &sc) +{ + ini::Section::const_iterator it; + std::string host, port; + + if ((it = sc.find("host")) == sc.end()) + throw std::invalid_argument("missing host parameter"); + + host = it->value(); + + if ((it = sc.find("port")) == sc.end()) + throw std::invalid_argument("missing port parameter"); + + port = it->value(); + + int domain = AF_INET; + + if ((it = sc.find("domain")) != sc.end()) { + if (it->value() == "ipv6") { + domain = AF_INET6; + } else if (it->value() == "ipv4") { + domain = AF_INET; + } else { + throw std::invalid_argument("invalid domain: " + it->value()); + } + } + + address = net::resolveOne(host, port, domain, SOCK_STREAM); + + if ((it = sc.find("ssl")) != sc.end() && util::isBoolean(it->value())) + client = std::make_unique<TlsClient>(); + else + client = std::make_unique<Client>(); +} + +/* + * readConnectLocal + * ------------------------------------------------------------------- + * + * Extract local connection for Unix. + * + * [connect] + * type = "unix" + * path = "path to socket file" + */ +void readConnectLocal(const ini::Section &sc) +{ +#if !defined(IRCCD_SYSTEM_WINDOWS) + auto it = sc.find("path"); + + if (it == sc.end()) + throw std::invalid_argument("missing path parameter"); + + address = net::local::create(it->value()); + client = std::make_unique<Client>(); +#else + (void)sc; + + throw std::invalid_argument("unix connection not supported on Windows"); +#endif +} + +/* + * readConnect + * ------------------------------------------------------------------- + * + * Generic function for reading the [connect] section. + */ +void readConnect(const ini::Section &sc) +{ + auto it = sc.find("type"); + + if (it == sc.end()) + throw std::invalid_argument("missing type parameter"); + + if (it->value() == "ip") { + readConnectIp(sc); + } else if (it->value() == "unix") { + readConnectLocal(sc); + } else { + throw std::invalid_argument("invalid type given: " + it->value()); + } + + auto password = sc.find("password"); + + if (password != sc.end()) + client->setPassword(password->value()); +} + +/* + * readGeneral + * ------------------------------------------------------------------- + * + * Read the general section. + * + * [general] + * verbose = true + */ +void readGeneral(const ini::Section &sc) +{ + auto verbose = sc.find("verbose"); + + if (verbose != sc.end()) + log::setVerbose(util::isBoolean(verbose->value())); +} + +/* + * readAliases + * ------------------------------------------------------------------- + * + * Read aliases for irccdctl. + * + * [alias] + * name = ( "command", "arg1, "...", "argn" ) + */ +void readAliases(const ini::Section &sc) +{ + for (const auto &option : sc) { + // This is the alias name. + Alias alias(option.key()); + + // Iterate over the list of commands to execute for this alias. + for (const auto &repl : option) { + // This is the alias split string. + auto list = util::split(repl, " \t"); + + if (list.size() < 1) + throw std::invalid_argument("alias require at least one argument"); + + // First argument is the command/alias to execute. + auto command = list[0]; + + // Remove command name and puts arguments. + alias.push_back({std::move(command), std::vector<AliasArg>(list.begin() + 1, list.end())}); + } + + aliases.emplace(option.key(), std::move(alias)); + } +} + +void read(const std::string &path) { try { - Irccdctl ctl(std::make_unique<Client>()); - ctl.client().connect(net::local::create("/tmp/irccd.sock")); + ini::Document doc = ini::readFile(path); + ini::Document::const_iterator it; - cli::PluginConfigCli command; - command.exec(ctl, {"ask", "disable_if", "ok"}); + if (!client && (it = doc.find("connect")) != doc.end()) + readConnect(*it); + if ((it = doc.find("general")) != doc.end()) + readGeneral(*it); + if ((it = doc.find("alias")) != doc.end()) + readAliases(*it); } catch (const std::exception &ex) { - std::cerr << ex.what() << std::endl; + log::warning() << path << ": " << ex.what() << std::endl; } } + +/* + * Command line parsing. + * ------------------------------------------------------------------- + */ + +/* + * parseConnectIp + * ------------------------------------------------------------------ + * + * Parse internet connection from command line. + * + * -t ip | ipv6 + * -h host or ip + * -p port + */ +void parseConnectIp(const option::Result &options) +{ + option::Result::const_iterator it; + + // Host (-h or --host). + std::string host; + + if ((it = options.find("-h")) == options.end() && (it = options.find("--host")) == options.end()) { + throw std::invalid_argument("missing host argument (-h or --host)"); + } + + host = it->second; + + // Port (-p or --port). + std::string port; + + if ((it = options.find("-p")) == options.end() && (it = options.find("--port")) == options.end()) { + throw std::invalid_argument("missing port argument (-p or --port)"); + } + + port = it->second; + + // Domain + int domain = AF_INET; + + if ((it = options.find("-t")) != options.end()) { + domain = it->second == "ipv6" ? AF_INET6 : AF_INET; + } else if ((it = options.find("--type")) != options.end()) { + domain = it->second == "ipv6" ? AF_INET6: AF_INET; + } + + address = net::resolveOne(host, port, domain, SOCK_STREAM); + client = std::make_unique<Client>(); +} + +/* + * parseConnectLocal + * ------------------------------------------------------------------ + * + * Parse local connection. + * + * -P file + */ +void parseConnectLocal(const option::Result &options) +{ +#if !defined(IRCCD_SYSTEM_WINDOWS) + option::Result::const_iterator it; + + if ((it = options.find("-P")) == options.end() && (it = options.find("--path")) == options.end()) + throw std::invalid_argument("missing path parameter (-P or --path)"); + + address = net::local::create(it->second, false); + client = std::make_unique<Client>(); +#else + (void)options; + + throw std::invalid_argument("unix connection not supported on Windows"); +#endif +} + +/* + * parseConnect + * ------------------------------------------------------------------ + * + * Generic parsing of command line option for connection. + */ +void parseConnect(const option::Result &options) +{ + assert(options.count("-t") > 0 || options.count("--type") > 0); + + auto it = options.find("-t"); + + if (it == options.end()) + it = options.find("--type"); + if (it->second == "ip" || it->second == "ipv6") + return parseConnectIp(options); + if (it->second == "unix") + return parseConnectLocal(options); + + throw std::invalid_argument("invalid type given: " + it->second); +} + +option::Result parse(int &argc, char **&argv) +{ + // 1. Parse command line options. + option::Options def{ + { "-c", true }, + { "--config", true }, + { "-h", true }, + { "--help", false }, + { "--host", true }, + { "-p", true }, + { "--port", true }, + { "-P", true }, + { "--path", true }, + { "-t", true }, + { "--type", true }, + { "-v", false }, + { "--verbose", false } + }; + + option::Result result; + + try { + result = option::read(argc, argv, def); + + if (result.count("--help") != 0) { + usage(); + // NOTREACHED + } + + if (result.count("-v") != 0 || result.count("--verbose") != 0) { + log::setVerbose(true); + } + } catch (const std::exception &ex) { + log::warning("{}: {}"_format(sys::programName(), ex.what())); + usage(); + } + + return result; +} + +void exec(std::vector<std::string>); + +void exec(const Alias &alias, std::vector<std::string> argsCopy) +{ + std::vector<nlohmann::json> values; + + for (const AliasCommand &cmd : alias) { + std::vector<std::string> args(argsCopy); + std::vector<std::string> cmdArgs; + std::vector<std::string>::size_type toremove = 0; + + // 1. Append command name before. + cmdArgs.push_back(cmd.command()); + + for (const auto &arg : cmd.args()) { + if (arg.isPlaceholder()) { + if (args.size() < arg.index() + 1) { + throw std::invalid_argument("missing argument for placeholder %" + std::to_string(arg.index())); + } + + cmdArgs.push_back(args[arg.index()]); + + if (arg.index() + 1 > toremove) { + toremove = arg.index() + 1; + } + } else { + cmdArgs.push_back(arg.value()); + } + } + + assert(toremove <= args.size()); + + // 2. Remove the arguments that been placed in placeholders. + args.erase(args.begin(), args.begin() + toremove); + + // 3. Now append the rest of arguments. + std::copy(args.begin(), args.end(), std::back_inserter(cmdArgs)); + + // 4. Finally try to execute. + exec(cmdArgs); + } +} + +void exec(std::vector<std::string> args) +{ + assert(args.size() > 0); + + auto name = args[0]; + auto alias = aliases.find(name); + + // Remove name. + args.erase(args.begin()); + + if (alias != aliases.end()) { + exec(alias->second, args); + } else { + auto cmd = std::find_if(commands.begin(), commands.end(), [&] (auto &it) { + return it->name() == name; + }); + + if (cmd != commands.end()) { + (*cmd)->exec(*irccdctl, args); + } else { + throw std::invalid_argument("no alias or command named " + name); + } + } +} + +void init(int &argc, char **&argv) +{ + sys::setProgramName("irccdctl"); + net::init(); + + --argc; + ++argv; + + commands.push_back(std::make_unique<cli::PluginConfigCli>()); + commands.push_back(std::make_unique<cli::PluginInfoCli>()); + commands.push_back(std::make_unique<cli::PluginListCli>()); + commands.push_back(std::make_unique<cli::PluginLoadCli>()); + commands.push_back(std::make_unique<cli::PluginReloadCli>()); + commands.push_back(std::make_unique<cli::PluginUnloadCli>()); + commands.push_back(std::make_unique<cli::ServerChannelMode>()); + commands.push_back(std::make_unique<cli::ServerChannelNoticeCli>()); + commands.push_back(std::make_unique<cli::ServerConnectCli>()); + commands.push_back(std::make_unique<cli::ServerDisconnectCli>()); + commands.push_back(std::make_unique<cli::ServerInfoCli>()); + commands.push_back(std::make_unique<cli::ServerInviteCli>()); + commands.push_back(std::make_unique<cli::ServerJoinCli>()); + commands.push_back(std::make_unique<cli::ServerKickCli>()); + commands.push_back(std::make_unique<cli::ServerListCli>()); + commands.push_back(std::make_unique<cli::ServerMeCli>()); + commands.push_back(std::make_unique<cli::ServerMessageCli>()); + commands.push_back(std::make_unique<cli::ServerModeCli>()); + commands.push_back(std::make_unique<cli::ServerNickCli>()); + commands.push_back(std::make_unique<cli::ServerNoticeCli>()); + commands.push_back(std::make_unique<cli::ServerPartCli>()); + commands.push_back(std::make_unique<cli::ServerReconnectCli>()); +} + +} // !namespace + +int main(int argc, char **argv) +{ + init(argc, argv); + + // 1. Read command line arguments. + auto result = parse(argc, argv); + + /* + * 2. Open optional config by command line or by searching it + * + * The connection to irccd is searched in the following order : + * + * 1. From the command line if specified + * 2. From the configuration file specified by -c + * 3. From the configuration file searched through directories + */ + try { + if (result.count("-t") > 0 || result.count("--type") > 0) + parseConnect(result); + + auto it = result.find("-c"); + + if (it != result.end() || (it = result.find("--config")) != result.end()) + read(it->second); + else { + for (const std::string &dir : path::list(path::PathConfig)) { + std::string path = dir + "irccdctl.conf"; + + if (fs::exists(path)) { + read(path); + break; + } + } + } + } catch (const std::exception &ex) { + log::warning() << sys::programName() << ": " << ex.what() << std::endl; + std::exit(1); + } + + if (argc <= 0) { + usage(); + // NOTREACHED + } + if (std::strcmp(argv[0], "help") == 0) { + help(); + // NOTREACHED + } + + if (!client) { + log::warning("{}: no connection specified"_format(sys::programName())); + std::exit(1); + } + + irccdctl = std::make_unique<Irccdctl>(std::move(client)); + irccdctl->client().onDisconnect.connect([&] (auto reason) { + log::warning() << "connection lost to irccd: " << reason << std::endl; + }); + irccdctl->client().onConnect.connect([&] (auto info) { + log::info() << "connected to irccd " + << info.major << "." + << info.minor << "." + << info.patch << std::endl; + }); + irccdctl->client().connect(address); + + // Build a vector of arguments. + std::vector<std::string> args; + + for (int i = 0; i < argc; ++i) + args.push_back(argv[i]); + + try { + exec(args); + } catch (const std::exception &ex) { + std::cerr << sys::programName() << ": unrecoverable error: " << ex.what() << std::endl; + } +}