Prettified controller + fixed Ctrl+C during startup questions + made 't' answer add protocols + No longer ask to add protocols when none are installed.

This commit is contained in:
Thulinma 2017-07-19 15:03:25 +02:00
parent cba764d8eb
commit 43e7b6b8be

View file

@ -1,80 +1,47 @@
/// \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 /// \file controller.cpp
/// Contains all code for the controller executable. /// Contains all code for the controller executable.
#include "controller_api.h"
#include <stdio.h>
#include <iostream>
#include <ctime>
#include <vector>
#include <sys/stat.h>
#include <sys/wait.h>
#include <mist/config.h>
#include <mist/socket.h>
#include <mist/http_parser.h>
#include <mist/procs.h>
#include <mist/auth.h>
#include <mist/timing.h>
#include <mist/stream.h>
#include <mist/defines.h>
#include <mist/tinythread.h>
#include <mist/shared_memory.h>
#include "controller_storage.h"
#include "controller_streams.h"
#include "controller_capabilities.h" #include "controller_capabilities.h"
#include "controller_connectors.h" #include "controller_connectors.h"
#include "controller_statistics.h" #include "controller_statistics.h"
#include "controller_api.h" #include "controller_storage.h"
#include "controller_streams.h"
#include <ctime>
#include <iostream>
#include <mist/auth.h>
#include <mist/config.h>
#include <mist/defines.h>
#include <mist/http_parser.h>
#include <mist/procs.h>
#include <mist/shared_memory.h>
#include <mist/socket.h>
#include <mist/stream.h>
#include <mist/timing.h>
#include <mist/tinythread.h>
#include <stdio.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <vector>
#ifndef COMPILED_USERNAME #ifndef COMPILED_USERNAME
#define COMPILED_USERNAME "" #define COMPILED_USERNAME ""
#define COMPILED_PASSWORD "" #define COMPILED_PASSWORD ""
#endif #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 /// 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){ static inline char yna(std::string &user_input){
switch (user_input[0]){ switch (user_input[0]){
case 'y': case 'Y': case 'y':
return 'y'; case 'Y': return 'y'; break;
break; case 'n':
case 'n': case 'N': case 'N': return 'n'; break;
return 'n'; case 'a':
break; case 'A': return 'a'; break;
case 'a': case 'A': case 't':
return 'a'; case 'T': return 't'; break;
break; default: return 'x'; break;
case 't': case 'T':
return 't';
break;
default:
return 'x';
break;
} }
} }
@ -92,7 +59,6 @@ void createAccount (std::string account){
} }
} }
/// Status monitoring thread. /// Status monitoring thread.
/// Will check outputs, inputs and converters every five seconds /// Will check outputs, inputs and converters every five seconds
void statusMonitor(void *np){ void statusMonitor(void *np){
@ -104,12 +70,14 @@ void statusMonitor(void * np){
tthread::lock_guard<tthread::mutex> guard(Controller::configMutex); tthread::lock_guard<tthread::mutex> guard(Controller::configMutex);
bool changed = false; bool changed = false;
// checks online protocols, reports changes to status // checks online protocols, reports changes to status
changed |= Controller::CheckProtocols(Controller::Storage["config"]["protocols"], Controller::capabilities); changed |= Controller::CheckProtocols(Controller::Storage["config"]["protocols"],
Controller::capabilities);
// checks stream statuses, reports changes to status // checks stream statuses, reports changes to status
changed |= Controller::CheckAllStreams(Controller::Storage["streams"]); changed |= Controller::CheckAllStreams(Controller::Storage["streams"]);
// check if the config semaphore is stuck, by trying to lock it for 5 attempts of 1 second... // 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()){ 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. // 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."); WARN_MSG("Configuration semaphore was stuck. Force-unlocking it and re-writing config.");
changed = true; changed = true;
@ -130,37 +98,79 @@ void statusMonitor(void * np){
configLock.unlink(); 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. ///\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::isTerminal = Controller::isColorized = isatty(fileno(stdin));
Controller::Storage = JSON::fromFile("config.json"); 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"]; stored_port["default"] = Controller::Storage["config"]["controller"]["port"];
if ( !stored_port["default"]){ if (!stored_port["default"]){stored_port["default"] = 4242;}
stored_port["default"] = 4242; JSON::Value stored_interface = JSON::fromString(
} "{\"long\":\"interface\", \"short\":\"i\", \"arg\":\"string\", \"help\":\"Interface address "
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.\"}"); "to listen on, or 0.0.0.0 for all available interfaces.\"}");
stored_interface["default"] = Controller::Storage["config"]["controller"]["interface"]; stored_interface["default"] = Controller::Storage["config"]["controller"]["interface"];
if ( !stored_interface["default"]){ if (!stored_interface["default"]){stored_interface["default"] = "0.0.0.0";}
stored_interface["default"] = "0.0.0.0"; JSON::Value stored_user =
} JSON::fromString("{\"long\":\"username\", \"short\":\"u\", \"arg\":\"string\", "
JSON::Value stored_user = JSON::fromString("{\"long\":\"username\", \"short\":\"u\", \"arg\":\"string\", \"help\":\"Username to transfer privileges to, default is root.\"}"); "\"help\":\"Username to transfer privileges to, default is root.\"}");
stored_user["default"] = Controller::Storage["config"]["controller"]["username"]; stored_user["default"] = Controller::Storage["config"]["controller"]["username"];
if ( !stored_user["default"]){ if (!stored_user["default"]){stored_user["default"] = "root";}
stored_user["default"] = "root";
}
Controller::conf.addOption("port", stored_port); Controller::conf.addOption("port", stored_port);
Controller::conf.addOption("interface", stored_interface); Controller::conf.addOption("interface", stored_interface);
Controller::conf.addOption("username", stored_user); 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(
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\"}")); "account", JSON::fromString("{\"long\":\"account\", \"short\":\"a\", \"arg\":\"string\" "
Controller::conf.addOption("configFile", JSON::fromString("{\"long\":\"config\", \"short\":\"c\", \"arg\":\"string\" \"default\":\"config.json\", \"help\":\"Specify a config file other than default.\"}")); "\"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); Controller::conf.parseArgs(argc, argv);
if (Controller::conf.getString("logfile") != ""){ if (Controller::conf.getString("logfile") != ""){
// open logfile, dup stdout to logfile // open logfile, dup stdout to logfile
int output = open(Controller::conf.getString("logfile").c_str(),O_APPEND|O_CREAT|O_WRONLY,S_IRWXU); int output =
open(Controller::conf.getString("logfile").c_str(), O_APPEND | O_CREAT | O_WRONLY, S_IRWXU);
if (output < 0){ if (output < 0){
DEBUG_MSG(DLVL_ERROR, "Could not redirect output to %s: %s",Controller::conf.getString("logfile").c_str(),strerror(errno)); DEBUG_MSG(DLVL_ERROR, "Could not redirect output to %s: %s",
Controller::conf.getString("logfile").c_str(), strerror(errno));
return 7; return 7;
}else{ }else{
Controller::isTerminal = Controller::isColorized = false; Controller::isTerminal = Controller::isColorized = false;
@ -172,7 +182,9 @@ int main_loop(int argc, char ** argv){
time(&rawtime); time(&rawtime);
timeinfo = localtime(&rawtime); timeinfo = localtime(&rawtime);
strftime(buffer, 25, "%c", timeinfo); strftime(buffer, 25, "%c", timeinfo);
std::cerr << std::endl << std::endl <<"!----MistServer Started at " << buffer << " ----!" << std::endl; std::cerr << std::endl
<< std::endl
<< "!----MistServer Started at " << buffer << " ----!" << std::endl;
} }
} }
// reload config from config file // reload config from config file
@ -189,23 +201,26 @@ int main_loop(int argc, char ** argv){
} }
} }
if (Controller::conf.getOption("debug", true).size() > 1){ if (Controller::conf.getOption("debug", true).size() > 1){
Controller::Storage["config"]["debug"] = Controller::conf.getInteger("debug"); 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(); Util::Config::printDebugLevel = Controller::Storage["config"]["debug"].asInt();
} }
// check for port, interface and username in arguments // check for port, interface and username in arguments
// if they are not there, take them from config file, if there // if they are not there, take them from config file, if there
if (Controller::Storage["config"]["controller"]["port"]){ 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"]){ 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"]){ 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); IPC::semaphore configLock(SEM_CONF, O_CREAT | O_RDWR, ACCESSPERMS, 1);
@ -214,13 +229,14 @@ int main_loop(int argc, char ** argv){
Controller::writeConfig(); Controller::writeConfig();
Controller::checkAvailProtocols(); Controller::checkAvailProtocols();
createAccount(Controller::conf.getString("account")); createAccount(Controller::conf.getString("account"));
Controller::conf.activate(); // activate early, so threads aren't killed.
// if a terminal is connected and we're not logging to file // if a terminal is connected and we're not logging to file
if (Controller::isTerminal){ if (Controller::isTerminal){
// check for username // check for username
if (!Controller::Storage.isMember("account") || Controller::Storage["account"].size() < 1){ if (!Controller::Storage.isMember("account") || Controller::Storage["account"].size() < 1){
std::string in_string = ""; 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 << "Account not set, do you want to create an account? (y)es, (n)o, (a)bort: ";
std::cout.flush(); std::cout.flush();
std::getline(std::cin, in_string); std::getline(std::cin, in_string);
@ -228,26 +244,48 @@ int main_loop(int argc, char ** argv){
case 'y':{ case 'y':{
// create account // create account
std::string usr_string = ""; std::string usr_string = "";
while(!(Controller::Storage.isMember("account") && Controller::Storage["account"].size() > 0)){ while (!(Controller::Storage.isMember("account") &&
std::cout << "Please type in the username, a colon and a password in the following format; username:password" << std::endl << ": "; 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::cout.flush();
std::getline(std::cin, usr_string); std::getline(std::cin, usr_string);
createAccount(usr_string); createAccount(usr_string);
} }
} }break;
break; case 'a':
case 'a': return 0; //abort bootup return 0; // abort bootup
case 't': case 't':{
createAccount("test:test"); createAccount("test:test");
break; 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;
} }
} }
} }
// check for protocols // check for protocols
if ( !Controller::Storage.isMember("config") || !Controller::Storage["config"].isMember("protocols") || Controller::Storage["config"]["protocols"].size() < 1){ if ((Controller::capabilities["connectors"].size()) &&
(!Controller::Storage.isMember("config") ||
!Controller::Storage["config"].isMember("protocols") ||
Controller::Storage["config"]["protocols"].size() < 1)){
std::string in_string = ""; std::string in_string = "";
while(yna(in_string) == 'x'){ 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
<< "Protocols not set, do you want to enable default protocols? (y)es, (n)o, (a)bort: ";
std::cout.flush(); std::cout.flush();
std::getline(std::cin, in_string); std::getline(std::cin, in_string);
if (yna(in_string) == 'y'){ if (yna(in_string) == 'y'){
@ -272,35 +310,45 @@ int main_loop(int argc, char ** argv){
std::string web_port = JSON::Value((long long)Controller::conf.getInteger("port")).asString(); std::string web_port = JSON::Value((long long)Controller::conf.getInteger("port")).asString();
// check for username // check for username
if (!Controller::Storage.isMember("account") || Controller::Storage["account"].size() < 1){ 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."); 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 // check for protocols
if ( !Controller::Storage.isMember("config") || !Controller::Storage["config"].isMember("protocols") || Controller::Storage["config"]["protocols"].size() < 1){ if (!Controller::Storage.isMember("config") ||
Controller::Log("CONF", "No protocols enabled, remember to set them up through the web interface on port "+web_port+" or API."); !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 // check for streams - regardless of logfile setting
if (!Controller::Storage.isMember("streams") || Controller::Storage["streams"].size() < 1){ 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",
"No streams configured, remember to set up streams through the web interface on port " +
web_port + " or API.");
} }
} }
Controller::Log("CONF", "Controller started"); 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 == ""){ if (Controller::instanceId == ""){
srand(time(NULL)); srand(mix(clock(), time(0), getpid()));
do{ do{
Controller::instanceId += (char)(64 + rand() % 62); 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); tthread::thread statsThread(Controller::SharedMemStats, &Controller::conf);
// start monitoring thread // start monitoring thread
tthread::thread monitorThread(statusMonitor, 0); tthread::thread monitorThread(statusMonitor, 0);
// start main loop // start main loop
while (Controller::conf.is_active){
Controller::conf.serveThreadedSocket(Controller::handleAPIConnection); Controller::conf.serveThreadedSocket(Controller::handleAPIConnection);
// print shutdown reason // print shutdown reason
std::string shutdown_reason; std::string shutdown_reason;
@ -309,13 +357,14 @@ int main_loop(int argc, char ** argv){
}else{ }else{
shutdown_reason = "socket problem (API port closed)"; shutdown_reason = "socket problem (API port closed)";
} }
if (Controller::restarting){ if (Controller::restarting){shutdown_reason = "restart (on request)";}
shutdown_reason = "restart (on request)";
}
Controller::conf.is_active = false; Controller::conf.is_active = false;
Controller::Log("CONF", "Controller shutting down because of " + shutdown_reason); 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(); statsThread.join();
HIGH_MSG("Joining monitor thread...");
monitorThread.join(); monitorThread.join();
// write config // write config
tthread::lock_guard<tthread::mutex> guard(Controller::logMutex); tthread::lock_guard<tthread::mutex> guard(Controller::logMutex);
@ -325,9 +374,7 @@ int main_loop(int argc, char ** argv){
// give everything some time to print messages // give everything some time to print messages
Util::wait(100); Util::wait(100);
std::cout << "Killed all processes, wrote config to disk. Exiting." << std::endl; std::cout << "Killed all processes, wrote config to disk. Exiting." << std::endl;
if (Controller::restarting){ if (Controller::restarting){return 42;}
return 42;
}
// close stderr to make the stderr reading thread exit // close stderr to make the stderr reading thread exit
close(STDERR_FILENO); close(STDERR_FILENO);
return 0; return 0;