diff --git a/CMakeLists.txt b/CMakeLists.txt index 83a4738c2..bbdee8503 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -46,6 +46,7 @@ option(PLUGIN_POWER "Include Power plugin" OFF) option(PLUGIN_REMOTECONTROL "Include RemoteControl plugin" OFF) option(PLUGIN_RESOURCEMONITOR "Include ResourceMonitor plugin" OFF) option(PLUGIN_RUSTBRIDGE "Include RustBridge plugin" OFF) +option(PLUGIN_SCRIPTENGINE "Include ScriptEngine plugin" OFF) option(PLUGIN_SECURESHELLSERVER "Include SecureShellServer plugin" OFF) option(PLUGIN_STREAMER "Include Streamer plugin" OFF) option(PLUGIN_SNAPSHOT "Include Snapshot plugin" OFF) @@ -191,6 +192,10 @@ if(PLUGIN_RUSTBRIDGE) add_subdirectory(RustBridge) endif() +if(PLUGIN_SCRIPTENGINE) + add_subdirectory(ScriptEngine) +endif() + if(PLUGIN_SECURESHELLSERVER) add_subdirectory(SecureShellServer) endif() diff --git a/ScriptEngine/CMakeLists.txt b/ScriptEngine/CMakeLists.txt new file mode 100644 index 000000000..f1e92e4f8 --- /dev/null +++ b/ScriptEngine/CMakeLists.txt @@ -0,0 +1,68 @@ +# If not stated otherwise in this file or this component's LICENSE file the +# following copyright and licenses apply: +# +# Copyright 2020 Metrological +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +project(ScriptEngine) + +cmake_minimum_required(VERSION 3.3) + +find_package(bridge) + +project_version(1.0.0) + +set(MODULE_NAME ${NAMESPACE}${PROJECT_NAME}) +set(PLUGIN_IMPLEMENTATION "${MODULE_NAME}Impl" CACHE STRING "library with NodeJS implementation." ) + +message("Setup ${MODULE_NAME} v${PROJECT_VERSION}") + +find_package(${NAMESPACE}Plugins REQUIRED) +find_package(${NAMESPACE}Definitions REQUIRED) +find_package(CompileSettingsDebug CONFIG REQUIRED) + +add_library(${MODULE_NAME} SHARED + ScriptEngine.cpp + Module.cpp) + +target_link_libraries(${MODULE_NAME} + PRIVATE + CompileSettingsDebug::CompileSettingsDebug + ${NAMESPACE}Plugins::${NAMESPACE}Plugins + ${NAMESPACE}Definitions::${NAMESPACE}Definitions + ) + +# Select the proper script engine implementation.. +add_subdirectory(Implementation/NodeJS) + +add_library(${PLUGIN_IMPLEMENTATION} SHARED + Module.cpp + ScriptEngineImplementation.cpp) + +target_link_libraries(${PLUGIN_IMPLEMENTATION} + PRIVATE + CompileSettingsDebug::CompileSettingsDebug + ${NAMESPACE}Plugins::${NAMESPACE}Plugins + common::implementation) + +# Library installation section +string(TOLOWER ${NAMESPACE} STORAGENAME) +install(TARGETS ${MODULE_NAME} DESTINATION ${CMAKE_INSTALL_LIBDIR}/${STORAGENAME}/plugins) +install(TARGETS ${PLUGIN_IMPLEMENTATION} DESTINATION ${CMAKE_INSTALL_LIBDIR}/${STORAGENAME}/plugins) + +set(PLUGIN_NODEJS_AUTOSTART "false" CACHE STRING "Automatically start Cobalt plugin") +set(PLUGIN_NODEJS_MODE "Local" CACHE STRING "Controls if the plugin should run in its own process, in process or remote.") +set(PLUGIN_NODEJS_RESUMED "true" CACHE STRING "Set Cobalt plugin resume state") + +write_config(PLUGINS ${PROJECT_NAME}) diff --git a/ScriptEngine/Implementation.h b/ScriptEngine/Implementation.h new file mode 100644 index 000000000..384207537 --- /dev/null +++ b/ScriptEngine/Implementation.h @@ -0,0 +1,16 @@ +#pragma once + +#include "Module.h" + +extern "C" { + +struct platform; + +extern platform* script_create_platform(const char configuration[]); +extern void script_destroy_platform(platform*); + +extern uint32_t script_prepare(platform*, const uint32_t length, const char script[]); +extern uint32_t script_execute(platform*); +extern uint32_t script_abort(platform*); + +} diff --git a/ScriptEngine/Implementation/NodeJS/CMakeLists.txt b/ScriptEngine/Implementation/NodeJS/CMakeLists.txt new file mode 100644 index 000000000..ff5cb18cf --- /dev/null +++ b/ScriptEngine/Implementation/NodeJS/CMakeLists.txt @@ -0,0 +1,28 @@ +set(IMPLEMENTATION NodeJS) + +list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_LIST_DIR}/cmake") + +message(STATUS "Setting up ${IMPLEMENTATION} as implementation for ${MODULE_NAME}") + +find_package(${NAMESPACE}Core REQUIRED) +find_package(${NAMESPACE}Messaging REQUIRED) +find_package(${NAMESPACE}Definitions REQUIRED) +find_package(CompileSettingsDebug CONFIG REQUIRED) +find_package(NodeJS REQUIRED) + +add_library(${IMPLEMENTATION} STATIC Implementation.cpp) + +target_include_directories(${IMPLEMENTATION} PRIVATE ${NODEJS_INCLUDE_DIRS}) + +target_link_libraries(${IMPLEMENTATION} + PRIVATE + CompileSettingsDebug::CompileSettingsDebug + ${NAMESPACE}Core::${NAMESPACE}Core + ${NAMESPACE}Messaging::${NAMESPACE}Messaging) + +set_target_properties(${IMPLEMENTATION} PROPERTIES + CXX_STANDARD 17 + CXX_STANDARD_REQUIRED YES + FRAMEWORK FALSE) + +add_library(common::implementation ALIAS ${IMPLEMENTATION}) diff --git a/ScriptEngine/Implementation/NodeJS/Implementation.cpp b/ScriptEngine/Implementation/NodeJS/Implementation.cpp new file mode 100644 index 000000000..5eb7b932e --- /dev/null +++ b/ScriptEngine/Implementation/NodeJS/Implementation.cpp @@ -0,0 +1,300 @@ +#include "../../Module.h" +#include "../../Implementation.h" + +#include + +// Node uses the EXTERNAL as an enum value undef it before it kill everything +#undef EXTERNAL + +#include + +#ifdef __WINDOWS__ +#pragma comment(lib, "libnode.lib") +#else + +using namespace Thunder; + +class FileDescriptorRedirect : public Core::IResource { +public: + FileDescriptorRedirect() = delete; + FileDescriptorRedirect(FileDescriptorRedirect&&) = delete; + FileDescriptorRedirect(const FileDescriptorRedirect&) = delete; + FileDescriptorRedirect& operator= (const FileDescriptorRedirect&) = delete; + + FileDescriptorRedirect(int fileDescriptor) + : _locator(fileDescriptor) + , _original(-1) + , _offset(0) { + + if (::pipe(_pipe) == 0) { /* make a pipe */ + _original = ::dup(_locator); /* save stdout for display later */ + ::dup2(_pipe[1], _locator); /* redirect stdout to the pipe */ + ::close(_pipe[1]); + ::fcntl(_pipe[0], F_SETFL, ::fcntl(_pipe[0], F_GETFL) | O_NONBLOCK); + } + } + ~FileDescriptorRedirect() { + if (_original != -1) { + dup2(_original, _locator); /* reconnect stdout for testing */ + } + } + +public: + handle Descriptor() const override { + return (static_cast(_pipe[0])); + } + uint16_t Events() override { + return (POLLHUP | POLLRDHUP | POLLIN); + } + void Handle(const uint16_t events) override { + if (events & POLLIN) { + ssize_t loaded = ::read(_pipe[0], &(_buffer[_offset]), sizeof(_buffer) - _offset); /* read from pipe into buffer */ + + if ( (loaded != -1) && (loaded > 0) ) { + _offset += loaded; + + } + } + } + +private: + int _locator; + int _original; + uint32_t _offset; + int _pipe[2]; + char _buffer[1024]; +}; + +#endif + +namespace Thunder { + + namespace Implementation { + + class Config : public Core::JSON::Container { + public: + Config(Config&&) = delete; + Config(const Config&) = delete; + Config& operator=(const Config&) = delete; + + Config() + : Core::JSON::Container() + , Workers(4) { + Add(_T("workers"), &Workers); + } + ~Config() override = default; + + public: + Core::JSON::DecUInt8 Workers; + + }; + } +} + +using namespace Thunder; + +struct platform { +public: + platform() = delete; + platform(platform&&) = delete; + platform(const platform&) = delete; + platform& operator= (const platform&) = delete; + + platform(const char* configuration) + : _platform() + , _environment() { + + Implementation::Config config; config.FromString(string(configuration)); + + std::vector args = { "embeddednode" }; + + // Parse Node.js CLI options, and print any errors that have occurred while + // trying to parse them. + std::unique_ptr result = + node::InitializeOncePerProcess( + args, + { + node::ProcessInitializationFlags::kNoInitializeV8, + node::ProcessInitializationFlags::kNoInitializeNodeV8Platform + + } + ); + + if (result->exit_code() != 0) { + for (const std::string& err : result->errors()) + TRACE(Trace::Error, (_T("NodeJS platform Initialization: %s"), err.c_str())); + } + else { + if (config.Workers.Value() > 0) { + // Create a v8::Platform instance. `MultiIsolatePlatform::Create()` is a way + // to create a v8::Platform instance that Node.js can use when creating + // Worker threads. When no `MultiIsolatePlatform` instance is present, + // Worker threads are disabled. + _platform = node::MultiIsolatePlatform::Create(config.Workers.Value()); + v8::V8::InitializePlatform(_platform.get()); + } + + v8::V8::Initialize(); + + // Setup up a libuv event loop, v8::Isolate, and Node.js Environment. + std::vector exec_args; + std::vector errors; + + _environment = + node::CommonEnvironmentSetup::Create(_platform.get(), &errors, args, exec_args); + + for (const std::string& err : result->errors()) + TRACE(Trace::Error, (_T("NodeJS environment Initialization: %s"), err.c_str())); + + if (_environment != nullptr) { + { + // obtain + lock iolsate + v8::Isolate* isolate = _environment->isolate(); + node::Environment* node_env = _environment->env(); + + v8::Locker locker(isolate); + v8::Isolate::Scope isolateScope(isolate); + v8::HandleScope handle_scope(isolate); + v8::Context::Scope context_scope(_environment->context()); + + v8::MaybeLocal loadenv_ret = node::LoadEnvironment( + node_env, + [&](const node::StartExecutionCallbackInfo& info) -> v8::MaybeLocal { + _require.Reset(isolate, info.native_require); + _process.Reset(isolate, info.process_object); + return(v8::Null(isolate)); + } + ); + } + } + } + } + ~platform() { + node::Stop(_environment->env()); + + _require.Reset(); + _process.Reset(); + _environment.reset(); + _platform.reset(); + + v8::V8::Dispose(); + v8::V8::DisposePlatform(); + } + +public: + bool IsValid() const { + return (_environment != nullptr); + } + uint32_t Prepare(const uint32_t length, const char script[]) { + _script = string(script, length); + return (Core::ERROR_NONE); + } + uint32_t Execute() { + uint32_t result = Core::ERROR_UNAVAILABLE; + + // obtain + lock iolsate + v8::Isolate* isolate = _environment->isolate(); + + v8::Locker locker(isolate); + v8::Isolate::Scope isolateScope(isolate); + v8::HandleScope handle_scope(isolate); + v8::Context::Scope context_scope(_environment->context()); + + v8::Local vm = LoadRequire(isolate, _T("vm")); + v8::Local object = Execute(isolate, vm, _T("runInThisContext"), { _script }); + + return (result); + } + uint32_t Abort() { + if (_environment != nullptr) { + node::Stop(_environment->env()); + } + _script.clear(); + return (Core::ERROR_NONE); + } + +private: + inline v8::Local Execute(v8::Isolate * isolate, v8::Local& classObject, const string & method, const std::vector& parameters) { + int index = 0; + v8::Local methodName = v8::String::NewFromUtf8(isolate, method.c_str(), v8::NewStringType::kNormal, static_cast(method.length())).ToLocalChecked(); + v8::Local methodFunction = classObject->Get(isolate->GetCurrentContext(), methodName).ToLocalChecked().As(); + v8::Local* function_args = static_cast< v8::Local* >(::alloca(sizeof(v8::Local) * parameters.size())); + + for (const string& entry : parameters) { + function_args[index] = v8::String::NewFromUtf8(isolate, entry.c_str(), v8::NewStringType::kNormal, static_cast(entry.length())).ToLocalChecked(); + index++; + } + + return (methodFunction->Call( + isolate->GetCurrentContext(), + v8::Null(isolate), + 1, + function_args).ToLocalChecked()); + } + v8::Local LoadRequire(v8::Isolate* isolate, const string& require) { + v8::Local vm_string = v8::String::NewFromUtf8(isolate, require.c_str(), v8::NewStringType::kNormal, static_cast(require.length())).ToLocalChecked(); + v8::Local function_args[1]; + function_args[0] = vm_string; + + v8::Local vm = _require.Get(isolate)->Call( + isolate->GetCurrentContext(), + v8::Null(isolate), + 1, + function_args).ToLocalChecked(); + + return (vm.As()); + } +private: + std::unique_ptr _platform; + std::unique_ptr _environment; + v8::Global _require; + v8::Global _process; + string _script; +}; + +platform* script_create_platform(const char configuration[]) { + platform* instance = new platform(configuration); + + if ((instance != nullptr) && (instance->IsValid() == false)) { + delete instance; + instance = nullptr; + } + + return (instance); +} + +void script_destroy_platform(platform* instance) { + if (instance != nullptr) { + delete instance; + } +} + +uint32_t script_prepare(platform* instance, const uint32_t length, const char script[]) { + uint32_t result = Core::ERROR_BAD_REQUEST; + + if (instance != nullptr) { + result = instance->Prepare(length, script); + } + + return (result); +} + +uint32_t script_execute(platform* instance) { + uint32_t result = Core::ERROR_BAD_REQUEST; + + if (instance != nullptr) { + result = instance->Execute(); + } + + return (result); +} + +uint32_t script_abort(platform* instance) { + uint32_t result = Core::ERROR_BAD_REQUEST; + + if (instance != nullptr) { + result = instance->Abort(); + } + return (result); +} + diff --git a/ScriptEngine/Implementation/NodeJS/Info.txt b/ScriptEngine/Implementation/NodeJS/Info.txt new file mode 100644 index 000000000..3c89b3270 --- /dev/null +++ b/ScriptEngine/Implementation/NodeJS/Info.txt @@ -0,0 +1,60 @@ +// Set up the Node.js instance for execution, and run code inside of it. +// There is also a variant that takes a callback and provides it with +// the `require` and `process` objects, so that it can manually compile +// and run scripts as needed. +// The `require` function inside this script does *not* access the file +// system, and can only load built-in Node.js modules. +// `module.createRequire()` is being used to create one that is able to +// load files from the disk, and uses the standard CommonJS file loader +// instead of the internal-only `require` function. +//v8::MaybeLocal loadenv_ret = node::LoadEnvironment( +// env, +// "const publicRequire =" +// " require('node:module').createRequire(process.cwd() + '/');" +// "globalThis.require = publicRequire;" +// "require('node:vm').runInThisContext(process.argv[1]);"); +//v8::MaybeLocal loadenv_ret = node::LoadEnvironment( +// _environment->env(), +// "console.log(\"Poep Chinees\");"); + +//v8::Local result = _script->Run(context).ToLocalChecked(); +//v8::String::Utf8Value utf8(isolate, result); +//std::cout << "result: " << *utf8 << std::endl; + +//if (loadenv_ret.IsEmpty() == true) { +// result = Core::ERROR_NONE; +//} +//else { // There has been a JS exception. +// int exit_code = node::SpinEventLoop(_environment->env()).FromMaybe(1); + +// // node::Stop() can be used to explicitly stop the event loop and keep +// // further JavaScript from running. It can be called from any thread, +// // and will act like worker.terminate() if called from another thread. +// node::Stop(_environment->env()); + +// result = (exit_code == 0 ? Core::ERROR_NONE : Core::ERROR_ASYNC_FAILED); +//} + + +// obtain + lock isolate +// v8::Isolate* isolate = _environment->isolate(); +// v8::Locker locker(isolate); +// v8::Isolate::Scope isolateScope(isolate); +// v8::HandleScope handle_scope(isolate); +// v8::Context::Scope context_scope(_environment->context()); + +// v8::Local context = isolate->GetCurrentContext(); +// v8::Local global_object = context->Global(); + +// global_object->Set(context, v8::String::NewFromUtf8(isolate, "require").ToLocalChecked(), _require.Get(isolate)).ToChecked(); +// global_object->Set(context, v8::String::NewFromUtf8(isolate, "process").ToLocalChecked(), _process.Get(isolate)).ToChecked(); + +// v8::Local source = +// v8::String::NewFromUtf8(isolate, script, v8::NewStringType::kNormal, length).ToLocalChecked(); +// _script = +// v8::Script::Compile(context, source).ToLocalChecked(); + +// v8::Local outcome = _script->Run(isolate->GetCurrentContext()).ToLocalChecked(); +// v8::String::Utf8Value utf8(isolate, outcome); +// std::cout << "result: " << *utf8 << std::endl; +// return v8::Null(isolate); \ No newline at end of file diff --git a/ScriptEngine/Implementation/NodeJS/cmake/FindNodeJS.cmake b/ScriptEngine/Implementation/NodeJS/cmake/FindNodeJS.cmake new file mode 100644 index 000000000..d5fa11bd1 --- /dev/null +++ b/ScriptEngine/Implementation/NodeJS/cmake/FindNodeJS.cmake @@ -0,0 +1,7 @@ +find_path (NODEJS_INCLUDE_DIRS NAME node.h PATHS "usr/include/" PATH_SUFFIXES "node") +find_library(NODEJS_LIBRARIES NAME libnode.so.115 PATH_SUFFIXES lib) + +include(FindPackageHandleStandardArgs) +find_package_handle_standard_args(NodeJS DEFAULT_MSG NODEJS_INCLUDE_DIRS NODEJS_LIBRARIES) +mark_as_advanced(NODEJS_INCLUDE_DIRS NODEJS_LIBRARIES) + diff --git a/ScriptEngine/Module.cpp b/ScriptEngine/Module.cpp new file mode 100644 index 000000000..bbf44ccdf --- /dev/null +++ b/ScriptEngine/Module.cpp @@ -0,0 +1,22 @@ +/* + * If not stated otherwise in this file or this component's LICENSE file the + * following copyright and licenses apply: + * + * Copyright 2020 Metrological + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "Module.h" + +MODULE_NAME_DECLARATION(BUILD_REFERENCE) diff --git a/ScriptEngine/Module.h b/ScriptEngine/Module.h new file mode 100644 index 000000000..b5bce0e2c --- /dev/null +++ b/ScriptEngine/Module.h @@ -0,0 +1,32 @@ +/* + * If not stated otherwise in this file or this component's LICENSE file the + * following copyright and licenses apply: + * + * Copyright 2020 Metrological + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#ifndef MODULE_NAME +#define MODULE_NAME Plugin_ScriptEngine +#endif + +#include +#include +#include +#include + +#undef EXTERNAL +#define EXTERNAL diff --git a/ScriptEngine/ScriptEngine.conf.in b/ScriptEngine/ScriptEngine.conf.in new file mode 100644 index 000000000..0d3487662 --- /dev/null +++ b/ScriptEngine/ScriptEngine.conf.in @@ -0,0 +1 @@ +startmode = "Activated" diff --git a/ScriptEngine/ScriptEngine.cpp b/ScriptEngine/ScriptEngine.cpp new file mode 100644 index 000000000..0ed883928 --- /dev/null +++ b/ScriptEngine/ScriptEngine.cpp @@ -0,0 +1,209 @@ +/* + * If not stated otherwise in this file or this component's LICENSE file the + * following copyright and licenses apply: + * + * Copyright 2020 Metrological + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "ScriptEngine.h" +#include +#include + +namespace Thunder { +namespace ScriptEngine { + +class MemoryObserverImpl: public Exchange::IMemory { +public: + MemoryObserverImpl(); + MemoryObserverImpl(MemoryObserverImpl&&); + MemoryObserverImpl(const MemoryObserverImpl&); + MemoryObserverImpl& operator=(const MemoryObserverImpl&); + + MemoryObserverImpl(const RPC::IRemoteConnection* connection) : + _main(connection == nullptr ? Core::ProcessInfo().Id() : connection->RemoteId()) { + } + ~MemoryObserverImpl() override = default; + +public: + uint64_t Resident() const override { + return _main.Resident(); + } + uint64_t Allocated() const override { + return _main.Allocated(); + } + uint64_t Shared() const override { + return _main.Shared(); + } + uint8_t Processes() const override { + return (IsOperational() ? 1 : 0); + } + bool IsOperational() const override { + return _main.IsActive(); + } + + BEGIN_INTERFACE_MAP(MemoryObserverImpl) + INTERFACE_ENTRY (Exchange::IMemory) + END_INTERFACE_MAP + +private: + Core::ProcessInfo _main; +}; + +Exchange::IMemory* MemoryObserver(const RPC::IRemoteConnection* connection) +{ + ASSERT(connection != nullptr); + Exchange::IMemory* result = Core::ServiceType::Create(connection); + return (result); +} + +} + +namespace Plugin { + + namespace { + + static Metadata metadata( + // Version + 1, 0, 0, + // Preconditions + {}, + // Terminations + {}, + // Controls + {} + ); + } + + + +static Core::ProxyPoolType _textBodies(2); +static Core::ProxyPoolType> jsonBodyDataFactory(2); + +const string ScriptEngine::Initialize(PluginHost::IShell *service) +{ + Config config; + string message; + + ASSERT(service != nullptr); + ASSERT(_service == nullptr); + ASSERT(_connectionId == 0); + ASSERT(_scriptEngine == nullptr); + ASSERT(_memory == nullptr); + + config.FromString(service->ConfigLine()); + + _service = service; + _service->AddRef(); + + // Register the Connection::Notification stuff. The Remote process might die + // before we get a + // change to "register" the sink for these events !!! So do it ahead of + // instantiation. + _service->Register(&_notification); + + Plugin::Config::RootConfig rootConfig(service); + const uint32_t permission = (Core::File::USER_READ | Core::File::USER_WRITE | + Core::File::GROUP_READ | Core::File::GROUP_WRITE); + + if (_service->EnablePersistentStorage(permission, rootConfig.User.Value(), rootConfig.Group.Value()) != Core::ERROR_NONE) { + message = _T("Could not setup persistent path: ") + service->PersistentPath(); + } + else { + _scriptEngine = _service->Root (_connectionId, 2000, _T("ScriptEngineImplementation")); + + if (_scriptEngine != nullptr) { + _scriptEngine->Register(&_notification); + Exchange::IConfiguration* configuration(_scriptEngine->QueryInterface()); + + if (configuration == nullptr) { + message = _T("ScriptEngine IConfiguration could not be Obtained."); + } else { + configuration->Configure(_service); + configuration->Release(); + Exchange::JScriptEngine::Register(*this, _scriptEngine); + + RPC::IRemoteConnection* remoteConnection = _service->RemoteConnection(_connectionId); + if (remoteConnection != nullptr) { + _memory = Thunder::ScriptEngine::MemoryObserver(remoteConnection); + ASSERT(_memory != nullptr); + remoteConnection->Release(); + } + } + } + else { + message = _T("ScriptEngine could not be instantiated."); + } + } + + return message; +} + +void ScriptEngine::Deinitialize(PluginHost::IShell *service VARIABLE_IS_NOT_USED) +{ + if (_service != nullptr) { + ASSERT(_service == service); + _service->Unregister(&_notification); + + if (_scriptEngine != nullptr) { + Exchange::JScriptEngine::Unregister(*this); + _scriptEngine->Unregister(&_notification); + + if (_memory != nullptr) { + _memory->Release(); + _memory = nullptr; + } + + RPC::IRemoteConnection* connection(_service->RemoteConnection(_connectionId)); + VARIABLE_IS_NOT_USED uint32_t result = _scriptEngine->Release(); + _scriptEngine = nullptr; + ASSERT(result == Core::ERROR_DESTRUCTION_SUCCEEDED); + + // The connection can disappear in the meantime... + if (connection != nullptr) { + // But if it did not dissapear in the meantime, forcefully terminate it. Shoot to kill :-) + connection->Terminate(); + connection->Release(); + } + } + _service->Release(); + _service = nullptr; + _connectionId = 0; + } +} + +string ScriptEngine::Information() const +{ + // No additional info to report. + return (string()); +} + +void ScriptEngine::URLChanged(const string &URL) +{ + Exchange::JScriptEngine::Event::URLChanged(*this, URL); +} + +void ScriptEngine::Deactivated(RPC::IRemoteConnection *connection) +{ + if (connection->Id() == _connectionId) { + + ASSERT(_service != nullptr); + Core::IWorkerPool::Instance().Submit( + PluginHost::IShell::Job::Create(_service, + PluginHost::IShell::DEACTIVATED, + PluginHost::IShell::FAILURE)); + } +} +} +} // namespace diff --git a/ScriptEngine/ScriptEngine.h b/ScriptEngine/ScriptEngine.h new file mode 100644 index 000000000..efc6fa7c9 --- /dev/null +++ b/ScriptEngine/ScriptEngine.h @@ -0,0 +1,135 @@ +/* + * If not stated otherwise in this file or this component's LICENSE file the + * following copyright and licenses apply: + * + * Copyright 2020 Metrological + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include "Module.h" +#include +#include + +namespace Thunder { +namespace Plugin { + +class ScriptEngine: public PluginHost::IPlugin, public PluginHost::JSONRPC { +private: + class Notification + : public RPC::IRemoteConnection::INotification + , public Exchange::IScriptEngine::INotification { + public: + Notification() = delete; + Notification(Notification&&) = delete; + Notification(const Notification&) = delete; + Notification& operator=(const Notification&) = delete; + + public: + explicit Notification(ScriptEngine& parent) + : _parent(parent) { + } + ~Notification() override = default; + + public: + BEGIN_INTERFACE_MAP (Notification) + INTERFACE_ENTRY (Exchange::IScriptEngine::INotification) + INTERFACE_ENTRY (RPC::IRemoteConnection::INotification) + END_INTERFACE_MAP + + private: + // + // Exchange::IScriptEngine::INotification + // ------------------------------------------------------------------ + void URLChanged(const string& URL) override { + _parent.URLChanged(URL); + } + + // + // RPC::IRemoteConnection::INotification + // ------------------------------------------------------------------ + void Activated(RPC::IRemoteConnection*) override { + } + void Deactivated(RPC::IRemoteConnection *connection) override { + _parent.Deactivated(connection); + } + void Terminated(RPC::IRemoteConnection*) override { + } + + private: + ScriptEngine& _parent; + }; + +public: + class Data: public Core::JSON::Container { + public: + Data(Data&&) = delete; + Data(const Data&) = delete; + Data& operator=(const Data&) = delete; + + Data() + : Core::JSON::Container() + , URL() { + Add(_T("url"), &URL); + } + ~Data() override = default; + + public: + Core::JSON::String URL; + }; + +public: + ScriptEngine(ScriptEngine&&); + ScriptEngine(const ScriptEngine&); + ScriptEngine& operator=(const ScriptEngine&); + ScriptEngine() + : _skipURL(0) + , _connectionId(0) + , _scriptEngine(nullptr) + , _memory(nullptr) + , _service(nullptr) + , _notification(*this) { + } + ~ScriptEngine() override = default; + +public: + BEGIN_INTERFACE_MAP (ScriptEngine) + INTERFACE_ENTRY (PluginHost::IPlugin) + INTERFACE_ENTRY (PluginHost::IDispatcher) + INTERFACE_AGGREGATE(Exchange::IScriptEngine, _scriptEngine) + INTERFACE_AGGREGATE(Exchange::IMemory, _memory) + END_INTERFACE_MAP + +public: + // IPlugin methods + // ------------------------------------------------------------------------------------------------------- + const string Initialize(PluginHost::IShell* service) override; + void Deinitialize(PluginHost::IShell *service) override; + string Information() const override; + +private: + void Deactivated(RPC::IRemoteConnection *connection); + void URLChanged(const string &URL); + +private: + uint8_t _skipURL; + uint32_t _connectionId; + Exchange::IScriptEngine* _scriptEngine; + Exchange::IMemory* _memory; + PluginHost::IShell* _service; + Core::SinkType _notification; +}; +} +} // namespace diff --git a/ScriptEngine/ScriptEngine.vcxproj b/ScriptEngine/ScriptEngine.vcxproj new file mode 100644 index 000000000..3d45edbb1 --- /dev/null +++ b/ScriptEngine/ScriptEngine.vcxproj @@ -0,0 +1,200 @@ + + + + + Debug + Win32 + + + Release + Win32 + + + Debug + x64 + + + Release + x64 + + + + 15.0 + {6D5E7937-796C-9CE8-9CFD-4F362D56895F} + Win32Proj + ScriptEngine + 10.0 + + + + DynamicLibrary + true + v143 + MultiByte + + + DynamicLibrary + false + v143 + true + MultiByte + + + DynamicLibrary + true + v143 + MultiByte + + + DynamicLibrary + false + v143 + true + MultiByte + + + + + + + + + + + + + + + + + + + + + true + $(SolutionDir)..\artifacts\$(Configuration)\ + $(OutDir)Plugins\$(TargetName)\ + lib$(ProjectName) + .so + + + true + $(SolutionDir)..\artifacts\$(Configuration)\ + $(OutDir)Plugins\$(TargetName)\ + lib$(ProjectName) + .so + + + false + $(SolutionDir)..\artifacts\$(Configuration)\ + $(OutDir)Plugins\$(TargetName)\ + lib$(ProjectName) + .so + + + false + $(SolutionDir)..\artifacts\$(Configuration)\ + $(OutDir)Plugins\$(TargetName)\ + lib$(ProjectName) + .so + + + + NotUsing + Level3 + Disabled + true + _CRT_SECURE_NO_WARNINGS;SECURESOCKETS_ENABLED;BUILDING_NODE_EXTENSION;_DEBUG;DEVICEINFO_EXPORTS;_WINDOWS;_USRDLL;%(PreprocessorDefinitions) + true + $(FrameworkPath);$(FrameworkPath)/extensions;$(ContractsPath);$(ClientsPath);$(WindowsPath);$(WindowsPath)node;$(WindowsPath)zlib + true + + + Windows + true + $(OutDir);$(WindowsLibraries)\static_x64 + $(IntDir)$(TargetName).pdb + + + + + NotUsing + Level3 + Disabled + true + _CRT_SECURE_NO_WARNINGS;SECURESOCKETS_ENABLED;BUILDING_NODE_EXTENSION;WIN32;_DEBUG;DEVICEINFO_EXPORTS;_WINDOWS;_USRDLL;%(PreprocessorDefinitions) + true + $(FrameworkPath);$(FrameworkPath)/extensions;$(ContractsPath);$(ClientsPath);$(WindowsPath);$(WindowsPath)node;$(WindowsPath)zlib + true + + + Windows + true + $(OutDir);$(WindowsLibraries)\static_x32 + $(IntDir)$(TargetName).pdb + + + + + NotUsing + Level3 + MaxSpeed + true + true + true + _CRT_SECURE_NO_WARNINGS;SECURESOCKETS_ENABLED;BUILDING_NODE_EXTENSION;WIN32;NDEBUG;DEVICEINFO_EXPORTS;_WINDOWS;_USRDLL;%(PreprocessorDefinitions) + true + $(FrameworkPath);$(FrameworkPath)/extensions;$(ContractsPath);$(ClientsPath);$(WindowsPath);$(WindowsPath)node;$(WindowsPath)zlib + true + + + Windows + true + true + true + $(OutDir);$(WindowsLibraries)\static_x32 + $(IntDir)$(TargetName).pdb + + + + + NotUsing + Level3 + MaxSpeed + true + true + true + _CRT_SECURE_NO_WARNINGS;SECURESOCKETS_ENABLED;BUILDING_NODE_EXTENSION;NDEBUG;DEVICEINFO_EXPORTS;_WINDOWS;_USRDLL;%(PreprocessorDefinitions) + true + $(FrameworkPath);$(FrameworkPath)/extensions;$(ContractsPath);$(ClientsPath);$(WindowsPath);$(WindowsPath)node;$(WindowsPath)zlib + true + + + Windows + true + true + true + $(OutDir);$(WindowsLibraries)\static_x64 + $(IntDir)$(TargetName).pdb + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/ScriptEngine/ScriptEngine.vcxproj.filters b/ScriptEngine/ScriptEngine.vcxproj.filters new file mode 100644 index 000000000..d5d70579f --- /dev/null +++ b/ScriptEngine/ScriptEngine.vcxproj.filters @@ -0,0 +1,50 @@ + + + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + + + {f9088a2a-de85-4d28-98f5-7c768562503d} + + + {fa64b870-082b-4de4-a1b8-200b1145ad1b} + + + {b1ab155c-cafb-4ba8-8f6d-ed09ef4c19d4} + + + + + Documentation + + + \ No newline at end of file diff --git a/ScriptEngine/ScriptEngineImplementation.cpp b/ScriptEngine/ScriptEngineImplementation.cpp new file mode 100644 index 000000000..4ac7a2cf5 --- /dev/null +++ b/ScriptEngine/ScriptEngineImplementation.cpp @@ -0,0 +1,205 @@ +/* + * If not stated otherwise in this file or this component's LICENSE file the + * following copyright and licenses apply: + * + * Copyright 2020 Metrological + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "Module.h" +#include "Implementation.h" + +#include +#include + +const TCHAR script[] = _T("console.log('hello super duper 4, world!')"); + +namespace Thunder { +namespace Plugin { + +class ScriptEngineImplementation + : public Exchange::IScriptEngine + , public Exchange::IConfiguration + , public Core::Thread { +private: + class Config: public Core::JSON::Container { + public: + Config(Config&&) = delete; + Config(const Config&) = delete; + Config& operator=(const Config&) = delete; + + Config() + : Core::JSON::Container() + , Url() { + Add(_T("url"), &Url); + } + ~Config() override = default; + + public: + Core::JSON::String Url; + }; + + using Observers = std::vector; + +public: + ScriptEngineImplementation(ScriptEngineImplementation&&) = delete; + ScriptEngineImplementation(const ScriptEngineImplementation&) = delete; + ScriptEngineImplementation& operator=(const ScriptEngineImplementation&) = delete; + + ScriptEngineImplementation() + : _url() + , _adminLock() + , _clients() + , _platform(nullptr) + , _service(nullptr) { + } + ~ScriptEngineImplementation() override { + if (_service) { + _service->Release(); + _service = nullptr; + } + + if (_platform != nullptr) { + Core::Thread::Wait((Core::Thread::BLOCKED | Core::Thread::STOPPED), Core::infinite); + + script_destroy_platform(_platform); + } + } + +public: + // + // IConfiguration members + // --------------------------------------------------------------------------------- + uint32_t Configure(PluginHost::IShell* service) override { + uint32_t result = Core::ERROR_NONE; + + ASSERT((_service == nullptr) && (service != nullptr)); + ASSERT(_platform == nullptr); + + Config config; + string info(service->ConfigLine()); + config.FromString(info); + Core::SystemInfo::SetEnvironment(_T("HOME"), service->PersistentPath()); + + if (config.Url.IsSet() == true) { + _url = config.Url.Value(); + } + + std::string converted(Core::ToString(info)); + _platform = script_create_platform(converted.c_str()); + + if (_platform != nullptr) { + _service = service; + _service->AddRef(); + + Run(); + } + + return (result); + } + + // + // IConfiguration members + // --------------------------------------------------------------------------------- + Core::hresult Register(INotification* sink /* @in */) /* override */ { + ASSERT (sink != nullptr); + + _adminLock.Lock(); + + // Make sure a sink is not registered multiple times. + ASSERT(std::find(_clients.begin(), _clients.end(), sink) == _clients.end()); + + _clients.push_back(sink); + sink->AddRef(); + + _adminLock.Unlock(); + + return (Core::ERROR_NONE); + } + Core::hresult Unregister(const INotification* sink /* @in */) /* override */ { + + ASSERT (sink != nullptr); + + _adminLock.Lock(); + + Observers::iterator index(std::find(_clients.begin(), _clients.end(), sink)); + + // Make sure you do not unregister something you did not register !!! + ASSERT(index != _clients.end()); + + if (index != _clients.end()) { + (*index)->Release(); + _clients.erase(index); + } + + _adminLock.Unlock(); + + return (Core::ERROR_NONE); + } + + // @property + // @brief Script to be loaded into the engine and to be executed. + // @param url Loaded URL (e.g. https://example.com) + Core::hresult URL(string& url /* @out */) const /* override */ { + url = _url; + return (Core::ERROR_NONE); + } + Core::hresult URL(const string& url) /* override */ { + // Get the contents associated with the URL: + Core::URL info(url); + + if (info.IsValid() == true) { + if (info.Type() == Core::URL::SchemeType::SCHEME_FILE) { + + } + _url = url; + } + + return (Core::ERROR_NONE); + } + + BEGIN_INTERFACE_MAP (ScriptEngineImplementation) + INTERFACE_ENTRY (Exchange::IScriptEngine) + INTERFACE_ENTRY (Exchange::IConfiguration) + END_INTERFACE_MAP + +private: + uint32_t Worker() override { + if (IsRunning() == true) { + ASSERT(_platform != nullptr); + + uint32_t length = ( (sizeof(script)/sizeof(TCHAR)) - 1); + + if (script_prepare(_platform, length, script) == Core::ERROR_NONE) { + script_execute(_platform); + } + } + + Block(); + // Do plugin de-activation + return (Core::infinite); + } + +private: + string _url; + mutable Core::CriticalSection _adminLock; + Observers _clients; + platform* _platform; + PluginHost::IShell* _service; +}; + +SERVICE_REGISTRATION(ScriptEngineImplementation, 1, 0); + +} // namespace Plugin +} // namespace diff --git a/ScriptEngine/SourceTransfer.cpp b/ScriptEngine/SourceTransfer.cpp new file mode 100644 index 000000000..f7574f2c2 --- /dev/null +++ b/ScriptEngine/SourceTransfer.cpp @@ -0,0 +1,86 @@ +#include "Module.h" +#include "SourceTransfer.h" + +namespace Thunder { + namespace Plugin { + + /* static */ Core::ProxyPoolType SourceTransfer::_responseFactory(1); + + uint32_t SourceTransfer::Download(const string& url) { + uint32_t result = Core::ERROR_ILLEGAL_STATE; + _adminLock.Lock(); + + if (_url.IsValid() == true) { + _adminLock.Unlock(); + } + else { + result = Core::ERROR_INCORRECT_URL; + _url = Core::URL(url.c_str()); + + if (_url.IsValid() == false) { + _adminLock.Unlock(); + } + else if (_url.Type() == Core::URL::SchemeType::SCHEME_FILE) { + + if ((_url.Path().IsSet() == false) || (Core::File(_url.Path().Value()).Exists() == false)) { + _adminLock.Unlock(); + result = Core::ERROR_BAD_REQUEST; + } + else { + Core::File content(_url.Path().Value()); + + if (content.Open(true) == false) { + _adminLock.Unlock(); + result = Core::ERROR_OPENING_FAILED; + } + else { + uint32_t size = static_cast(content.Size()); + _source.reserve(size); + while (size != 0) { + uint8_t buffer[1024]; + uint32_t readBytes = content.Read(buffer, sizeof(buffer)); + size -= readBytes; + _source.append(reinterpret_cast(buffer), readBytes); + } + _adminLock.Unlock(); + + _callback->Transfered(url, Core::ERROR_NONE); + result = Core::ERROR_NONE; + } + } + } + else if (_url.Type() == Core::URL::SchemeType::SCHEME_HTTP) { + _downloader = Core::ProxyType::Create(*this, _url); + _adminLock.Unlock(); + result = Core::ERROR_INPROGRESS; + + } + else if (_url.Type() == Core::URL::SchemeType::SCHEME_HTTPS) { + _downloader = Core::ProxyType::Create(*this, _url); + _adminLock.Unlock(); + + result = Core::ERROR_INPROGRESS; + } + else { + _url.Clear(); + _adminLock.Unlock(); + result = Core::ERROR_INVALID_DESIGNATOR; + } + } + return (result); + } + uint32_t SourceTransfer::Abort() { + + _adminLock.Lock(); + + if (_url.IsValid() == true) { + if (_downloader.IsValid() == true) { + _downloader.Release(); + } + _url.Clear(); + } + _adminLock.Unlock(); + return (Core::ERROR_NONE); + } + } +} \ No newline at end of file diff --git a/ScriptEngine/SourceTransfer.h b/ScriptEngine/SourceTransfer.h new file mode 100644 index 000000000..cbc2af81a --- /dev/null +++ b/ScriptEngine/SourceTransfer.h @@ -0,0 +1,134 @@ +#pragma once + +#include "Module.h" + +namespace Thunder { + namespace Plugin { + class SourceTransfer { + private: + template + class WebClientType : public Web::WebLinkType&> { + private: + using BaseClass = Web::WebLinkType&>; + + public: + WebClientType() = delete; + WebClientType(WebClientType&& copy) = delete; + WebClientType(const WebClientType& copy) = delete; + WebClientType& operator=(const WebClientType&) = delete; + + WebClientType(SourceTransfer& parent, const Core::URL& url) + : BaseClass(5, _responseFactory, Core::SocketPort::STREAM, Core::NodeId(), Core::NodeId(), 2048, 2048) + , _parent(parent) + , _request() + , _content() { + if (url.Host().IsSet() == true) { + Core::NodeId nodeId(url.Host().Value().c_str(), url.Port().Value()); + BaseClass::Link().RemoteNode(nodeId); + BaseClass::Link().LocalNode(nodeId.AnyInterface()); + + _request.Verb = Web::Request::HTTP_GET; + _request.Path = url.Path().Value(); + + uint32_t result = BaseClass::Open(100); + + if ((result != Core::ERROR_INPROGRESS) && (result != Core::ERROR_NONE)) { + _request.Verb = Web::Request::HTTP_NONE; + BaseClass::Close(0); + } + } + } + ~WebClientType() override { + BaseClass::Close(Core::infinite); + } + + public: + // Notification of a Partial Request received, time to attach a body.. + void LinkBody(Core::ProxyType& element) override { + // Time to attach a String Body + element->Body(Core::ProxyType(_content)); + } + void Received(Core::ProxyType& element) override { + _request.Verb = Web::Request::HTTP_NONE; + _parent.StateChange(element->ErrorCode == Web::WebStatus::STATUS_OK ? Core::ERROR_NONE : Core::ERROR_BAD_REQUEST); + } + void Send(const Core::ProxyType& response) override { + } + void StateChange() override { + if ((BaseClass::IsOpen() == false) && (_request.Verb == Web::Request::HTTP_GET)) { + _request.Verb = Web::Request::HTTP_NONE; + _parent.StateChange(Core::ERROR_ASYNC_FAILED); + } + } + void Load(string content) { + content = std::move(_content); + } + + private: + SourceTransfer& _parent; + Core::ProxyObject _request; + Core::ProxyObject _content; + }; + + using WebClient = WebClientType; + #if defined(SECURESOCKETS_ENABLED) + using SecureWebClient = WebClientType; + #endif + + public: + struct ICallback { + virtual ~ICallback() = default; + virtual void Transfered(const string& url, const uint32_t result) = 0; + }; + + public: + SourceTransfer() = delete; + SourceTransfer(SourceTransfer&&) = delete; + SourceTransfer(const SourceTransfer&) = delete; + SourceTransfer& operator= (const SourceTransfer&) = delete; + + SourceTransfer(ICallback* callback, const string& storageSpace) + : _adminLock() + , _url() + , _callback(callback) + , _source() + , _downloader() { + } + ~SourceTransfer() { + Abort(); + } + + public: + inline void Reset() { + _url.Clear(); + } + inline bool IsValid() const { + return (_url.IsValid()); + } + inline bool IsLoaded() const { + return ((_url.IsValid()) && (_downloader.IsValid() == false)); + } + inline const string& Source() const { + return(_source); + } + uint32_t Download(const string& url); + uint32_t Abort(); + + private: + void StateChange(const uint32_t error) { + } + + private: + Core::CriticalSection _adminLock; + Core::URL _url; + ICallback* _callback; + string _source; + Core::ProxyType _downloader; + + // All requests needed by any instance of this socket class, is coming from this static factory. + // This means that all Requests, received are shared among all WebServer sockets, hopefully limiting + // the number of requests that need to be created. + static Core::ProxyPoolType _responseFactory; + }; + } +}