mirror of
https://github.com/OpenSpace/OpenSpace.git
synced 2026-01-09 05:00:42 -06:00
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:
21
data/assets/util/static_server.asset
Normal file
21
data/assets/util/static_server.asset
Normal 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)
|
||||
@@ -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)
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user