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
This commit is contained in:
Emil Axelsson
2019-07-19 16:49:54 +02:00
committed by GitHub
parent 134468b0d5
commit 5079f3fc60
7 changed files with 231 additions and 44 deletions

View File

@@ -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)

View File

@@ -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)

View File

@@ -25,7 +25,6 @@
#include <modules/cefwebgui/cefwebguimodule.h>
#include <modules/webbrowser/webbrowsermodule.h>
#include <modules/webgui/webguimodule.h>
#include <modules/cefwebgui/include/guirenderhandler.h>
#include <modules/cefwebgui/include/guikeyboardhandler.h>
#include <modules/webbrowser/include/browserinstance.h>
@@ -166,7 +165,15 @@ void CefWebGuiModule::internalInitialize(const ghoul::Dictionary& configuration)
} else {
WebGuiModule* webGuiModule = global::moduleEngine.module<WebGuiModule>();
_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<float>(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>();
webGuiModule->removeEndpointChangeCallback(_endpointCallback);
_endpointCallback = -1;
}
_enabled = false;
startOrStopGui();
});

View File

@@ -27,6 +27,8 @@
#include <openspace/util/openspacemodule.h>
#include <modules/webgui/webguimodule.h>
#include <openspace/properties/scalar/boolproperty.h>
#include <openspace/properties/scalar/floatproperty.h>
#include <openspace/properties/triggerproperty.h>
@@ -53,6 +55,8 @@ private:
properties::StringProperty _url;
properties::FloatProperty _guiScale;
std::unique_ptr<BrowserInstance> _instance;
WebGuiModule::CallbackHandle _endpointCallback = -1;
};
} // namespace openspace

View File

@@ -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;
}

View File

@@ -27,6 +27,7 @@
#include <modules/server/servermodule.h>
#include <openspace/engine/globals.h>
#include <openspace/engine/moduleengine.h>
#include <openspace/documentation/documentationgenerator.h>
#include <ghoul/fmt.h>
#include <ghoul/filesystem/filesystem.h>
#include <ghoul/logging/logmanager.h>
@@ -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 /<DefaultEndpoint>",
};
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<CallbackHandle, EndpointCallback>& 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<int>(PortInfo.identifier)) {
_port = configuration.value<int>(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<std::string, std::string> newEndpoints;
std::vector<std::string> list = _servedDirectories.value();
for (int i = 0; i < list.size() - 1; i += 2) {
newEndpoints[list[i]] = newEndpoints[list[i + 1]];
}
for (const std::pair<const std::string, std::string>& 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<const std::string, std::string>& 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<const K, V>& it : _endpointChangeCallbacks) {
it.second(endpoint, exists);
}
}
void WebGuiModule::startProcess() {
_endpoints.clear();
ServerModule* serverModule = global::moduleEngine.module<ServerModule>();
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<std::string> 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<ghoul::Process>(
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();
}

View File

@@ -28,19 +28,27 @@
#include <openspace/util/openspacemodule.h>
#include <openspace/properties/stringproperty.h>
#include <openspace/properties/stringlistproperty.h>
#include <openspace/properties/scalar/intproperty.h>
#include <openspace/properties/scalar/boolproperty.h>
#include <ghoul/misc/process.h>
#include <memory>
#include <vector>
#include <unordered_map>
namespace openspace {
class WebGuiModule : public OpenSpaceModule {
public:
using CallbackHandle = int;
using EndpointCallback = std::function<void(const std::string&, bool)>;
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<ghoul::Process> _process;
properties::BoolProperty _enabled;
properties::StringProperty _entryPoint;
properties::StringProperty _webDirectory;
properties::StringListProperty _directories;
properties::StringListProperty _servedDirectories;
properties::StringProperty _defaultEndpoint;
std::unordered_map<std::string, std::string> _endpoints;
properties::IntProperty _port;
properties::StringProperty _address;
properties::StringProperty _webSocketInterface;
std::vector<std::pair<CallbackHandle, EndpointCallback>> _endpointChangeCallbacks;
int _nextCallbackHandle = 0;
};
} // namespace openspace