From 2a5c965e62f22adf6780d17fcfcb383da999bd8b Mon Sep 17 00:00:00 2001 From: Emmanuel Durand Date: Sun, 1 Mar 2020 08:35:46 -0500 Subject: [PATCH] Moved argument parsing in splash-app.cpp instead of World --- src/core/constants.h | 2 + src/core/root_object.cpp | 12 +- src/core/root_object.h | 34 +- src/core/scene.cpp | 8 +- src/core/scene.h | 53 ++- src/core/world.cpp | 432 ++++++--------------- src/core/world.h | 34 +- src/splash-app.cpp | 250 +++++++++++- src/splash.h | 29 -- tests/CMakeLists.txt | 2 + tests/unit_tests/core/attribute.cpp | 2 +- tests/unit_tests/core/base_object.cpp | 1 - tests/unit_tests/core/scene.cpp | 20 + tests/unit_tests/core/world.cpp | 22 ++ tests/unit_tests/utils/jsonutils.cpp | 1 - tests/unit_tests/utils/resizable_array.cpp | 2 +- 16 files changed, 489 insertions(+), 415 deletions(-) delete mode 100644 src/splash.h create mode 100644 tests/unit_tests/core/scene.cpp create mode 100644 tests/unit_tests/core/world.cpp diff --git a/src/core/constants.h b/src/core/constants.h index 6c3011f5..a26e7bf9 100644 --- a/src/core/constants.h +++ b/src/core/constants.h @@ -36,6 +36,8 @@ #define SPLASH_FILE_CONFIGURATION "splashConfiguration" #define SPLASH_FILE_PROJECT "splashProject" +#define SPLASH_CAMERA_LINK "__camera_link" + #include #include diff --git a/src/core/root_object.cpp b/src/core/root_object.cpp index 43b970eb..ffe4a48e 100644 --- a/src/core/root_object.cpp +++ b/src/core/root_object.cpp @@ -14,7 +14,17 @@ namespace Splash /**************/ RootObject::RootObject() - : _factory(unique_ptr(new Factory(this))) + : _context(Context()) + , _factory(unique_ptr(new Factory(this))) +{ + registerAttributes(); + initializeTree(); +} + +/**************/ +RootObject::RootObject(Context context) + : _context(context) + , _factory(unique_ptr(new Factory(this))) { registerAttributes(); initializeTree(); diff --git a/src/core/root_object.h b/src/core/root_object.h index a619be6f..8795edea 100644 --- a/src/core/root_object.h +++ b/src/core/root_object.h @@ -29,6 +29,7 @@ #include #include #include +#include #include #include @@ -56,6 +57,28 @@ class RootObject : public BaseObject friend UserInput; public: + struct Context + { + bool defaultConfigurationFile{true}; + bool hide{false}; + bool info{false}; + bool log2file{false}; + bool childProcess{false}; + bool spawnSubprocesses{true}; + bool unitTest{false}; + std::string executableName{""}; + std::string executablePath{""}; + std::string socketPrefix{""}; + std::string childSceneName{"scene"}; + std::string configurationFile{std::string(DATADIR) + "splash.json"}; + std::optional pythonScriptPath{}; + Values pythonArgs{}; +#if HAVE_LINUX + std::optional forcedDisplay{}; + std::string displayServer{"0"}; +#endif + }; + enum Command { callObject, @@ -68,6 +91,12 @@ class RootObject : public BaseObject */ RootObject(); + /** + * Constructor + * \param context Context for the creation of this RootObject + */ + explicit RootObject(Context context); + /** * Destructor */ @@ -112,7 +141,7 @@ class RootObject : public BaseObject * Get the socket prefix * \return Return the socket prefx */ - std::string getSocketPrefix() const { return _linkSocketPrefix; } + std::string getSocketPrefix() const { return _context.socketPrefix; } /** * \brief Get the configuration path @@ -183,13 +212,14 @@ class RootObject : public BaseObject void signalBufferObjectUpdated(); protected: + Context _context{}; + Tree::Root _tree{}; //!< Configuration / status tree, shared between all root objects std::unordered_map _treeCallbackIds{}; std::unordered_map _attributeCallbackHandles{}; std::unique_ptr _factory{}; //!< Object factory std::unique_ptr _link{}; //!< Link object for communicatin between World and Scene - std::string _linkSocketPrefix{""}; //!< Prefix to add to shared memory socket paths Values _lastAnswerReceived{}; //!< Holds the last answer received through the link std::condition_variable _answerCondition{}; diff --git a/src/core/scene.cpp b/src/core/scene.cpp index c6fb46c8..5b1862a0 100644 --- a/src/core/scene.cpp +++ b/src/core/scene.cpp @@ -66,16 +66,16 @@ bool Scene::getHasNVSwapGroup() } /*************/ -Scene::Scene(const string& name, const string& socketPrefix) - : _objectLibrary(dynamic_cast(this)) +Scene::Scene(Context context) + : RootObject(context) + , _objectLibrary(dynamic_cast(this)) { #ifdef DEBUG Log::get() << Log::DEBUGGING << "Scene::Scene - Scene created successfully" << Log::endl; #endif _isRunning = true; - _name = name; - _linkSocketPrefix = socketPrefix; + _name = _context.childSceneName; registerAttributes(); initializeTree(); diff --git a/src/core/scene.h b/src/core/scene.h index 7551de67..b4f1bd40 100644 --- a/src/core/scene.h +++ b/src/core/scene.h @@ -56,19 +56,18 @@ class Scene : public RootObject public: /** - * \brief Constructor - * \param name Scene name - * \param autoRun If true, the Scene will start without waiting for a start message from the World + * Constructor + * \param context Context for the creation of this Scene object */ - Scene(const std::string& name = "Splash", const std::string& socketPrefix = ""); + explicit Scene(Context context); /** - * \brief Destructor + * Destructor */ ~Scene() override; /** - * \brief Add an object of the given type, with the given name + * Add an object of the given type, with the given name * \param type Object type * \param name Object name * \return Return a shared pointer to the created object @@ -76,14 +75,14 @@ class Scene : public RootObject std::shared_ptr addObject(const std::string& type, const std::string& name = ""); /** - * \brief Add an object ghosting one in another Scene. Used in master Scene for controlling purposes + * Add an object ghosting one in another Scene. Used in master Scene for controlling purposes * \param type Object type * \param name Object name */ void addGhost(const std::string& type, const std::string& name = ""); /** - * \brief Get a glfw window sharing the same context as _mainWindow + * Get a glfw window sharing the same context as _mainWindow * \param name Window name * \return Return a shared pointer to the new window */ @@ -108,7 +107,7 @@ class Scene : public RootObject ObjectLibrary& getObjectLibrary() { return _objectLibrary; } /** - * \brief Get the status of the scene + * Get the status of the scene * \return Return true if all is well */ bool getStatus() const { return _status; } @@ -120,25 +119,25 @@ class Scene : public RootObject int getSwapInterval() const { return _swapInterval; } /** - * \brief Check whether it is initialized + * Check whether it is initialized * \return Return true if the Scene is initialized */ bool isInitialized() const { return _isInitialized; } /** - * \brief Ask whether the scene is Master or not + * Ask whether the scene is Master or not * \return Return true if the Scene is master */ bool isMaster() const { return _isMaster; } /** - * \brief Check wether the scene is running + * Check wether the scene is running * \return Return true if the scene runs */ bool isRunning() const { return _isRunning; } /** - * \brief Link an object to another, base on their types + * Link an object to another, base on their types * \param first Child object * \param second Parent object * \return Return true if the linking succeeded @@ -147,7 +146,7 @@ class Scene : public RootObject bool link(const std::shared_ptr& first, const std::shared_ptr& second); /** - * \brief Unlink two objects. This always succeeds + * Unlink two objects. This always succeeds * \param first Child object * \param second Parent object */ @@ -155,36 +154,36 @@ class Scene : public RootObject void unlink(const std::shared_ptr& first, const std::shared_ptr& second); /** - * \brief Remove an object + * Remove an object * \param name Object name */ void remove(const std::string& name); /** - * \brief Render everything + * Render everything */ void render(); /** - * \brief Main loop for the scene + * Main loop for the scene */ void run(); /** - * \brief Set the Scene as the master one + * Set the Scene as the master one * \param configFilePath File path for the loaded configuration */ void setAsMaster(const std::string& configFilePath = ""); /** - * \brief Set a message to be sent to the world + * Set a message to be sent to the world * \param message Message type to send, which should correspond to a World attribute * \param value Message content */ void sendMessageToWorld(const std::string& message, const Values& value = {}); /** - * \brief Set a message to be sent to the world, and wait for the World to send an answer + * Set a message to be sent to the world, and wait for the World to send an answer * \param message Message type to send, which should correspond to a World attribute * \param value Message content * \param timeout Timeout in microseconds @@ -240,37 +239,37 @@ class Scene : public RootObject static std::vector _ghostableTypes; /** - * \brief Find which OpenGL version is available (from a predefined list) + * Find which OpenGL version is available (from a predefined list) * \return Return MAJOR and MINOR */ std::vector findGLVersion(); /** - * \brief Set up the context and everything + * Set up the context and everything * \param name Scene name */ void init(const std::string& name); /** - * \brief Computes and store the duration of a frame at the refresh rate of the primary monitor + * Computes and store the duration of a frame at the refresh rate of the primary monitor * \return The duration of a frame at the refresh rate of the primary monitor in microseconds */ unsigned long long updateTargetFrameDuration(); /** - * \brief Callback for GLFW errors + * Callback for GLFW errors * \param code Error code * \param msg Associated error message */ static void glfwErrorCallback(int code, const char* msg); /** - * \brief Callback for GL errors and warnings + * Callback for GL errors and warnings */ static void glMsgCallback(GLenum, GLenum, GLuint, GLenum, GLsizei, const GLchar*, void*); /** - * \brief Register new attributes + * Register new attributes */ void registerAttributes(); @@ -280,7 +279,7 @@ class Scene : public RootObject void initializeTree(); /** - * \brief Update the various inputs (mouse, keyboard...) + * Update the various inputs (mouse, keyboard...) */ void updateInputs(); }; diff --git a/src/core/world.cpp b/src/core/world.cpp index d17db2d8..1b362ee7 100644 --- a/src/core/world.cpp +++ b/src/core/world.cpp @@ -26,18 +26,29 @@ using namespace glm; using namespace std; -#define SPLASH_CAMERA_LINK "__camera_link" - namespace Splash { /*************/ World* World::_that; /*************/ -World::World(int argc, char** argv) +World::World(Context context) + : RootObject(context) { - parseArguments(argc, argv); - init(); + _name = "world"; + + _that = this; + _signals.sa_handler = leave; + _signals.sa_flags = 0; + sigaction(SIGINT, &_signals, nullptr); + sigaction(SIGTERM, &_signals, nullptr); + + if (_context.socketPrefix.empty()) + _context.socketPrefix = to_string(static_cast(getpid())); + _link = make_unique(this, _name); + + registerAttributes(); + initializeTree(); } /*************/ @@ -51,21 +62,65 @@ World::~World() } /*************/ -void World::run() +bool World::applyContext() { - // If set to run as a child process, only create a scene which will wait for instructions - // from the master process - if (_runAsChild) + if (_context.info) { - Log::get() << Log::MESSAGE << "World::" << __FUNCTION__ << " - Creating child Scene with name " << _childSceneName << Log::endl; - - Scene scene(_childSceneName, _linkSocketPrefix); - scene.run(); - - return; + auto descriptions = getObjectsAttributesDescriptions(); + cout << descriptions << endl; + return false; } - applyConfig(); + if (_context.pythonScriptPath) + { + auto pythonScriptPath = *_context.pythonScriptPath; + auto pythonArgs = _context.pythonArgs; + + // The Python script will be added once the loop runs + addTask([=]() { + Log::get() << Log::MESSAGE + << "World::parseArguments - Adding Python script from command " + "line argument: " + << pythonScriptPath << Log::endl; + + auto pythonObjectName = string("_pythonArgScript"); + if (!_nameRegistry.registerName(pythonObjectName)) + pythonObjectName = _nameRegistry.generateName("_pythonArgScript"); + + sendMessage(SPLASH_ALL_PEERS, "addObject", {"python", pythonObjectName, _masterSceneName}); + sendMessage(pythonObjectName, "savable", {false}); + sendMessage(pythonObjectName, "args", {pythonArgs}); + sendMessage(pythonObjectName, "file", {pythonScriptPath}); + }); + } + + if (_context.log2file) + addTask([=] { setAttribute("logToFile", {_context.log2file}); }); + + if (_context.defaultConfigurationFile) + Log::get() << Log::MESSAGE << "No filename specified, loading default file" << Log::endl; + else + Log::get() << Log::MESSAGE << "Loading file " << _context.configurationFile << Log::endl; + + Json::Value config; + _status &= loadConfig(_context.configurationFile, config); + + if (_status) + _config = config; + else if (!_context.unitTest) + return false; + + return true; +} + +/*************/ +void World::run() +{ + if (!applyContext()) + return; + + if (!applyConfig()) + return; while (true) { @@ -157,7 +212,7 @@ void World::addToWorld(const string& type, const string& name) } /*************/ -void World::applyConfig() +bool World::applyConfig() { lock_guard lockConfiguration(_configurationMutex); @@ -172,7 +227,7 @@ void World::applyConfig() if (!_config.isMember("scenes")) { Log::get() << Log::ERROR << "World::" << __FUNCTION__ << " - Error while getting scenes configuration" << Log::endl; - return; + return false; } const Json::Value& scenes = _config["scenes"]; @@ -182,7 +237,7 @@ void World::applyConfig() string sceneDisplay = scenes[sceneName].isMember("display") ? scenes[sceneName]["display"].asString() : ""; bool spawn = scenes[sceneName].isMember("spawn") ? scenes[sceneName]["spawn"].asBool() : true; - if (!addScene(sceneName, sceneDisplay, sceneAddress, spawn && _spawnSubprocesses)) + if (!addScene(sceneName, sceneDisplay, sceneAddress, spawn && _context.spawnSubprocesses)) continue; // Set the remaining parameters @@ -218,10 +273,11 @@ void World::applyConfig() setAttribute("addObject", {objects[objectName]["type"].asString(), objectName, scene.first, false}); } - sendMessage(SPLASH_ALL_PEERS, "runInBackground", {_runInBackground}); + sendMessage(SPLASH_ALL_PEERS, "runInBackground", {_context.hide}); } - // Make sure all objects have been created in every Scene, by sending a sync message + // Make sure all objects have been created in every Scene, by sending a sync + // message for (const auto& s : _scenes) sendMessageWithAnswer(s.first, "sync"); @@ -278,7 +334,8 @@ void World::applyConfig() } // Lastly, configure this very World - // This happens last as some parameters are sent to Scenes (like blending computation) + // This happens last as some parameters are sent to Scenes (like blending + // computation) if (_config.isMember("world")) { const Json::Value jsWorld = _config["world"]; @@ -296,7 +353,7 @@ void World::applyConfig() catch (...) { Log::get() << Log::ERROR << "Exception caught while applying configuration from file " << _configFilename << Log::endl; - return; + return false; } // Also, enable the master clock if it was not enabled @@ -318,6 +375,8 @@ void World::applyConfig() break; } } + + return true; } /*************/ @@ -345,15 +404,15 @@ bool World::addScene(const std::string& sceneName, const std::string& sceneDispl if (regex_match(sceneDisplay, match, regDisplayFull)) display = "DISPLAY=" + sceneDisplay; else if (regex_match(sceneDisplay, match, regDisplayInt)) - display = "DISPLAY=:" + _displayServer + "." + sceneDisplay; + display = "DISPLAY=:" + _context.displayServer + "." + sceneDisplay; } - if (!_forcedDisplay.empty()) + if (_context.forcedDisplay) { - if (regex_match(_forcedDisplay, match, regDisplayFull)) - display = "DISPLAY=" + _forcedDisplay; - else if (regex_match(_forcedDisplay, match, regDisplayInt)) - display = "DISPLAY=:" + _displayServer + "." + _forcedDisplay; + if (regex_match(*_context.forcedDisplay, match, regDisplayFull)) + display = "DISPLAY=" + *_context.forcedDisplay; + else if (regex_match(*_context.forcedDisplay, match, regDisplayInt)) + display = "DISPLAY=:" + _context.displayServer + "." + *_context.forcedDisplay; } #endif @@ -366,7 +425,9 @@ bool World::addScene(const std::string& sceneName, const std::string& sceneDispl if (worldDisplay.size() > 0 && display.find(worldDisplay) == display.size() - worldDisplay.size() && !_innerScene) { Log::get() << Log::MESSAGE << "World::" << __FUNCTION__ << " - Starting an inner Scene" << Log::endl; - _innerScene = make_shared(sceneName, _linkSocketPrefix); + auto sceneContext = _context; + sceneContext.childSceneName = sceneName; + _innerScene = make_shared(sceneContext); _innerSceneThread = thread([&]() { _innerScene->run(); }); } else @@ -374,17 +435,17 @@ bool World::addScene(const std::string& sceneName, const std::string& sceneDispl // Spawn a new process containing this Scene Log::get() << Log::MESSAGE << "World::" << __FUNCTION__ << " - Starting a Scene in another process" << Log::endl; - string cmd = _currentExePath; + string cmd = _context.executablePath; string debug = (Log::get().getVerbosity() == Log::DEBUGGING) ? "-d" : ""; string timer = Timer::get().isDebug() ? "-t" : ""; string slave = "--child"; string xauth = "XAUTHORITY=" + Utils::getHomePath() + "/.Xauthority"; vector argv = {const_cast(cmd.c_str()), const_cast(slave.c_str())}; - if (!_linkSocketPrefix.empty()) + if (!_context.socketPrefix.empty()) { argv.push_back((char*)"--prefix"); - argv.push_back(const_cast(_linkSocketPrefix.c_str())); + argv.push_back(const_cast(_context.socketPrefix.c_str())); } if (!debug.empty()) argv.push_back(const_cast(debug.c_str())); @@ -496,7 +557,8 @@ string World::getObjectsAttributesDescriptions() root["world"][d[0].as()] = formatDescription(d[1].as(), d[2].as()); } - setlocale(LC_NUMERIC, "C"); // Needed to make sure numbers are written with commas + setlocale(LC_NUMERIC, + "C"); // Needed to make sure numbers are written with commas string jsonString; jsonString = root.toStyledString(); @@ -506,7 +568,8 @@ string World::getObjectsAttributesDescriptions() /*************/ void World::saveConfig() { - setlocale(LC_NUMERIC, "C"); // Needed to make sure numbers are written with commas + setlocale(LC_NUMERIC, + "C"); // Needed to make sure numbers are written with commas // Local objects configuration can differ from the scenes objects, // as their type is not necessarily identical @@ -546,7 +609,8 @@ void World::saveConfig() _config["world"][attr] = worldConfiguration[attr]; } - setlocale(LC_NUMERIC, "C"); // Needed to make sure numbers are written with commas + setlocale(LC_NUMERIC, + "C"); // Needed to make sure numbers are written with commas ofstream out(_configFilename, ios::binary); out << _config.toStyledString(); out.close(); @@ -557,15 +621,16 @@ void World::saveProject(const string& filename) { try { - setlocale(LC_NUMERIC, "C"); // Needed to make sure numbers are written with commas + setlocale(LC_NUMERIC, + "C"); // Needed to make sure numbers are written with commas auto root = Json::Value(); // Haha, auto root... root["description"] = SPLASH_FILE_PROJECT; root["version"] = string(PACKAGE_VERSION); root["links"] = Json::Value(); - // Here, we don't care about which Scene holds which object, as objects with the - // same name in different Scenes are necessarily clones + // Here, we don't care about which Scene holds which object, as objects with + // the same name in different Scenes are necessarily clones std::set> existingLinks{}; // We keep a list of already existing links for (auto& s : _scenes) { @@ -594,7 +659,8 @@ void World::saveProject(const string& filename) for (const auto& member : config["objects"].getMemberNames()) { - // We only save configuration for non Scene-specific objects, which are one of the following: + // We only save configuration for non Scene-specific objects, which are + // one of the following: if (!_factory->isProjectSavable(config["objects"][member]["type"].asString())) continue; @@ -649,29 +715,6 @@ bool World::handleSerializedObject(const string& name, const shared_ptr(getpid())); - _link = make_unique(this, _name); - - registerAttributes(); - initializeTree(); - } -} - /*************/ void World::leave(int /*signal_value*/) { @@ -749,8 +792,8 @@ bool World::loadProject(const string& filename) _configurationPath = Utils::getPathFromFilePath(filename); // Now, we apply the configuration depending on the current state - // Meaning, we replace objects with the same name, create objects with non-existing name, - // and delete objects which are not in the partial config + // Meaning, we replace objects with the same name, create objects with + // non-existing name, and delete objects which are not in the partial config // Delete existing objects for (const auto& s : _scenes) @@ -833,261 +876,6 @@ bool World::loadProject(const string& filename) } } -/*************/ -void World::parseArguments(int argc, char** argv) -{ - auto printWelcome = []() { - cout << endl; - cout << "\t \033[33;1m- Splash -\033[0m" << endl; - cout << "\t\033[1m- Modular multi-output video mapper -\033[0m" << endl; - cout << "\t \033[1m- Version " << PACKAGE_VERSION << " -\033[0m" << endl; - cout << endl; - }; - - // Get the executable directory - _splashExecutable = argv[0]; - _currentExePath = Utils::getCurrentExecutablePath(); - _executionPath = Utils::getPathFromExecutablePath(_splashExecutable); - - // Parse the other args - string filename = string(DATADIR) + "splash.json"; - bool defaultFile = true; - - while (true) - { - static struct option longOptions[] = { - {"debug", no_argument, 0, 'd'}, -#if HAVE_LINUX - {"forceDisplay", required_argument, 0, 'D'}, - {"displayServer", required_argument, 0, 'S'}, -#endif - {"help", no_argument, 0, 'h'}, - {"hide", no_argument, 0, 'H'}, - {"info", no_argument, 0, 'i'}, - {"log2file", no_argument, 0, 'l'}, - {"open", required_argument, 0, 'o'}, - {"prefix", required_argument, 0, 'p'}, - {"python", required_argument, 0, 'P'}, - {"silent", no_argument, 0, 's'}, - {"timer", no_argument, 0, 't'}, - {"child", no_argument, 0, 'c'}, - {"doNotSpawn", no_argument, 0, 'x'}, - {0, 0, 0, 0} - }; - - int optionIndex = 0; - auto ret = getopt_long(argc, argv, "+cdD:S:hHilo:p:P:stx", longOptions, &optionIndex); - - if (ret == -1) - break; - - switch (ret) - { - default: - case 'h': - { - printWelcome(); - - cout << "Basic usage: splash [options] [config.json] -- [python script argument]" << endl; - cout << "Options:" << endl; - cout << "\t-o (--open) [filename] : set [filename] as the configuration file to open" << endl; - cout << "\t-d (--debug) : activate debug messages (if Splash was compiled with -DDEBUG)" << endl; - cout << "\t-t (--timer) : activate more timers, at the cost of performance" << endl; -#if HAVE_LINUX - cout << "\t-D (--forceDisplay) : force the display on which to show all windows" << endl; - cout << "\t-S (--displayServer) : set the display server ID" << endl; -#endif - cout << "\t-s (--silent) : disable all messages" << endl; - cout << "\t-i (--info) : get description for all objects attributes" << endl; - cout << "\t-H (--hide) : run Splash in background" << endl; - cout << "\t-P (--python) : add the given Python script to the loaded configuration" << endl; - cout << " any argument after -- will be sent to the script" << endl; - cout << "\t-l (--log2file) : write the logs to /var/log/splash.log, if possible" << endl; - cout << "\t-p (--prefix) : set the shared memory socket paths prefix (defaults to the PID)" << endl; - cout << "\t-c (--child): run as a child controlled by a master Splash process" << endl; - cout << "\t-x (--doNotSpawn): do not spawn subprocesses, which have to be ran manually" << endl; - cout << endl; - exit(0); - } - case 'd': - { - Log::get().setVerbosity(Log::DEBUGGING); - break; - } - case 'D': - { - auto regDisplayFull = regex("(:[0-9]\\.[0-9])", regex_constants::extended); - auto regDisplayInt = regex("[0-9]", regex_constants::extended); - smatch match; - - _forcedDisplay = string(optarg); - if (regex_match(_forcedDisplay, match, regDisplayFull)) - { - Log::get() << Log::MESSAGE << "World::" << __FUNCTION__ << " - Display forced to " << _forcedDisplay << Log::endl; - } - else if (regex_match(_forcedDisplay, match, regDisplayInt)) - { - Log::get() << Log::MESSAGE << "World::" << __FUNCTION__ << " - Display forced to :0." << _forcedDisplay << Log::endl; - } - else - { - Log::get() << Log::WARNING << "World::" << __FUNCTION__ << " - " << string(optarg) << ": argument expects a positive integer, or a string in the form of \":x.y\"" - << Log::endl; - exit(0); - } - break; - } - case 'S': - { - auto regInt = regex("[0-9]+", regex_constants::extended); - smatch match; - - _displayServer = string(optarg); - if (regex_match(_displayServer, match, regInt)) - { - Log::get() << Log::MESSAGE << "World::" << __FUNCTION__ << " - Display server forced to :" << _displayServer << Log::endl; - } - else - { - Log::get() << Log::WARNING << "World::" << __FUNCTION__ << " - " << string(optarg) << ": argument expects a positive integer" << Log::endl; - exit(0); - } - break; - } - case 'H': - { - _runInBackground = true; - break; - } - case 'P': - { - auto pythonScriptPath = Utils::getFullPathFromFilePath(string(optarg), Utils::getCurrentWorkingDirectory()); - - // Build the Python arg list - auto pythonArgs = Values({pythonScriptPath}); - bool isPythonArg = false; - for (int i = 0; i < argc; ++i) - { - if (!isPythonArg && "--" == string(argv[i])) - { - isPythonArg = true; - continue; - } - else if (!isPythonArg) - { - continue; - } - else - { - pythonArgs.push_back(string(argv[i])); - } - } - - // The Python script will be added once the loop runs - addTask([=]() { - Log::get() << Log::MESSAGE << "World::parseArguments - Adding Python script from command line argument: " << pythonScriptPath << Log::endl; - auto pythonObjectName = string("_pythonArgScript"); - if (!_nameRegistry.registerName(pythonObjectName)) - pythonObjectName = _nameRegistry.generateName("_pythonArgScript"); - sendMessage(SPLASH_ALL_PEERS, "addObject", {"python", pythonObjectName, _masterSceneName}); - sendMessage(pythonObjectName, "savable", {false}); - sendMessage(pythonObjectName, "args", {pythonArgs}); - sendMessage(pythonObjectName, "file", {pythonScriptPath}); - }); - break; - } - case 'i': - { - auto descriptions = getObjectsAttributesDescriptions(); - cout << descriptions << endl; - exit(0); - } - case 'l': - { - setAttribute("logToFile", {1}); - addTask([&]() { setAttribute("logToFile", {1}); }); - break; - } - case 'o': - { - defaultFile = false; - filename = string(optarg); - break; - } - case 'p': - { - _linkSocketPrefix = string(optarg); - break; - } - case 's': - { - Log::get().setVerbosity(Log::NONE); - break; - } - case 't': - { - Timer::get().setDebug(true); - break; - } - case 'c': - { - _runAsChild = true; - break; - } - case 'x': - { - _spawnSubprocesses = false; - break; - } - } - } - - // Find last argument index, or "--" - int lastArgIndex = 0; - for (; lastArgIndex < argc; ++lastArgIndex) - if (string(argv[lastArgIndex]) == "--") - break; - - string lastArg = ""; - if (optind < lastArgIndex) - lastArg = string(argv[optind]); - - if (_runAsChild) - { - if (!lastArg.empty()) - _childSceneName = lastArg; - } - else - { - printWelcome(); - - if (!lastArg.empty()) - { - filename = lastArg; - defaultFile = false; - } - if (filename != "") - { - Json::Value config; - _status &= loadConfig(filename, config); - - if (_status) - _config = config; - else - exit(0); - } - else - { - exit(0); - } - } - - if (defaultFile && !_runAsChild) - Log::get() << Log::MESSAGE << "No filename specified, loading default file" << Log::endl; - else if (!_runAsChild) - Log::get() << Log::MESSAGE << "Loading file " << filename << Log::endl; -} - /*************/ void World::registerAttributes() { @@ -1219,7 +1007,9 @@ void World::registerAttributes() return true; }, {'s'}); - setAttributeDescription("copyCameraParameters", "Copy the camera parameters from the given configuration file (based on camera names)"); + setAttributeDescription("copyCameraParameters", + "Copy the camera parameters from the given " + "configuration file (based on camera names)"); addAttribute("pong", [&](const Values& args) { diff --git a/src/core/world.h b/src/core/world.h index 177fda40..17ddb552 100644 --- a/src/core/world.h +++ b/src/core/world.h @@ -56,10 +56,9 @@ class World : public RootObject public: /** * Constructor - * \param argc Argument count - * \param argv Arguments value + * \param context Context for the creation of this World object */ - World(int argc, char** argv); + explicit World(Context context); /** * Destructor @@ -78,9 +77,6 @@ class World : public RootObject void run(); private: - std::string _splashExecutable{"splash"}; - std::string _currentExePath{""}; - #if HAVE_PORTAUDIO std::unique_ptr _clock{nullptr}; //!< Master clock from a LTC signal std::string _clockDeviceName{""}; //!< Name of the input sound source for the master clock @@ -93,7 +89,6 @@ class World : public RootObject bool _quit{false}; //!< True if the World should quit static World* _that; //!< Pointer to the World struct sigaction _signals; //!< System signals - std::string _executionPath{""}; std::mutex _configurationMutex; bool _enforceCoreAffinity{false}; //!< If true, World and Scenes have their affinity fixed in specific, separate cores bool _enforceRealtime{false}; //!< If true, realtime scheduling is asked to the system, if possible @@ -109,8 +104,6 @@ class World : public RootObject std::map _scenes; //!< Map holding the PID of the Scene processes std::string _masterSceneName{""}; //!< Name of the master Scene - std::string _displayServer{"0"}; //!< Display server. - std::string _forcedDisplay{""}; //!< Set to force an output display std::string _configurationPath{""}; //!< Path to the configuration file std::string _mediaPath{""}; //!< Default path to the medias @@ -134,9 +127,16 @@ class World : public RootObject void addToWorld(const std::string& type, const std::string& name); /** - * Apply the configuration + * Match the current context + * \return Return true if the context was applied successfully */ - void applyConfig(); + bool applyContext(); + + /** + * Apply the configuration + * \return Return true if the configuration was applied successfully + */ + bool applyConfig(); /** * Spawn a scene given its parameters @@ -188,11 +188,6 @@ class World : public RootObject */ bool handleSerializedObject(const std::string& name, const std::shared_ptr& obj) override; - /** - * Initializes the World - */ - void init(); - /** * Handle the exit signal messages */ @@ -213,13 +208,6 @@ class World : public RootObject */ bool loadProject(const std::string& filename); - /** - * Parse the given arguments - * \param argc Argument count - * \param argv Argument values - */ - void parseArguments(int argc, char** argv); - /** * Callback for GLFW errors */ diff --git a/src/splash-app.cpp b/src/splash-app.cpp index cec2aedf..dccfe36a 100644 --- a/src/splash-app.cpp +++ b/src/splash-app.cpp @@ -22,14 +22,256 @@ * The main program from the Splash suite. */ -#include "./splash.h" +#include +#include +#include +#include +#include + +#include "./core/constants.h" +#include "./core/scene.h" +#include "./core/world.h" +#include "./utils/log.h" +#include "./utils/osutils.h" +#include "./utils/timer.h" using namespace Splash; +using namespace std; + +/*************/ +RootObject::Context parseArguments(int argc, char** argv) +{ + RootObject::Context context; + + // Get the executable directory + context.executableName = argv[0]; + context.executablePath = Utils::getCurrentExecutablePath(); + + // Parse the other args + context.configurationFile = string(DATADIR) + "splash.json"; + + while (true) + { + static struct option longOptions[] = { + {"debug", no_argument, 0, 'd'}, +#if HAVE_LINUX + {"forceDisplay", required_argument, 0, 'D'}, + {"displayServer", required_argument, 0, 'S'}, +#endif + {"help", no_argument, 0, 'h'}, + {"hide", no_argument, 0, 'H'}, + {"info", no_argument, 0, 'i'}, + {"log2file", no_argument, 0, 'l'}, + {"open", required_argument, 0, 'o'}, + {"prefix", required_argument, 0, 'p'}, + {"python", required_argument, 0, 'P'}, + {"silent", no_argument, 0, 's'}, + {"timer", no_argument, 0, 't'}, + {"child", no_argument, 0, 'c'}, + {"doNotSpawn", no_argument, 0, 'x'}, + {0, 0, 0, 0} + }; + + int optionIndex = 0; + auto ret = getopt_long(argc, argv, "+cdD:S:hHilo:p:P:stx", longOptions, &optionIndex); + + if (ret == -1) + break; + + switch (ret) + { + default: + case 'h': + { + cout << "Basic usage: splash [options] [config.json] -- [python script argument]" << endl; + cout << "Options:" << endl; + cout << "\t-o (--open) [filename] : set [filename] as the configuration file to open" << endl; + cout << "\t-d (--debug) : activate debug messages (if Splash was compiled with -DDEBUG)" << endl; + cout << "\t-t (--timer) : activate more timers, at the cost of performance" << endl; +#if HAVE_LINUX + cout << "\t-D (--forceDisplay) : force the display on which to show all windows" << endl; + cout << "\t-S (--displayServer) : set the display server ID" << endl; +#endif + cout << "\t-s (--silent) : disable all messages" << endl; + cout << "\t-i (--info) : get description for all objects attributes" << endl; + cout << "\t-H (--hide) : run Splash in background" << endl; + cout << "\t-P (--python) : add the given Python script to the loaded configuration" << endl; + cout << " any argument after -- will be sent to the script" << endl; + cout << "\t-l (--log2file) : write the logs to /var/log/splash.log, if possible" << endl; + cout << "\t-p (--prefix) : set the shared memory socket paths prefix (defaults to the PID)" << endl; + cout << "\t-c (--child): run as a child controlled by a master Splash process" << endl; + cout << "\t-x (--doNotSpawn): do not spawn subprocesses, which have to be ran manually" << endl; + cout << endl; + exit(0); + } + case 'd': + { + Log::get().setVerbosity(Log::DEBUGGING); + break; + } + case 'D': + { + auto regDisplayFull = regex("(:[0-9]\\.[0-9])", regex_constants::extended); + auto regDisplayInt = regex("[0-9]", regex_constants::extended); + smatch match; + + context.forcedDisplay = string(optarg); + if (regex_match(*context.forcedDisplay, match, regDisplayFull)) + { + Log::get() << Log::MESSAGE << "Splash::" << __FUNCTION__ << " - Display forced to " << *context.forcedDisplay << Log::endl; + } + else if (regex_match(*context.forcedDisplay, match, regDisplayInt)) + { + Log::get() << Log::MESSAGE << "Splash::" << __FUNCTION__ << " - Display forced to :0." << *context.forcedDisplay << Log::endl; + } + else + { + Log::get() << Log::WARNING << "Splash::" << __FUNCTION__ << " - " << string(optarg) << ": argument expects a positive integer, or a string in the form of \":x.y\"" + << Log::endl; + exit(0); + } + break; + } + case 'S': + { + auto regInt = regex("[0-9]+", regex_constants::extended); + smatch match; + + context.displayServer = string(optarg); + if (regex_match(context.displayServer, match, regInt)) + { + Log::get() << Log::MESSAGE << "Splash::" << __FUNCTION__ << " - Display server forced to :" << context.displayServer << Log::endl; + } + else + { + Log::get() << Log::WARNING << "Splash::" << __FUNCTION__ << " - " << string(optarg) << ": argument expects a positive integer" << Log::endl; + exit(0); + } + break; + } + case 'H': + { + context.hide = true; + break; + } + case 'P': + { + context.pythonScriptPath = Utils::getFullPathFromFilePath(string(optarg), Utils::getCurrentWorkingDirectory()); + context.pythonArgs.push_back(*context.pythonScriptPath); + + // Build the Python arg list + bool isPythonArg = false; + for (int i = 0; i < argc; ++i) + { + if (!isPythonArg && "--" == string(argv[i])) + { + isPythonArg = true; + continue; + } + else if (!isPythonArg) + { + continue; + } + else + { + context.pythonArgs.push_back(string(argv[i])); + } + } + break; + } + case 'i': + { + context.info = true; + break; + } + case 'l': + { + context.log2file = true; + break; + } + case 'o': + { + context.defaultConfigurationFile = false; + context.configurationFile = string(optarg); + break; + } + case 'p': + { + context.socketPrefix = string(optarg); + break; + } + case 's': + { + Log::get().setVerbosity(Log::NONE); + break; + } + case 't': + { + Timer::get().setDebug(true); + break; + } + case 'c': + { + context.childProcess = true; + break; + } + case 'x': + { + context.spawnSubprocesses = false; + break; + } + } + } + + // Find last argument index, or "--" + int lastArgIndex = 0; + for (; lastArgIndex < argc; ++lastArgIndex) + if (string(argv[lastArgIndex]) == "--") + break; + + string lastArg = ""; + if (optind < lastArgIndex) + lastArg = string(argv[optind]); + + if (context.childProcess) + { + if (!lastArg.empty()) + context.childSceneName = lastArg; + } + else + { + if (!lastArg.empty()) + { + context.defaultConfigurationFile = false; + context.configurationFile = lastArg; + } + } + + return context; +} /*************/ int main(int argc, char** argv) { - World world(argc, argv); - world.run(); - return world.getStatus(); + auto context = parseArguments(argc, argv); + + if (context.childProcess) + { + Scene scene(context); + scene.run(); + + return 0; + } + else + { + cout << endl; + cout << "\t \033[33;1m- Splash -\033[0m" << endl; + cout << "\t\033[1m- Modular multi-output video mapper -\033[0m" << endl; + cout << "\t \033[1m- Version " << PACKAGE_VERSION << " -\033[0m" << endl; + cout << endl; + + World world(context); + world.run(); + return world.getStatus(); + } } diff --git a/src/splash.h b/src/splash.h deleted file mode 100644 index 9aaad58c..00000000 --- a/src/splash.h +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Copyright (C) 2014 Emmanuel Durand - * - * This file is part of Log. - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Splash is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Splash. If not, see . - */ - -#ifndef SPLASH_H -#define SPLASH_H - -#include "./core/constants.h" -#include "./core/scene.h" -#include "./core/world.h" -#include "./utils/log.h" -#include "./utils/timer.h" - -#endif // SPLASH_H diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 67b9dd43..73c6cc04 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -74,9 +74,11 @@ target_sources(unitTests PRIVATE unit_tests/core/name_registry.cpp unit_tests/core/root_object.cpp unit_tests/core/buffer_object.cpp + unit_tests/core/scene.cpp unit_tests/core/serializer.cpp unit_tests/core/tree.cpp unit_tests/core/value.cpp + unit_tests/core/world.cpp unit_tests/image/image_list.cpp unit_tests/utils/dense_deque.cpp unit_tests/utils/dense_map.cpp diff --git a/tests/unit_tests/core/attribute.cpp b/tests/unit_tests/core/attribute.cpp index 2ccdc7cb..04996723 100644 --- a/tests/unit_tests/core/attribute.cpp +++ b/tests/unit_tests/core/attribute.cpp @@ -1,6 +1,6 @@ #include -#include "./splash.h" +#include "./core/attribute.h" using namespace std; using namespace Splash; diff --git a/tests/unit_tests/core/base_object.cpp b/tests/unit_tests/core/base_object.cpp index 4254884b..39cd35f7 100644 --- a/tests/unit_tests/core/base_object.cpp +++ b/tests/unit_tests/core/base_object.cpp @@ -6,7 +6,6 @@ #include #include "./core/base_object.h" -#include "./splash.h" using namespace std; using namespace Splash; diff --git a/tests/unit_tests/core/scene.cpp b/tests/unit_tests/core/scene.cpp new file mode 100644 index 00000000..46e53d64 --- /dev/null +++ b/tests/unit_tests/core/scene.cpp @@ -0,0 +1,20 @@ +#include + +#include + +#include "./core/scene.h" + +using namespace Splash; +using namespace std::chrono; + +/*************/ +TEST_CASE("Testing creating a Scene") +{ + auto context = RootObject::Context(); + context.unitTest = true; + Scene scene(context); + auto sceneThread = std::thread([&]() { scene.run(); }); + std::this_thread::sleep_for(2s); + scene.setAttribute("quit", {1}); + sceneThread.join(); +} diff --git a/tests/unit_tests/core/world.cpp b/tests/unit_tests/core/world.cpp new file mode 100644 index 00000000..ee5c489d --- /dev/null +++ b/tests/unit_tests/core/world.cpp @@ -0,0 +1,22 @@ +#include + +#include + +#include "./core/world.h" + +using namespace Splash; +using namespace std::chrono; + +/*************/ +TEST_CASE("Testing creating a World and running it") +{ + auto context = RootObject::Context(); + context.spawnSubprocesses = false; + context.configurationFile = ""; + context.unitTest = true; + World world(context); + auto worldThread = std::thread([&]() { world.run(); }); + std::this_thread::sleep_for(2s); + world.setAttribute("quit", {1}); + worldThread.join(); +} diff --git a/tests/unit_tests/utils/jsonutils.cpp b/tests/unit_tests/utils/jsonutils.cpp index 0345a470..0433e267 100644 --- a/tests/unit_tests/utils/jsonutils.cpp +++ b/tests/unit_tests/utils/jsonutils.cpp @@ -3,7 +3,6 @@ #include #include -#include "./splash.h" #include "./utils/jsonutils.h" #include "./utils/osutils.h" diff --git a/tests/unit_tests/utils/resizable_array.cpp b/tests/unit_tests/utils/resizable_array.cpp index b78c16b5..2f800e18 100644 --- a/tests/unit_tests/utils/resizable_array.cpp +++ b/tests/unit_tests/utils/resizable_array.cpp @@ -3,7 +3,7 @@ #include #include -#include "./splash.h" +#include "./utils/resizable_array.h" using namespace std; using namespace Splash;