From 43e7b6b8be408219e30f28b5bb8dfa25257469f7 Mon Sep 17 00:00:00 2001 From: Thulinma Date: Wed, 19 Jul 2017 15:03:25 +0200 Subject: [PATCH] Prettified controller + fixed Ctrl+C during startup questions + made 't' answer add protocols + No longer ask to add protocols when none are installed. --- src/controller/controller.cpp | 433 +++++++++++++++++++--------------- 1 file changed, 240 insertions(+), 193 deletions(-) diff --git a/src/controller/controller.cpp b/src/controller/controller.cpp index 10075587..556a04e6 100644 --- a/src/controller/controller.cpp +++ b/src/controller/controller.cpp @@ -1,86 +1,53 @@ -/// \page api API calls -/// \brief Listing of all controller API calls. -/// The controller listens for commands through a JSON-based API. This page describes the API in full. -/// -/// A default interface implementing this API as a single HTML page is included in the controller itself. This default interface will be send for invalid API requests, and is thus triggered by default when a browser attempts to access the API port directly. -/// The default API port is 4242 - but this can be changed through both the API and commandline parameters. -/// -/// To send an API request, simply send a HTTP request to this port for any file, and include either a GET or POST parameter called `"command"`, containing a JSON object as payload. Nearly all members of the request object are optional, and described below. -/// A simple example request logging in to the system would look like this: -/// -/// GET /api?command={"authorize":{"username":"test","password":"941d7b88b2312d4373aff526cf7b6114"}} HTTP/1.0 -/// -/// Or, when properly URL encoded: -/// -/// GET /api?command=%7B%22authorize%22%3A%7B%22username%22%3A%22test%22%2C%22password%22%3A%22941d7b88b2312d4373aff526cf7b6114%22%7D%7D HTTP/1.0 -/// -/// The server is quite lenient about not URL encoding your strings, but it's a good idea to always do it, anyway. -/// See the `"authorize"` section below for more information about security and logging in. -/// -/// As mentioned above, sending an invalid request will trigger a response containing the default interface. As you may not want to receive a big HTML page as response to an invalid request, requesting the file `"/api"` (as done in the example above) will force a JSON response, even when the request is invalid. -/// -/// You may also include a `"callback"` or `"jsonp"` HTTP variable, to trigger JSONP compatibility mode. JSONP is useful for getting around the cross-domain scripting protection in most modern browsers. Developers creating non-JavaScript applications will most likely not want to use JSONP mode, though nothing is stopping you if you really want to. -/// - - - - /// \file controller.cpp /// Contains all code for the controller executable. - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include "controller_storage.h" -#include "controller_streams.h" +#include "controller_api.h" #include "controller_capabilities.h" #include "controller_connectors.h" #include "controller_statistics.h" -#include "controller_api.h" +#include "controller_storage.h" +#include "controller_streams.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include #ifndef COMPILED_USERNAME #define COMPILED_USERNAME "" #define COMPILED_PASSWORD "" #endif -/// the following function is a simple check if the user wants to proceed to fix (y), ignore (n) or abort on (a) a question -static inline char yna(std::string & user_input){ +/// the following function is a simple check if the user wants to proceed to fix (y), ignore (n) or +/// abort on (a) a question +static inline char yna(std::string &user_input){ switch (user_input[0]){ - case 'y': case 'Y': - return 'y'; - break; - case 'n': case 'N': - return 'n'; - break; - case 'a': case 'A': - return 'a'; - break; - case 't': case 'T': - return 't'; - break; - default: - return 'x'; - break; + case 'y': + case 'Y': return 'y'; break; + case 'n': + case 'N': return 'n'; break; + case 'a': + case 'A': return 'a'; break; + case 't': + case 'T': return 't'; break; + default: return 'x'; break; } } /// createAccount accepts a string in the form of username:account /// and creates an account. -void createAccount (std::string account){ +void createAccount(std::string account){ if (account.size() > 0){ size_t colon = account.find(':'); if (colon != std::string::npos && colon != 0 && colon != account.size()){ @@ -92,25 +59,26 @@ void createAccount (std::string account){ } } - /// Status monitoring thread. /// Will check outputs, inputs and converters every five seconds -void statusMonitor(void * np){ +void statusMonitor(void *np){ IPC::semaphore configLock(SEM_CONF, O_CREAT | O_RDWR, ACCESSPERMS, 1); Controller::loadActiveConnectors(); while (Controller::conf.is_active){ - //this scope prevents the configMutex from being locked constantly + // this scope prevents the configMutex from being locked constantly { tthread::lock_guard guard(Controller::configMutex); bool changed = false; - //checks online protocols, reports changes to status - changed |= Controller::CheckProtocols(Controller::Storage["config"]["protocols"], Controller::capabilities); - //checks stream statuses, reports changes to status + // checks online protocols, reports changes to status + changed |= Controller::CheckProtocols(Controller::Storage["config"]["protocols"], + Controller::capabilities); + // checks stream statuses, reports changes to status changed |= Controller::CheckAllStreams(Controller::Storage["streams"]); - - //check if the config semaphore is stuck, by trying to lock it for 5 attempts of 1 second... - if (!configLock.tryWaitOneSecond() && !configLock.tryWaitOneSecond() && !configLock.tryWaitOneSecond() && !configLock.tryWaitOneSecond()){ - //that failed. We now unlock it, no matter what - and print a warning that it was stuck. + + // check if the config semaphore is stuck, by trying to lock it for 5 attempts of 1 second... + if (!configLock.tryWaitOneSecond() && !configLock.tryWaitOneSecond() && + !configLock.tryWaitOneSecond() && !configLock.tryWaitOneSecond()){ + // that failed. We now unlock it, no matter what - and print a warning that it was stuck. WARN_MSG("Configuration semaphore was stuck. Force-unlocking it and re-writing config."); changed = true; } @@ -120,7 +88,7 @@ void statusMonitor(void * np){ Controller::configChanged = false; } } - Util::sleep(5000);//wait at least 5 seconds + Util::sleep(5000); // wait at least 5 seconds } if (Controller::restarting){ Controller::prepareActiveConnectorsForReload(); @@ -130,82 +98,129 @@ void statusMonitor(void * np){ configLock.unlink(); } +static unsigned long mix(unsigned long a, unsigned long b, unsigned long c){ + a = a - b; + a = a - c; + a = a ^ (c >> 13); + b = b - c; + b = b - a; + b = b ^ (a << 8); + c = c - a; + c = c - b; + c = c ^ (b >> 13); + a = a - b; + a = a - c; + a = a ^ (c >> 12); + b = b - c; + b = b - a; + b = b ^ (a << 16); + c = c - a; + c = c - b; + c = c ^ (b >> 5); + a = a - b; + a = a - c; + a = a ^ (c >> 3); + b = b - c; + b = b - a; + b = b ^ (a << 10); + c = c - a; + c = c - b; + c = c ^ (b >> 15); + return c; +} + ///\brief The main loop for the controller. -int main_loop(int argc, char ** argv){ +int main_loop(int argc, char **argv){ Controller::isTerminal = Controller::isColorized = isatty(fileno(stdin)); Controller::Storage = JSON::fromFile("config.json"); - JSON::Value stored_port = JSON::fromString("{\"long\":\"port\", \"short\":\"p\", \"arg\":\"integer\", \"help\":\"TCP port to listen on.\"}"); + JSON::Value stored_port = + JSON::fromString("{\"long\":\"port\", \"short\":\"p\", \"arg\":\"integer\", \"help\":\"TCP " + "port to listen on.\"}"); stored_port["default"] = Controller::Storage["config"]["controller"]["port"]; - if ( !stored_port["default"]){ - stored_port["default"] = 4242; - } - JSON::Value stored_interface = JSON::fromString("{\"long\":\"interface\", \"short\":\"i\", \"arg\":\"string\", \"help\":\"Interface address to listen on, or 0.0.0.0 for all available interfaces.\"}"); + if (!stored_port["default"]){stored_port["default"] = 4242;} + JSON::Value stored_interface = JSON::fromString( + "{\"long\":\"interface\", \"short\":\"i\", \"arg\":\"string\", \"help\":\"Interface address " + "to listen on, or 0.0.0.0 for all available interfaces.\"}"); stored_interface["default"] = Controller::Storage["config"]["controller"]["interface"]; - if ( !stored_interface["default"]){ - stored_interface["default"] = "0.0.0.0"; - } - JSON::Value stored_user = JSON::fromString("{\"long\":\"username\", \"short\":\"u\", \"arg\":\"string\", \"help\":\"Username to transfer privileges to, default is root.\"}"); + if (!stored_interface["default"]){stored_interface["default"] = "0.0.0.0";} + JSON::Value stored_user = + JSON::fromString("{\"long\":\"username\", \"short\":\"u\", \"arg\":\"string\", " + "\"help\":\"Username to transfer privileges to, default is root.\"}"); stored_user["default"] = Controller::Storage["config"]["controller"]["username"]; - if ( !stored_user["default"]){ - stored_user["default"] = "root"; - } + if (!stored_user["default"]){stored_user["default"] = "root";} Controller::conf.addOption("port", stored_port); Controller::conf.addOption("interface", stored_interface); Controller::conf.addOption("username", stored_user); - Controller::conf.addOption("account", JSON::fromString("{\"long\":\"account\", \"short\":\"a\", \"arg\":\"string\" \"default\":\"\", \"help\":\"A username:password string to create a new account with.\"}")); - Controller::conf.addOption("logfile", JSON::fromString("{\"long\":\"logfile\", \"short\":\"L\", \"arg\":\"string\" \"default\":\"\",\"help\":\"Redirect all standard output to a log file, provided with an argument\"}")); - Controller::conf.addOption("configFile", JSON::fromString("{\"long\":\"config\", \"short\":\"c\", \"arg\":\"string\" \"default\":\"config.json\", \"help\":\"Specify a config file other than default.\"}")); + Controller::conf.addOption( + "account", JSON::fromString("{\"long\":\"account\", \"short\":\"a\", \"arg\":\"string\" " + "\"default\":\"\", \"help\":\"A username:password string to " + "create a new account with.\"}")); + Controller::conf.addOption( + "logfile", JSON::fromString("{\"long\":\"logfile\", \"short\":\"L\", \"arg\":\"string\" " + "\"default\":\"\",\"help\":\"Redirect all standard output to a " + "log file, provided with an argument\"}")); + Controller::conf.addOption( + "configFile", JSON::fromString("{\"long\":\"config\", \"short\":\"c\", \"arg\":\"string\" " + "\"default\":\"config.json\", \"help\":\"Specify a config " + "file other than default.\"}")); Controller::conf.parseArgs(argc, argv); - if(Controller::conf.getString("logfile")!= ""){ - //open logfile, dup stdout to logfile - int output = open(Controller::conf.getString("logfile").c_str(),O_APPEND|O_CREAT|O_WRONLY,S_IRWXU); - if(output < 0){ - DEBUG_MSG(DLVL_ERROR, "Could not redirect output to %s: %s",Controller::conf.getString("logfile").c_str(),strerror(errno)); + if (Controller::conf.getString("logfile") != ""){ + // open logfile, dup stdout to logfile + int output = + open(Controller::conf.getString("logfile").c_str(), O_APPEND | O_CREAT | O_WRONLY, S_IRWXU); + if (output < 0){ + DEBUG_MSG(DLVL_ERROR, "Could not redirect output to %s: %s", + Controller::conf.getString("logfile").c_str(), strerror(errno)); return 7; }else{ Controller::isTerminal = Controller::isColorized = false; - dup2(output,STDOUT_FILENO); - dup2(output,STDERR_FILENO); + dup2(output, STDOUT_FILENO); + dup2(output, STDERR_FILENO); time_t rawtime; - struct tm * timeinfo; - char buffer [25]; - time (&rawtime); - timeinfo = localtime (&rawtime); - strftime (buffer,25,"%c",timeinfo); - std::cerr << std::endl << std::endl <<"!----MistServer Started at " << buffer << " ----!" << std::endl; + struct tm *timeinfo; + char buffer[25]; + time(&rawtime); + timeinfo = localtime(&rawtime); + strftime(buffer, 25, "%c", timeinfo); + std::cerr << std::endl + << std::endl + << "!----MistServer Started at " << buffer << " ----!" << std::endl; } } - //reload config from config file + // reload config from config file Controller::Storage = JSON::fromFile(Controller::conf.getString("configFile")); - - {//spawn thread that reads stderr of process + + {// spawn thread that reads stderr of process int pipeErr[2]; if (pipe(pipeErr) >= 0){ - dup2(pipeErr[1], STDERR_FILENO);//cause stderr to write to the pipe - close(pipeErr[1]);//close the unneeded pipe file descriptor + dup2(pipeErr[1], STDERR_FILENO); // cause stderr to write to the pipe + close(pipeErr[1]); // close the unneeded pipe file descriptor Util::Procs::socketList.insert(pipeErr[0]); - tthread::thread msghandler(Controller::handleMsg, (void*)(((char*)0) + pipeErr[0])); + tthread::thread msghandler(Controller::handleMsg, (void *)(((char *)0) + pipeErr[0])); msghandler.detach(); } } - - - if (Controller::conf.getOption("debug",true).size() > 1){ + + if (Controller::conf.getOption("debug", true).size() > 1){ Controller::Storage["config"]["debug"] = Controller::conf.getInteger("debug"); } - if (Controller::Storage.isMember("config") && Controller::Storage["config"].isMember("debug") && Controller::Storage["config"]["debug"].isInt()){ + if (Controller::Storage.isMember("config") && Controller::Storage["config"].isMember("debug") && + Controller::Storage["config"]["debug"].isInt()){ Util::Config::printDebugLevel = Controller::Storage["config"]["debug"].asInt(); } - //check for port, interface and username in arguments - //if they are not there, take them from config file, if there + // check for port, interface and username in arguments + // if they are not there, take them from config file, if there if (Controller::Storage["config"]["controller"]["port"]){ - Controller::conf.getOption("port", true)[0u] = Controller::Storage["config"]["controller"]["port"]; + Controller::conf.getOption("port", true)[0u] = + Controller::Storage["config"]["controller"]["port"]; } if (Controller::Storage["config"]["controller"]["interface"]){ - Controller::conf.getOption("interface", true)[0u] = Controller::Storage["config"]["controller"]["interface"]; + Controller::conf.getOption("interface", true)[0u] = + Controller::Storage["config"]["controller"]["interface"]; } if (Controller::Storage["config"]["controller"]["username"]){ - Controller::conf.getOption("username", true)[0u] = Controller::Storage["config"]["controller"]["username"]; + Controller::conf.getOption("username", true)[0u] = + Controller::Storage["config"]["controller"]["username"]; } { IPC::semaphore configLock(SEM_CONF, O_CREAT | O_RDWR, ACCESSPERMS, 1); @@ -214,135 +229,167 @@ int main_loop(int argc, char ** argv){ Controller::writeConfig(); Controller::checkAvailProtocols(); createAccount(Controller::conf.getString("account")); - - //if a terminal is connected and we're not logging to file + Controller::conf.activate(); // activate early, so threads aren't killed. + + // if a terminal is connected and we're not logging to file if (Controller::isTerminal){ - //check for username - if ( !Controller::Storage.isMember("account") || Controller::Storage["account"].size() < 1){ + // check for username + if (!Controller::Storage.isMember("account") || Controller::Storage["account"].size() < 1){ std::string in_string = ""; - while(yna(in_string) == 'x'){ + while (yna(in_string) == 'x' && Controller::conf.is_active){ std::cout << "Account not set, do you want to create an account? (y)es, (n)o, (a)bort: "; std::cout.flush(); std::getline(std::cin, in_string); switch (yna(in_string)){ - case 'y':{ - //create account - std::string usr_string = ""; - while(!(Controller::Storage.isMember("account") && Controller::Storage["account"].size() > 0)){ - std::cout << "Please type in the username, a colon and a password in the following format; username:password" << std::endl << ": "; - std::cout.flush(); - std::getline(std::cin, usr_string); - createAccount(usr_string); + case 'y':{ + // create account + std::string usr_string = ""; + while (!(Controller::Storage.isMember("account") && + Controller::Storage["account"].size() > 0) && + Controller::conf.is_active){ + std::cout << "Please type in the username, a colon and a password in the following " + "format; username:password" + << std::endl + << ": "; + std::cout.flush(); + std::getline(std::cin, usr_string); + createAccount(usr_string); + } + }break; + case 'a': + return 0; // abort bootup + case 't':{ + createAccount("test:test"); + if ((Controller::capabilities["connectors"].size()) && + (!Controller::Storage.isMember("config") || + !Controller::Storage["config"].isMember("protocols") || + Controller::Storage["config"]["protocols"].size() < 1)){ + // create protocols + jsonForEach(Controller::capabilities["connectors"], it){ + if (!it->isMember("required")){ + JSON::Value newProtocol; + newProtocol["connector"] = it.key(); + Controller::Storage["config"]["protocols"].append(newProtocol); } } - break; - case 'a': return 0; //abort bootup - case 't': - createAccount("test:test"); - break; + } + }break; } } } - //check for protocols - if ( !Controller::Storage.isMember("config") || !Controller::Storage["config"].isMember("protocols") || Controller::Storage["config"]["protocols"].size() < 1){ + // check for protocols + if ((Controller::capabilities["connectors"].size()) && + (!Controller::Storage.isMember("config") || + !Controller::Storage["config"].isMember("protocols") || + Controller::Storage["config"]["protocols"].size() < 1)){ std::string in_string = ""; - while(yna(in_string) == 'x'){ - std::cout << "Protocols not set, do you want to enable default protocols? (y)es, (n)o, (a)bort: "; + while (yna(in_string) == 'x' && Controller::conf.is_active){ + std::cout + << "Protocols not set, do you want to enable default protocols? (y)es, (n)o, (a)bort: "; std::cout.flush(); std::getline(std::cin, in_string); if (yna(in_string) == 'y'){ - //create protocols - jsonForEach(Controller::capabilities["connectors"], it) { + // create protocols + jsonForEach(Controller::capabilities["connectors"], it){ if (!it->isMember("required")){ JSON::Value newProtocol; newProtocol["connector"] = it.key(); Controller::Storage["config"]["protocols"].append(newProtocol); } } - }else if(yna(in_string) == 'a'){ - //abort controller startup + }else if (yna(in_string) == 'a'){ + // abort controller startup return 0; } } } } - //Check if we have a usable server, if not, print messages with helpful hints + // Check if we have a usable server, if not, print messages with helpful hints { std::string web_port = JSON::Value((long long)Controller::conf.getInteger("port")).asString(); - //check for username - if ( !Controller::Storage.isMember("account") || Controller::Storage["account"].size() < 1){ - Controller::Log("CONF", "No login configured. To create one, attempt to login through the web interface on port "+web_port+" and follow the instructions."); + // check for username + if (!Controller::Storage.isMember("account") || Controller::Storage["account"].size() < 1){ + Controller::Log("CONF", + "No login configured. To create one, attempt to login through the web " + "interface on port " + + web_port + " and follow the instructions."); } - //check for protocols - if ( !Controller::Storage.isMember("config") || !Controller::Storage["config"].isMember("protocols") || Controller::Storage["config"]["protocols"].size() < 1){ - Controller::Log("CONF", "No protocols enabled, remember to set them up through the web interface on port "+web_port+" or API."); + // check for protocols + if (!Controller::Storage.isMember("config") || + !Controller::Storage["config"].isMember("protocols") || + Controller::Storage["config"]["protocols"].size() < 1){ + Controller::Log( + "CONF", + "No protocols enabled, remember to set them up through the web interface on port " + + web_port + " or API."); } - //check for streams - regardless of logfile setting - if ( !Controller::Storage.isMember("streams") || Controller::Storage["streams"].size() < 1){ - Controller::Log("CONF", "No streams configured, remember to set up streams through the web interface on port "+web_port+" or API."); + // check for streams - regardless of logfile setting + if (!Controller::Storage.isMember("streams") || Controller::Storage["streams"].size() < 1){ + Controller::Log( + "CONF", + "No streams configured, remember to set up streams through the web interface on port " + + web_port + " or API."); } } - + Controller::Log("CONF", "Controller started"); - Controller::conf.activate();//activate early, so threads aren't killed. - //Generate instanceId once per boot. + // Generate instanceId once per boot. if (Controller::instanceId == ""){ - srand(time(NULL)); + srand(mix(clock(), time(0), getpid())); do{ Controller::instanceId += (char)(64 + rand() % 62); - }while(Controller::instanceId.size() < 16); + }while (Controller::instanceId.size() < 16); } - - //start stats thread + // start stats thread tthread::thread statsThread(Controller::SharedMemStats, &Controller::conf); - //start monitoring thread + // start monitoring thread tthread::thread monitorThread(statusMonitor, 0); - - //start main loop - Controller::conf.serveThreadedSocket(Controller::handleAPIConnection); - //print shutdown reason - std::string shutdown_reason; - if (!Controller::conf.is_active){ - shutdown_reason = "user request (received shutdown signal)"; - }else{ - shutdown_reason = "socket problem (API port closed)"; + + // start main loop + while (Controller::conf.is_active){ + Controller::conf.serveThreadedSocket(Controller::handleAPIConnection); + // print shutdown reason + std::string shutdown_reason; + if (!Controller::conf.is_active){ + shutdown_reason = "user request (received shutdown signal)"; + }else{ + shutdown_reason = "socket problem (API port closed)"; + } + if (Controller::restarting){shutdown_reason = "restart (on request)";} + Controller::conf.is_active = false; + Controller::Log("CONF", "Controller shutting down because of " + shutdown_reason); } - if (Controller::restarting){ - shutdown_reason = "restart (on request)"; - } - Controller::conf.is_active = false; - Controller::Log("CONF", "Controller shutting down because of "+shutdown_reason); - //join all joinable threads + // join all joinable threads + HIGH_MSG("Joining stats thread..."); statsThread.join(); + HIGH_MSG("Joining monitor thread..."); monitorThread.join(); - //write config + // write config tthread::lock_guard guard(Controller::logMutex); Controller::writeConfigToDisk(); - //stop all child processes + // stop all child processes Util::Procs::StopAll(); - //give everything some time to print messages + // give everything some time to print messages Util::wait(100); std::cout << "Killed all processes, wrote config to disk. Exiting." << std::endl; - if (Controller::restarting){ - return 42; - } - //close stderr to make the stderr reading thread exit + if (Controller::restarting){return 42;} + // close stderr to make the stderr reading thread exit close(STDERR_FILENO); return 0; } -void handleUSR1(int signum, siginfo_t * sigInfo, void * ignore){ +void handleUSR1(int signum, siginfo_t *sigInfo, void *ignore){ Controller::Log("CONF", "USR1 received - restarting controller"); Controller::restarting = true; - raise(SIGINT); //trigger restart + raise(SIGINT); // trigger restart } ///\brief The controller angel process. -///Starts a forked main_loop in a loop. Yes, you read that right. -int main(int argc, char ** argv){ - Util::Procs::setHandler();//set child handler +/// Starts a forked main_loop in a loop. Yes, you read that right. +int main(int argc, char **argv){ + Util::Procs::setHandler(); // set child handler { struct sigaction new_action; struct sigaction cur_action; @@ -374,7 +421,7 @@ int main(int argc, char ** argv){ FAIL_MSG("Unable to spawn controller process!"); return 2; } - //wait for the process to exit + // wait for the process to exit int status; while (waitpid(pid, &status, 0) != pid && errno == EINTR){ if (Controller::restarting){ @@ -388,7 +435,7 @@ int main(int argc, char ** argv){ } continue; } - //if the exit was clean, don't restart it + // if the exit was clean, don't restart it if (WIFEXITED(status) && (WEXITSTATUS(status) == 0)){ MEDIUM_MSG("Controller shut down cleanly"); break;