/***************************************************************************************** * * * OpenSpace * * * * Copyright (c) 2014-2021 * * * * Permission is hereby granted, free of charge, to any person obtaining a copy of this * * software and associated documentation files (the "Software"), to deal in the Software * * without restriction, including without limitation the rights to use, copy, modify, * * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to * * permit persons to whom the Software is furnished to do so, subject to the following * * conditions: * * * * The above copyright notice and this permission notice shall be included in all copies * * or substantial portions of the Software. * * * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, * * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A * * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF * * CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE * * OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * ****************************************************************************************/ #include #include #include #include #include #include #include #include #include #include namespace { constexpr const char* _loggerCat = "WebGuiModule"; constexpr openspace::properties::Property::PropertyInfo ServerProcessEnabledInfo = { "ServerProcessEnabled", "Enable Server Process", "Enable the node js based process used to serve the Web GUI." }; constexpr openspace::properties::Property::PropertyInfo AddressInfo = { "Address", "Address", "The network address to use when connecting to OpenSpace from the Web GUI." }; constexpr openspace::properties::Property::PropertyInfo PortInfo = { "Port", "Port", "The network port to use when serving the Web GUI over HTTP." }; constexpr openspace::properties::Property::PropertyInfo WebSocketInterfaceInfo = { "WebSocketInterface", "WebSocket Interface", "The identifier of the websocket interface to use when communicating." }; constexpr openspace::properties::Property::PropertyInfo ServerProcessEntryPointInfo = { "ServerProcessEntryPoint", "Server Process Entry Point", "The node js command to invoke." }; constexpr openspace::properties::Property::PropertyInfo DirectoriesInfo = { "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"; constexpr const int DefaultPort = 4680; } namespace openspace { WebGuiModule::WebGuiModule() : OpenSpaceModule(WebGuiModule::Name) , _enabled(ServerProcessEnabledInfo, true) , _entryPoint(ServerProcessEntryPointInfo) , _directories(DirectoriesInfo) , _servedDirectories(ServedDirectoriesInfo) , _defaultEndpoint(DefaultEndpointInfo) , _port(PortInfo, DefaultPort) , _address(AddressInfo, DefaultAddress) , _webSocketInterface(WebSocketInterfaceInfo, "") { addProperty(_enabled); addProperty(_entryPoint); addProperty(_directories); addProperty(_servedDirectories); addProperty(_defaultEndpoint); addProperty(_address); addProperty(_port); } int WebGuiModule::port() const { return _port; } 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 != _endpointChangeCallbacks.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); } if (configuration.hasValue(AddressInfo.identifier)) { _address = configuration.value(AddressInfo.identifier); } if (configuration.hasValue(WebSocketInterfaceInfo.identifier)) { _webSocketInterface = configuration.value(WebSocketInterfaceInfo.identifier); } auto startOrStop = [this]() { if (_enabled && !_entryPoint.value().empty()) { startProcess(); } else { stopProcess(); } }; auto restartIfEnabled = [this]() { stopProcess(); if (_enabled) { startProcess(); } }; _enabled.onChange(startOrStop); _entryPoint.onChange(restartIfEnabled); _directories.onChange(restartIfEnabled); _defaultEndpoint.onChange(restartIfEnabled); _servedDirectories.onChange([this]() { std::unordered_map newEndpoints; std::vector list = _servedDirectories.value(); if (!list.empty()) { for (size_t 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() { ZoneScoped _endpoints.clear(); ServerModule* serverModule = global::moduleEngine->module(); const ServerInterface* serverInterface = serverModule->serverInterfaceByIdentifier(_webSocketInterface); if (!serverInterface) { LERROR("Missing server interface. Server process could not start."); return; } const int webSocketPort = serverInterface->port(); #ifdef _MSC_VER const std::string nodePath = absPath("${MODULE_WEBGUI}/ext/nodejs/node.exe"); #else const std::string nodePath = absPath("${MODULE_WEBGUI}/ext/nodejs/node"); #endif std::string formattedDirectories = "["; std::vector directories = _directories.value(); for (size_t i = 0; i < directories.size(); ++i) { std::string arg = directories[i]; if (i & 1) { arg = absPath(arg); } formattedDirectories += "\\\"" + escapedJson(escapedJson(arg)) + "\\\""; if (i != directories.size() - 1) { formattedDirectories += ","; } } formattedDirectories += "]"; const std::string defaultEndpoint = _defaultEndpoint.value().empty() ? "" : " --redirect \"" + _defaultEndpoint.value() + "\""; const std::string command = "\"" + nodePath + "\" " + "\"" + absPath(_entryPoint.value()) + "\"" + " --directories \"" + formattedDirectories + "\"" + defaultEndpoint + " --http-port \"" + std::to_string(_port.value()) + "\" " + " --ws-address \"" + _address.value() + "\"" + " --ws-port " + std::to_string(webSocketPort) + " --auto-close --local"; _process = std::make_unique( command, 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(); } _process = nullptr; } } // namespace openspace