From 5079f3fc60f809820fb8d781baa080d49cfec385 Mon Sep 17 00:00:00 2001 From: Emil Axelsson Date: Fri, 19 Jul 2019 16:49:54 +0200 Subject: [PATCH] Feature/multiple endpoints (#938) * Add support for multiple endpoints for webserver * Add support for a default endpoint (redirect) in webgui * Always serve prod gui * Update webgui deps --- data/assets/util/static_server.asset | 21 +++ data/assets/util/webgui.asset | 52 ++++--- modules/cefwebgui/cefwebguimodule.cpp | 16 +- modules/cefwebgui/cefwebguimodule.h | 4 + .../server/src/topics/setpropertytopic.cpp | 24 ++- modules/webgui/webguimodule.cpp | 140 ++++++++++++++++-- modules/webgui/webguimodule.h | 18 ++- 7 files changed, 231 insertions(+), 44 deletions(-) create mode 100644 data/assets/util/static_server.asset diff --git a/data/assets/util/static_server.asset b/data/assets/util/static_server.asset new file mode 100644 index 0000000000..94759f5936 --- /dev/null +++ b/data/assets/util/static_server.asset @@ -0,0 +1,21 @@ +local backendHash = "7ca0a34e9c4c065b7bfad0623db0e322bf3e0af9" +local dataProvider = "data.openspaceproject.com/files/webgui" + +local backend = asset.syncedResource({ + Identifier = "WebGuiBackend", + Name = "Web Gui Backend", + Type = "UrlSynchronization", + Url = dataProvider .. "/backend/" .. backendHash .. "/backend.zip" +}) + +asset.onInitialize(function () + -- Unzip the server bundle + dest = backend .. "/backend" + if not openspace.directoryExists(dest) then + openspace.unzipFile(backend .. "/backend.zip", dest, true) + end + + openspace.setPropertyValueSingle( + "Modules.WebGui.ServerProcessEntryPoint", backend .. "/backend/backend.js" + ) +end) \ No newline at end of file diff --git a/data/assets/util/webgui.asset b/data/assets/util/webgui.asset index d4d54451c5..b51a582660 100644 --- a/data/assets/util/webgui.asset +++ b/data/assets/util/webgui.asset @@ -1,18 +1,11 @@ +asset.require('./static_server') + local guiCustomization = asset.require('customization/gui') -- Select which commit hashes to use for the frontend and backend -local frontendHash = "c603ad07d1407a7d3def43f1e203244cee1c06f6" -local backendHash = "84737f9785f12efbb12d2de9d511154c6215fe9c" - +local frontendHash = "2d1bb8d8d8478b6ed025ccc6f1e0ceacf04b6114" local dataProvider = "data.openspaceproject.com/files/webgui" -local backend = asset.syncedResource({ - Identifier = "WebGuiBackend", - Name = "Web Gui Backend", - Type = "UrlSynchronization", - Url = dataProvider .. "/backend/" .. backendHash .. "/backend.zip" -}) - local frontend = asset.syncedResource({ Identifier = "WebGuiFrontend", Name = "Web Gui Frontend", @@ -27,22 +20,21 @@ asset.onInitialize(function () openspace.unzipFile(frontend .. "/frontend.zip", dest, true) end - -- Unzip the frontend bundle - dest = backend .. "/backend" - if not openspace.directoryExists(dest) then - openspace.unzipFile(backend .. "/backend.zip", dest, true) - end + -- Serve the production GUI: + local directories = openspace.getPropertyValue("Modules.WebGui.Directories") + directories[#directories + 1] = "frontend" + directories[#directories + 1] = frontend .. '/frontend' + openspace.setPropertyValueSingle("Modules.WebGui.Directories", directories) + openspace.setPropertyValueSingle("Modules.WebGui.DefaultEndpoint", "frontend") - -- Do not serve the files if we are in webgui development mode. - -- In that case, you have to serve the webgui manually, using `npm start`. - if not guiCustomization.webguiDevelopmentMode then + if guiCustomization.webguiDevelopmentMode then + -- Route CEF to the deveopment version of the GUI. + -- This must be manually served using `npm start` + -- in the OpenSpace-WebGuiFrontend repository. openspace.setPropertyValueSingle( - "Modules.WebGui.ServerProcessEntryPoint", backend .. "/backend/backend.js" + "Modules.CefWebGui.GuiUrl", + "http://127.0.0.1:4690/frontend/#/onscreen" ) - openspace.setPropertyValueSingle( - "Modules.WebGui.WebDirectory", frontend .. "/frontend" - ) - openspace.setPropertyValueSingle("Modules.WebGui.ServerProcessEnabled", true) end -- The GUI contains date and simulation increment, @@ -54,5 +46,17 @@ asset.onInitialize(function () end) asset.onDeinitialize(function () - openspace.setPropertyValueSingle("Modules.WebGui.ServerProcessEnabled", false) + -- Remove the frontend endpoint + local directories = openspace.getPropertyValue("Modules.WebGui.Directories") + local newDirectories; + + openspace.setPropertyValueSingle("Modules.WebGui.DefaultEndpoint", "") + + for i = 0, #directories, 2 do + if (string.find(directories[i], "frontend") == nil) then + newDirectories[#newDirectories + 1] = directories[i] + newDirectories[#newDirectories + 1] = directories[i + 1] + end + end + openspace.setPropertyValueSingle("Modules.WebGui.Directories", newDirectories) end) diff --git a/modules/cefwebgui/cefwebguimodule.cpp b/modules/cefwebgui/cefwebguimodule.cpp index ecdbfc46b1..4a69090acf 100644 --- a/modules/cefwebgui/cefwebguimodule.cpp +++ b/modules/cefwebgui/cefwebguimodule.cpp @@ -25,7 +25,6 @@ #include #include -#include #include #include #include @@ -166,7 +165,15 @@ void CefWebGuiModule::internalInitialize(const ghoul::Dictionary& configuration) } else { WebGuiModule* webGuiModule = global::moduleEngine.module(); _url = "http://127.0.0.1:" + - std::to_string(webGuiModule->port()) + "/#/onscreen"; + std::to_string(webGuiModule->port()) + "/frontend/#/onscreen"; + + _endpointCallback = webGuiModule->addEndpointChangeCallback( + [this](const std::string& endpoint, bool exists) { + if (exists && endpoint == "frontend" && _instance) { + _instance->reloadBrowser(); + } + } + ); } if (configuration.hasValue(GuiScaleInfo.identifier)) { @@ -204,6 +211,11 @@ void CefWebGuiModule::internalInitialize(const ghoul::Dictionary& configuration) }); global::callback::deinitializeGL.emplace_back([this]() { + if (_endpointCallback != -1) { + WebGuiModule* webGuiModule = global::moduleEngine.module(); + webGuiModule->removeEndpointChangeCallback(_endpointCallback); + _endpointCallback = -1; + } _enabled = false; startOrStopGui(); }); diff --git a/modules/cefwebgui/cefwebguimodule.h b/modules/cefwebgui/cefwebguimodule.h index 81bf8231f4..d822b838a5 100644 --- a/modules/cefwebgui/cefwebguimodule.h +++ b/modules/cefwebgui/cefwebguimodule.h @@ -27,6 +27,8 @@ #include +#include + #include #include #include @@ -53,6 +55,8 @@ private: properties::StringProperty _url; properties::FloatProperty _guiScale; std::unique_ptr _instance; + + WebGuiModule::CallbackHandle _endpointCallback = -1; }; } // namespace openspace diff --git a/modules/server/src/topics/setpropertytopic.cpp b/modules/server/src/topics/setpropertytopic.cpp index 584f8c2fe8..2e40e4d99d 100644 --- a/modules/server/src/topics/setpropertytopic.cpp +++ b/modules/server/src/topics/setpropertytopic.cpp @@ -41,15 +41,29 @@ namespace { std::string escapedLuaString(const std::string& str) { std::string luaString; + for (const char& c : str) { switch (c) { - case '\'': - luaString += "\'"; - break; - default: - luaString += c; + case '\t': + luaString += "\\t"; // Replace tab with \t. + break; + case '"': + luaString += "\\\""; // Replace " with \". + break; + case '\\': + luaString += "\\\\"; // Replace \ with \\. + break; + case '\n': + luaString += "\\\\n"; // Replace newline with \n. + break; + case '\r': + luaString += "\\r"; // Replace carriage return with \r. + break; + default: + luaString += c; } } + return luaString; } diff --git a/modules/webgui/webguimodule.cpp b/modules/webgui/webguimodule.cpp index 47f9ea7672..706c50a703 100644 --- a/modules/webgui/webguimodule.cpp +++ b/modules/webgui/webguimodule.cpp @@ -27,6 +27,7 @@ #include #include #include +#include #include #include #include @@ -67,11 +68,34 @@ namespace { }; constexpr openspace::properties::Property::PropertyInfo - WebDirectoryInfo = + DirectoriesInfo = { - "WebDirectory", - "Web Directory", - "Directory from which to to serve static content", + "Directories", + "Directories", + "Directories from which to to serve static content, as a string list " + "with entries expressed as pairs, where every odd is the endpoint name and every " + "even is the directory.", + }; + + constexpr openspace::properties::Property::PropertyInfo + DefaultEndpointInfo = + { + "DefaultEndpoint", + "Default Endpoint", + "The 'default' endpoint. " + "The server will redirect http requests from / to /", + }; + + + constexpr openspace::properties::Property::PropertyInfo + ServedDirectoriesInfo = + { + "ServedDirectories", + "ServedDirectories", + "Directories that are currently served. This value is set by the server process, " + "as a verification of the actually served directories. For example, an onChange " + "callback can be registered to this, to reload browsers when the server is ready." + "Manual changes to this property have no effect." }; constexpr const char* DefaultAddress = "localhost"; @@ -82,16 +106,20 @@ namespace openspace { WebGuiModule::WebGuiModule() : OpenSpaceModule(WebGuiModule::Name) - , _enabled(ServerProcessEnabledInfo, false) + , _enabled(ServerProcessEnabledInfo, true) , _entryPoint(ServerProcessEntryPointInfo) - , _webDirectory(WebDirectoryInfo) + , _directories(DirectoriesInfo) + , _servedDirectories(ServedDirectoriesInfo) + , _defaultEndpoint(DefaultEndpointInfo) , _port(PortInfo, DefaultPort) , _address(AddressInfo, DefaultAddress) , _webSocketInterface(WebSocketInterfaceInfo, "") { addProperty(_enabled); addProperty(_entryPoint); - addProperty(_webDirectory); + addProperty(_directories); + addProperty(_servedDirectories); + addProperty(_defaultEndpoint); addProperty(_address); addProperty(_port); } @@ -104,6 +132,30 @@ std::string WebGuiModule::address() const { return _address; } +WebGuiModule::CallbackHandle WebGuiModule::addEndpointChangeCallback(EndpointCallback cb) +{ + CallbackHandle handle = _nextCallbackHandle++; + _endpointChangeCallbacks.push_back({ handle, std::move(cb) }); + return handle; +} + +void WebGuiModule::removeEndpointChangeCallback(CallbackHandle handle) { + const auto it = std::find_if( + _endpointChangeCallbacks.begin(), + _endpointChangeCallbacks.end(), + [handle](const std::pair& cb) { + return cb.first == handle; + } + ); + + ghoul_assert( + it != _deltaTimeChangeCallbacks.end(), + "handle must be a valid callback handle" + ); + + _endpointChangeCallbacks.erase(it); +} + void WebGuiModule::internalInitialize(const ghoul::Dictionary& configuration) { if (configuration.hasValue(PortInfo.identifier)) { _port = configuration.value(PortInfo.identifier); @@ -119,7 +171,7 @@ void WebGuiModule::internalInitialize(const ghoul::Dictionary& configuration) { } auto startOrStop = [this]() { - if (_enabled) { + if (_enabled && !_entryPoint.value().empty()) { startProcess(); } else { stopProcess(); @@ -135,12 +187,46 @@ void WebGuiModule::internalInitialize(const ghoul::Dictionary& configuration) { _enabled.onChange(startOrStop); _entryPoint.onChange(restartIfEnabled); - _webDirectory.onChange(restartIfEnabled); + _directories.onChange(restartIfEnabled); + _defaultEndpoint.onChange(restartIfEnabled); + _servedDirectories.onChange([this]() { + std::unordered_map newEndpoints; + std::vector list = _servedDirectories.value(); + for (int i = 0; i < list.size() - 1; i += 2) { + newEndpoints[list[i]] = newEndpoints[list[i + 1]]; + } + for (const std::pair& e : _endpoints) { + if (newEndpoints.find(e.first) == newEndpoints.end()) { + // This endpoint existed before but does not exist anymore. + notifyEndpointListeners(e.first, false); + } + } + for (const std::pair& e : newEndpoints) { + if (_endpoints.find(e.first) == _endpoints.end() || + newEndpoints[e.first] != e.second) + { + // This endpoint exists now but did not exist before, + // or the directory has changed. + notifyEndpointListeners(e.first, true); + } + } + _endpoints = newEndpoints; + }); _port.onChange(restartIfEnabled); startOrStop(); } +void WebGuiModule::notifyEndpointListeners(const std::string& endpoint, bool exists) { + using K = CallbackHandle; + using V = EndpointCallback; + for (const std::pair& it : _endpointChangeCallbacks) { + it.second(endpoint, exists); + } +} + void WebGuiModule::startProcess() { + _endpoints.clear(); + ServerModule* serverModule = global::moduleEngine.module(); const ServerInterface* serverInterface = serverModule->serverInterfaceByIdentifier(_webSocketInterface); @@ -157,25 +243,55 @@ void WebGuiModule::startProcess() { const std::string nodePath = absPath("${MODULE_WEBGUI}/ext/nodejs/node"); #endif + std::string formattedDirectories = "["; + + std::vector directories = _directories.value(); + bool first = true; + + for (const std::string& str : directories) { + if (!first) { + formattedDirectories += ","; + } + first = false; + + // Escape twice: First json, and then bash (which needs same escape sequences) + formattedDirectories += "\\\"" + escapedJson(escapedJson(str)) + "\\\""; + } + formattedDirectories += "]"; + + const std::string defaultEndpoint = _defaultEndpoint.value().empty() ? + "" : + " --redirect \"" + _defaultEndpoint.value() + "\""; + const std::string command = "\"" + nodePath + "\" " + "\"" + absPath(_entryPoint.value()) + "\"" + - " --directory \"" + absPath(_webDirectory.value()) + "\"" + + " --directories \"" + formattedDirectories + "\"" + + defaultEndpoint + " --http-port \"" + std::to_string(_port.value()) + "\" " + " --ws-address \"" + _address.value() + "\"" + - " --ws-port \"" + std::to_string(webSocketPort) + "\"" + + " --ws-port " + std::to_string(webSocketPort) + " --auto-close --local"; _process = std::make_unique( command, - _webDirectory.value(), + absPath("${BIN}"), [](const char* data, size_t n) { const std::string str(data, n); LDEBUG(fmt::format("Web GUI server output: {}", str)); + }, + [](const char* data, size_t n) { + const std::string str(data, n); + LERROR(fmt::format("Web GUI server error: {}", str)); } ); } void WebGuiModule::stopProcess() { + for (const auto& e : _endpoints) { + notifyEndpointListeners(e.first, false); + } + _endpoints.clear(); + if (_process) { _process->kill(); } diff --git a/modules/webgui/webguimodule.h b/modules/webgui/webguimodule.h index 1f721b0193..7419d1b44e 100644 --- a/modules/webgui/webguimodule.h +++ b/modules/webgui/webguimodule.h @@ -28,19 +28,27 @@ #include #include +#include #include #include #include #include +#include +#include namespace openspace { class WebGuiModule : public OpenSpaceModule { public: + using CallbackHandle = int; + using EndpointCallback = std::function; + static constexpr const char* Name = "WebGui"; WebGuiModule(); int port() const; std::string address() const; + CallbackHandle addEndpointChangeCallback(EndpointCallback cb); + void removeEndpointChangeCallback(CallbackHandle); protected: void internalInitialize(const ghoul::Dictionary&) override; @@ -48,15 +56,23 @@ protected: private: void startProcess(); void stopProcess(); + void notifyEndpointListeners(const std::string& endpoint, bool exists); std::unique_ptr _process; properties::BoolProperty _enabled; properties::StringProperty _entryPoint; - properties::StringProperty _webDirectory; + properties::StringListProperty _directories; + properties::StringListProperty _servedDirectories; + properties::StringProperty _defaultEndpoint; + + std::unordered_map _endpoints; properties::IntProperty _port; properties::StringProperty _address; properties::StringProperty _webSocketInterface; + + std::vector> _endpointChangeCallbacks; + int _nextCallbackHandle = 0; }; } // namespace openspace