diff --git a/CMakeLists.txt b/CMakeLists.txt index 35ffbc4..d54bc4b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -32,7 +32,7 @@ option(NATIVEJS_L2_BUILD "NATIVEJS_L2_BUILD" OFF) option(JSRUNTIME_ENGINE_NAME "JSRUNTIME_ENGINE_NAME" "jsc") option(ENABLE_JSRUNTIME_ESSOS "ENABLE_JSRUNTIME_ESSOS" OFF) option(ENABLE_WEBSOCKET_SERVER "ENABLE_WEBSOCKET_SERVER" OFF) -option(ENABLE_REMOTE_INSPECTOR "ENABLE_REMOTE_INSPECTOR" OFF) +option(REMOTE_INSPECTOR_ENABLE "REMOTE_INSPECTOR_ENABLE" OFF) option(ENABLE_AAMP_JSBINDINGS "ENABLE_AAMP_JSBINDINGS" OFF) option(ENABLE_AAMP_JSBINDINGS_STATIC "ENABLE_AAMP_JSBINDINGS_STATIC" OFF) option(ENABLE_AAMP_JSBINDINGS_DYNAMIC "ENABLE_AAMP_JSBINDINGS_DYNAMIC" OFF) @@ -79,9 +79,9 @@ if (ENABLE_JSRUNTIME_ESSOS) add_definitions("-DENABLE_ESSOS") endif (ENABLE_JSRUNTIME_ESSOS) -if ( ENABLE_REMOTE_INSPECTOR ) - add_definitions("-DENABLE_REMOTE_INSPECTOR") -endif ( ENABLE_REMOTE_INSPECTOR ) +if ( REMOTE_INSPECTOR_ENABLE ) + add_definitions("-DREMOTE_INSPECTOR_ENABLE") +endif ( REMOTE_INSPECTOR_ENABLE) if ( ENABLE_JSRUNTIME_PLAYER ) add_definitions("-DENABLE_JSRUNTIME_PLAYER") @@ -154,6 +154,11 @@ if (ENABLE_JSRUNTIME_THUNDER_SECURITYAGENT) add_definitions("-DENABLE_JSRUNTIME_THUNDER_SECURITYAGENT") set(JSRUNTIME_LINK_LIBRARIES ${JSRUNTIME_LINK_LIBRARIES} -lsecurityagent) endif(ENABLE_JSRUNTIME_THUNDER_SECURITYAGENT) + +if (REMOTE_INSPECTOR_ENABLE) + set(JSRUNTIME_LINK_LIBRARIES ${JSRUNTIME_LINK_LIBRARIES} -lsoup-3.0 -lgio-2.0) +endif(REMOTE_INSPECTOR_ENABLE) + if (BUILD_JSRUNTIME_DESKTOP) set(JSRUNTIME_INCLUDE_DIRECTORIES ${JSRUNTIME_INCLUDE_DIRECTORIES} $ENV{PKG_CONFIG_SYSROOT_DIR}/include $ENV{PKG_CONFIG_SYSROOT_DIR}/include/glib-2.0 $ENV{PKG_CONFIG_SYSROOT_DIR}/include/glib-2.0/glib $ENV{PKG_CONFIG_SYSROOT_DIR}/include/WPEFramework $ENV{PKG_CONFIG_SYSROOT_DIR}/include/uwebsockets) set(JSRUNTIME_LIBRARY_LINK_DIRECTORIES ${JSRUNTIME_LIBRARY_LINK_DIRECTORIES} -L$ENV{PKG_CONFIG_SYSROOT_DIR}/lib) @@ -175,6 +180,9 @@ if (BUILD_JSRUNTIME_DESKTOP) set(JSRUNTIME_LIBRARY_LINK_DIRECTORIES ${JSRUNTIME_LIBRARY_LINK_DIRECTORIES} -L${CMAKE_CURRENT_SOURCE_DIR}/build/) else () set(JSRUNTIME_INCLUDE_DIRECTORIES ${JSRUNTIME_INCLUDE_DIRECTORIES} $ENV{PKG_CONFIG_SYSROOT_DIR}/usr/include/glib-2.0 $ENV{PKG_CONFIG_SYSROOT_DIR}/usr/lib/glib-2.0/include/ $ENV{PKG_CONFIG_SYSROOT_DIR}/usr/include/rtcore $ENV{PKG_CONFIG_SYSROOT_DIR}/usr/include/WPEFramework $ENV{PKG_CONFIG_SYSROOT_DIR}/usr/include/gstreamer-1.0 $ENV{PKG_CONFIG_SYSROOT_DIR}/usr/include/uwebsockets) + if (REMOTE_INSPECTOR_ENABLE) + set(JSRUNTIME_INCLUDE_DIRECTORIES ${JSRUNTIME_INCLUDE_DIRECTORIES} $ENV{PKG_CONFIG_SYSROOT_DIR}/usr/include/libsoup-3.0) + endif(REMOTE_INSPECTOR_ENABLE) set(JSRUNTIME_LINK_LIBRARIES ${JSRUNTIME_LINK_LIBRARIES} -lessos) set(JSRUNTIME_LIBRARY_LINK_DIRECTORIES ${JSRUNTIME_LIBRARY_LINK_DIRECTORIES} -L${CMAKE_CURRENT_SOURCE_DIR}/../build/) endif (BUILD_JSRUNTIME_DESKTOP) diff --git a/include/InspectorHTTPServer.h b/include/InspectorHTTPServer.h new file mode 100644 index 0000000..68a1bb0 --- /dev/null +++ b/include/InspectorHTTPServer.h @@ -0,0 +1,98 @@ +/** +* If not stated otherwise in this file or this component's LICENSE +* file the following copyright and licenses apply: +* +* Copyright 2024 RDK Management +* +* 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 + +#ifdef REMOTE_INSPECTOR_ENABLE + +#include +#include +#include +#include +#include +#include + +class InspectorHTTPServer { +public: + static InspectorHTTPServer& singleton(); + ~InspectorHTTPServer(); + + bool start(const char* address, int port); + void stop(); + + bool isRunning() const { return m_server != nullptr; } + + void registerContext(JSGlobalContextRef context, const char* title, const char* url); + + void unregisterContext(JSGlobalContextRef context); + + void sendConsoleMessage(JSContextRef context, const char* level, const char* text); + + void registerScript(const char* url, const char* source); + + // Called when frontend sends Page.reload. + void setReloadCallback(std::function callback); + +private: + InspectorHTTPServer(); + + static void onHTTPRequest(SoupServer* server, SoupServerMessage* msg, + const char* path, GHashTable* query, + gpointer userData); + + static void onWebSocketRequest(SoupServer* server, SoupServerMessage* msg, + const char* path, SoupWebsocketConnection* connection, + gpointer userData); + + static void onWebSocketMessage(SoupWebsocketConnection* connection, + SoupWebsocketDataType dataType, + GBytes* message, gpointer userData); + + static void onWebSocketClosed(SoupWebsocketConnection* connection, gpointer userData); + + std::string generateTargetListJSON(); + + void handleCDPMessage(SoupWebsocketConnection* connection, const char* message); + + struct ContextInfo { + JSGlobalContextRef context; + std::string title; + std::string url; + uint64_t id; + }; + + struct ScriptInfo { + std::string id; + std::string url; + std::string source; + }; + + SoupServer* m_server; + int m_port; + std::map m_contexts; + std::map m_connections; + std::map m_scripts; + std::mutex m_scriptsMutex; + uint64_t m_nextContextId; + uint64_t m_nextScriptId; + std::function m_reloadCallback; +}; + +#endif // REMOTE_INSPECTOR_ENABLE + diff --git a/src/InspectorHTTPServer.cpp b/src/InspectorHTTPServer.cpp new file mode 100644 index 0000000..aae892d --- /dev/null +++ b/src/InspectorHTTPServer.cpp @@ -0,0 +1,741 @@ +/** +* If not stated otherwise in this file or this component's LICENSE +* file the following copyright and licenses apply: +* +* Copyright 2024 RDK Management +* +* 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. +**/ + +#ifdef REMOTE_INSPECTOR_ENABLE + +#include "InspectorHTTPServer.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +static std::string escapeJSON(const char* str) +{ + if (!str) { + return {}; + } + + static const std::array kEscapes = [] { + std::array table{}; + table['"'] = "\\\""; + table['\\'] = "\\\\"; + table['\b'] = "\\b"; + table['\f'] = "\\f"; + table['\n'] = "\\n"; + table['\r'] = "\\r"; + table['\t'] = "\\t"; + return table; + }(); + static const char kHexDigits[] = "0123456789abcdef"; + + std::string escaped; + escaped.reserve(std::strlen(str) + 8); + + for (const unsigned char* p = reinterpret_cast(str); *p; ++p) { + const char* replacement = kEscapes[*p]; + if (replacement) { + escaped.append(replacement); + continue; + } + + if (*p < 0x20) { + escaped += "\\u00"; + escaped.push_back(kHexDigits[(*p >> 4) & 0x0f]); + escaped.push_back(kHexDigits[*p & 0x0f]); + } else { + escaped.push_back(static_cast(*p)); + } + } + + return escaped; +} + +InspectorHTTPServer& InspectorHTTPServer::singleton() +{ + static InspectorHTTPServer instance; + return instance; +} + +InspectorHTTPServer::InspectorHTTPServer() + : m_server(nullptr) + , m_port(0) + , m_nextContextId(1) + , m_nextScriptId(1) +{ +} + +InspectorHTTPServer::~InspectorHTTPServer() +{ + stop(); +} + +bool InspectorHTTPServer::start(const char* address, int port) +{ + if (m_server) { + NativeJSLogger::log(DEBUG, "InspectorHTTPServer: Already running\n"); + return false; + } + + m_server = soup_server_new(nullptr, nullptr); + + if (!m_server) { + NativeJSLogger::log(ERROR, "InspectorHTTPServer: Failed to create server\n"); + return false; + } + + soup_server_add_handler(m_server, "/", onHTTPRequest, this, nullptr); + soup_server_add_handler(m_server, "/json", onHTTPRequest, this, nullptr); + soup_server_add_handler(m_server, "/json/list", onHTTPRequest, this, nullptr); + soup_server_add_handler(m_server, "/json/version", onHTTPRequest, this, nullptr); + + soup_server_add_websocket_handler(m_server, "/devtools", nullptr, nullptr, + onWebSocketRequest, this, nullptr); + + // Bind requested port; if busy, retry next few ports. + static const int kMaxPortTries = 10; + int boundPort = -1; + for (int tryPort = port; tryPort < port + kMaxPortTries; tryPort++) { + GError* error = nullptr; + gboolean success = soup_server_listen_all(m_server, tryPort, + static_cast(0), + &error); + if (success) { + boundPort = tryPort; + break; + } + NativeJSLogger::log(DEBUG, "InspectorHTTPServer: Port %d busy (%s), trying %d\n", + tryPort, error ? error->message : "unknown", tryPort + 1); + if (error) g_error_free(error); + } + + if (boundPort < 0) { + NativeJSLogger::log(WARN, "InspectorHTTPServer: No free port found in range %d-%d\n", + port, port + kMaxPortTries - 1); + g_object_unref(m_server); + m_server = nullptr; + return false; + } + + m_port = boundPort; + + NativeJSLogger::log(INFO, "InspectorHTTPServer: Started on %s:%d\n", address, boundPort); + NativeJSLogger::log(DEBUG, "InspectorHTTPServer: Open chrome://inspect in Chrome to connect\n"); + + return true; +} + +void InspectorHTTPServer::stop() +{ + if (m_server) { + soup_server_disconnect(m_server); + g_object_unref(m_server); + m_server = nullptr; + NativeJSLogger::log(INFO, "InspectorHTTPServer: Stopped\n"); + } + + for (auto& pair : m_connections) { + g_object_unref(pair.first); + } + + m_contexts.clear(); + m_connections.clear(); +} + +void InspectorHTTPServer::registerContext(JSGlobalContextRef context, const char* title, const char* url) +{ + NativeJSLogger::log(DEBUG, "InspectorHTTPServer: registerContext called - context=%p, title=%s, url=%s\n", + context, title ? title : "null", url ? url : "null"); + + if (!context) { + NativeJSLogger::log(WARN, "InspectorHTTPServer: registerContext - context is NULL, aborting\n"); + return; + } + + ContextInfo info; + info.context = context; + info.title = title ? title : "JavaScript Context"; + info.url = url ? url : "rdknativescript://context"; + info.id = m_nextContextId++; + + m_contexts[context] = info; + + NativeJSLogger::log(INFO, "InspectorHTTPServer: Registered context '%s' (id=%llu), total contexts=%zu\n", + info.title.c_str(), info.id, m_contexts.size()); +} + +void InspectorHTTPServer::unregisterContext(JSGlobalContextRef context) +{ + auto it = m_contexts.find(context); + if (it != m_contexts.end()) { + NativeJSLogger::log(INFO, "InspectorHTTPServer: Unregistered context (id=%llu)\n", it->second.id); + m_contexts.erase(it); + } +} + +void InspectorHTTPServer::sendConsoleMessage(JSContextRef context, const char* level, const char* text) +{ + if (!context || !level || !text) return; + + JSGlobalContextRef globalContext = const_cast(context); + + SoupWebsocketConnection* targetConnection = nullptr; + for (const auto& pair : m_connections) { + if (pair.second == globalContext) { + targetConnection = pair.first; + break; + } + } + + if (!targetConnection) { + NativeJSLogger::log(DEBUG, "InspectorHTTPServer: No WebSocket connection found for this context\n"); + return; + } + + SoupWebsocketState state = soup_websocket_connection_get_state(targetConnection); + if (state != SOUP_WEBSOCKET_STATE_OPEN) { + NativeJSLogger::log(DEBUG, "InspectorHTTPServer: WebSocket connection is not open (state=%d)\n", state); + return; + } + + const std::string escapedText = escapeJSON(text); + + std::ostringstream event; + event << "{\"method\":\"Runtime.consoleAPICalled\",\"params\":{" + << "\"type\":\"" << level << "\"," + << "\"args\":[{\"type\":\"string\",\"value\":\"" << escapedText << "\"}]," + << "\"executionContextId\":1," + << "\"timestamp\":" << (long long)(time(nullptr) * 1000) + << "}}"; + + std::string eventStr = event.str(); + soup_websocket_connection_send_text(targetConnection, eventStr.c_str()); + +} + +void InspectorHTTPServer::setReloadCallback(std::function callback) +{ + m_reloadCallback = std::move(callback); +} + +void InspectorHTTPServer::registerScript(const char* url, const char* source) +{ + if (!url || !source) return; + + // Skip tiny internal snippets. + if (strlen(source) < 10) return; + + std::lock_guard lock(m_scriptsMutex); + + for (const auto& pair : m_scripts) { + if (pair.second.url == url) return; + } + + std::string scriptId = std::to_string(m_nextScriptId++); + + ScriptInfo info; + info.id = scriptId; + info.url = url; + info.source = source; + m_scripts[scriptId] = info; + + std::ostringstream evt; + evt << "{\"method\":\"Debugger.scriptParsed\",\"params\":{" + << "\"scriptId\":\"" << scriptId << "\"," + << "\"url\":\"" << escapeJSON(url) << "\"," + << "\"startLine\":0,\"startColumn\":0," + << "\"endLine\":0,\"endColumn\":0," + << "\"executionContextId\":1,\"hash\":\"\"" + << "}}"; + std::string evtStr = evt.str(); + + for (const auto& pair : m_connections) { + SoupWebsocketConnection* conn = pair.first; + if (soup_websocket_connection_get_state(conn) == SOUP_WEBSOCKET_STATE_OPEN) { + soup_websocket_connection_send_text(conn, evtStr.c_str()); + } + } +} + +std::string InspectorHTTPServer::generateTargetListJSON() +{ + std::ostringstream json; + json << "["; + + bool first = true; + for (const auto& pair : m_contexts) { + const ContextInfo& info = pair.second; + + if (!first) json << ","; + first = false; + + json << "{"; + json << "\"id\":\"" << info.id << "\","; + json << "\"type\":\"page\","; + json << "\"title\":\"" << info.title << "\","; + json << "\"url\":\"" << info.url << "\","; + json << "\"webSocketDebuggerUrl\":\"ws://127.0.0.1:" << m_port << "/devtools/page/" << info.id << "\","; + json << "\"devtoolsFrontendUrl\":\"devtools://devtools/bundled/inspector.html?ws=127.0.0.1:" << m_port << "/devtools/page/" << info.id << "\""; + json << "}"; + } + + json << "]"; + + std::string result = json.str(); + return result; +} + +void InspectorHTTPServer::onHTTPRequest(SoupServer* server, SoupServerMessage* msg, + const char* path, GHashTable* query, + gpointer userData) +{ + InspectorHTTPServer* self = static_cast(userData); + + if (strcmp(path, "/") == 0 || strcmp(path, "/inspector") == 0) { + const char* possiblePaths[] = { + "src/nativevjsinspector.html", + "../src/nativevjsinspector.html", + "/runtime/modules/nativevjsinspector.html", + nullptr + }; + + std::string htmlContent; + bool loaded = false; + + char cwdBuf[1024] = {}; + getcwd(cwdBuf, sizeof(cwdBuf)); + + for (int i = 0; possiblePaths[i] != nullptr; i++) { + char resolvedPath[4096] = {}; + realpath(possiblePaths[i], resolvedPath); + + std::ifstream file(possiblePaths[i]); + if (file.is_open()) { + htmlContent.assign((std::istreambuf_iterator(file)), + std::istreambuf_iterator()); + file.close(); + loaded = true; + char absPath[4096] = {}; + realpath(possiblePaths[i], absPath); + break; + } + } + + if (loaded) { + SoupMessageHeaders* headers = soup_server_message_get_response_headers(msg); + soup_message_headers_set_content_type(headers, "text/html", nullptr); + soup_server_message_set_response(msg, "text/html", SOUP_MEMORY_COPY, + htmlContent.c_str(), htmlContent.length()); + soup_server_message_set_status(msg, SOUP_STATUS_OK, nullptr); + return; + } + + NativeJSLogger::log(WARN, "InspectorHTTPServer: nativevjsinspector.html not found, using fallback\n"); + + const char* fallbackHTML = R"HTML( +Inspector Error +

Inspector UI Not Found

Could not load nativevjsinspector.html

)HTML"; + + SoupMessageHeaders* headers = soup_server_message_get_response_headers(msg); + soup_message_headers_set_content_type(headers, "text/html", nullptr); + soup_server_message_set_response(msg, "text/html", SOUP_MEMORY_STATIC, + fallbackHTML, strlen(fallbackHTML)); + soup_server_message_set_status(msg, SOUP_STATUS_OK, nullptr); + return; + } + + if (strcmp(path, "/json/version") == 0) { + std::ostringstream version; + version << "{" + << "\"Browser\":\"RDK NativeScript/1.0\"," + << "\"Protocol-Version\":\"1.3\"," + << "\"User-Agent\":\"RDK NativeScript\"," + << "\"V8-Version\":\"JavaScriptCore\"," + << "\"WebKit-Version\":\"JavaScriptCore\"," + << "\"webSocketDebuggerUrl\":\"ws://127.0.0.1:" << self->m_port << "/devtools\"" + << "}"; + + std::string versionStr = version.str(); + + SoupMessageHeaders* headers = soup_server_message_get_response_headers(msg); + soup_message_headers_set_content_type(headers, "application/json", nullptr); + soup_message_headers_append(headers, "Access-Control-Allow-Origin", "*"); + soup_server_message_set_response(msg, "application/json", SOUP_MEMORY_COPY, + versionStr.c_str(), versionStr.length()); + soup_server_message_set_status(msg, SOUP_STATUS_OK, nullptr); + return; + } + + std::string jsonResponse = self->generateTargetListJSON(); + + SoupMessageHeaders* headers = soup_server_message_get_response_headers(msg); + soup_message_headers_set_content_type(headers, "application/json", nullptr); + soup_message_headers_append(headers, "Access-Control-Allow-Origin", "*"); + + soup_server_message_set_response(msg, "application/json", SOUP_MEMORY_COPY, + jsonResponse.c_str(), jsonResponse.length()); + soup_server_message_set_status(msg, SOUP_STATUS_OK, nullptr); +} + +void InspectorHTTPServer::onWebSocketRequest(SoupServer* server, SoupServerMessage* msg, + const char* path, SoupWebsocketConnection* connection, + gpointer userData) +{ + InspectorHTTPServer* self = static_cast(userData); + + uint64_t contextId = 0; + if (sscanf(path, "/devtools/page/%llu", &contextId) == 1) { + JSGlobalContextRef targetContext = nullptr; + for (const auto& pair : self->m_contexts) { + if (pair.second.id == contextId) { + targetContext = pair.first; + break; + } + } + + if (targetContext) { + g_object_ref(connection); + self->m_connections[connection] = targetContext; + + g_signal_connect(connection, "message", G_CALLBACK(onWebSocketMessage), userData); + g_signal_connect(connection, "closed", G_CALLBACK(onWebSocketClosed), userData); + + NativeJSLogger::log(DEBUG, "InspectorHTTPServer: WebSocket connected to context %llu\n", contextId); + } else { + NativeJSLogger::log(WARN, "InspectorHTTPServer: Context %llu not found\n", contextId); + soup_websocket_connection_close(connection, SOUP_WEBSOCKET_CLOSE_NORMAL, "Context not found"); + } + } else { + NativeJSLogger::log(WARN, "InspectorHTTPServer: Invalid WebSocket path: %s\n", path); + soup_websocket_connection_close(connection, SOUP_WEBSOCKET_CLOSE_NORMAL, "Invalid path"); + } +} + +void InspectorHTTPServer::onWebSocketMessage(SoupWebsocketConnection* connection, + SoupWebsocketDataType dataType, + GBytes* message, gpointer userData) +{ + InspectorHTTPServer* self = static_cast(userData); + + if (dataType != SOUP_WEBSOCKET_DATA_TEXT) { + NativeJSLogger::log(DEBUG, "InspectorHTTPServer: Received non-text WebSocket message\n"); + return; + } + + gsize size; + const char* data = static_cast(g_bytes_get_data(message, &size)); + + if (data && size > 0) { + std::string messageStr(data, size); + self->handleCDPMessage(connection, messageStr.c_str()); + } +} + +void InspectorHTTPServer::onWebSocketClosed(SoupWebsocketConnection* connection, gpointer userData) +{ + InspectorHTTPServer* self = static_cast(userData); + + auto it = self->m_connections.find(connection); + if (it != self->m_connections.end()) { + NativeJSLogger::log(DEBUG, "InspectorHTTPServer: WebSocket connection closed\n"); + self->m_connections.erase(it); + g_object_unref(connection); + } +} + +void InspectorHTTPServer::handleCDPMessage(SoupWebsocketConnection* connection, const char* message) +{ + auto it = m_connections.find(connection); + if (it == m_connections.end()) { + NativeJSLogger::log(DEBUG, "InspectorHTTPServer: No context for connection\n"); + return; + } + + JSGlobalContextRef context = it->second; + + std::string msg(message); + + size_t idPos = msg.find("\"id\":"); + int messageId = 1; + if (idPos != std::string::npos) { + sscanf(msg.c_str() + idPos + 5, "%d", &messageId); + } + + std::string method; + size_t methodPos = msg.find("\"method\":\""); + if (methodPos != std::string::npos) { + size_t methodStart = methodPos + 10; + size_t methodEnd = msg.find("\"", methodStart); + if (methodEnd != std::string::npos) { + method = msg.substr(methodStart, methodEnd - methodStart); + } + } + + std::ostringstream response; + + if (method == "Runtime.enable") { + response << "{\"id\":" << messageId << ",\"result\":{}}"; + + std::ostringstream contextCreated; + contextCreated << "{\"method\":\"Runtime.executionContextCreated\",\"params\":{" + << "\"context\":{" + << "\"id\":1," + << "\"origin\":\"\"," + << "\"name\":\"RDK NativeScript\"" + << "}}}"; + std::string eventStr = contextCreated.str(); + soup_websocket_connection_send_text(connection, eventStr.c_str()); + } + else if (method == "Debugger.enable" || + method == "Console.enable" || method == "Runtime.runIfWaitingForDebugger" || + method == "Profiler.enable" || method == "Profiler.setSamplingInterval" || + method == "HeapProfiler.enable" || method == "Debugger.setAsyncCallStackDepth" || + method == "Debugger.setPauseOnExceptions" || method == "Debugger.setBlackboxPatterns" || + method == "Runtime.compileScript" || method == "Page.enable" || + method == "Page.getResourceTree" || method == "Network.enable" || + method == "Log.enable" || method == "Log.startViolationsReport") { + response << "{\"id\":" << messageId << ",\"result\":{}}"; + + // Replay known scripts after Debugger.enable. + if (method == "Debugger.enable") { + std::lock_guard lock(m_scriptsMutex); + for (const auto& pair : m_scripts) { + const ScriptInfo& info = pair.second; + std::ostringstream scriptParsed; + scriptParsed << "{\"method\":\"Debugger.scriptParsed\",\"params\":{" + << "\"scriptId\":\"" << info.id << "\"," + << "\"url\":\"" << escapeJSON(info.url.c_str()) << "\"," + << "\"startLine\":0,\"startColumn\":0," + << "\"endLine\":0,\"endColumn\":0," + << "\"executionContextId\":1,\"hash\":\"\"" + << "}}"; + std::string evtStr = scriptParsed.str(); + soup_websocket_connection_send_text(connection, evtStr.c_str()); + } + } + } + else if (method == "Runtime.evaluate") { + // Parse expression while honoring escaped quotes. + size_t exprPos = msg.find("\"expression\":\""); + std::string expression; + if (exprPos != std::string::npos) { + size_t i = exprPos + 14; // skip past "expression":" + while (i < msg.size()) { + if (msg[i] == '\\' && i + 1 < msg.size()) { + char next = msg[i + 1]; + switch (next) { + case '"': expression += '"'; break; + case '\\': expression += '\\'; break; + case 'n': expression += '\n'; break; + case 'r': expression += '\r'; break; + case 't': expression += '\t'; break; + default: expression += '\\'; expression += next; break; + } + i += 2; + } else if (msg[i] == '"') { + break; + } else { + expression += msg[i++]; + } + } + } + + if (!expression.empty()) { + // fullScript=true: evaluate script as-is. + bool isFullScript = (msg.find("\"fullScript\":true") != std::string::npos); + + JSStringRef script = nullptr; + if (isFullScript) { + script = JSStringCreateWithUTF8CString(expression.c_str()); + } else { + // Wrap expression to normalize result as a string. + std::string evalWrapper = + "(function(){" + " var __r = (" + expression + ");" + " if (__r === null) return 'null';" + " if (__r === undefined) return 'undefined';" + " if (typeof __r === 'object' || typeof __r === 'function') {" + " try { return JSON.stringify(__r, null, 2); }" + " catch(e) { return String(__r); }" + " }" + " return String(__r);" + "})()"; + script = JSStringCreateWithUTF8CString(evalWrapper.c_str()); + } + + JSValueRef exception = nullptr; + JSValueRef result = JSEvaluateScript(context, script, nullptr, nullptr, 0, &exception); + JSStringRelease(script); + + if (exception) { + JSStringRef exStr = JSValueToStringCopy(context, exception, nullptr); + size_t maxSize = JSStringGetMaximumUTF8CStringSize(exStr); + char* buffer = new char[maxSize]; + JSStringGetUTF8CString(exStr, buffer, maxSize); + JSStringRelease(exStr); + + response << "{\"id\":" << messageId + << ",\"result\":{\"exceptionDetails\":{\"text\":\"" + << escapeJSON(buffer) << "\"}}}"; + delete[] buffer; + } else if (!isFullScript && result) { + JSStringRef resultStr = JSValueToStringCopy(context, result, nullptr); + size_t maxSize = JSStringGetMaximumUTF8CStringSize(resultStr); + char* buffer = new char[maxSize]; + JSStringGetUTF8CString(resultStr, buffer, maxSize); + JSStringRelease(resultStr); + + response << "{\"id\":" << messageId + << ",\"result\":{\"result\":{\"type\":\"string\",\"value\":\"" + << escapeJSON(buffer) << "\"}}}"; + delete[] buffer; + } else { + response << "{\"id\":" << messageId + << ",\"result\":{\"result\":{\"type\":\"undefined\",\"value\":\"undefined\"}}}"; + } + } else { + response << "{\"id\":" << messageId << ",\"result\":{}}"; + } + } + else if (method == "Page.reload") { + NativeJSLogger::log(INFO, "InspectorHTTPServer: Page.reload requested\n"); + + // Ack first so frontend does not time out. + response << "{\"id\":" << messageId << ",\"result\":{}}"; + std::string ackStr = response.str(); + soup_websocket_connection_send_text(connection, ackStr.c_str()); + + const char* destroyedEvt = + "{\"method\":\"Runtime.executionContextDestroyed\"," + "\"params\":{\"executionContextId\":1}}"; + soup_websocket_connection_send_text(connection, destroyedEvt); + + { + std::lock_guard lock(m_scriptsMutex); + m_scripts.clear(); + m_nextScriptId = 1; + } + + if (m_reloadCallback) { + NativeJSLogger::log(DEBUG, "InspectorHTTPServer: Invoking reload callback\n"); + m_reloadCallback(); + } else { + NativeJSLogger::log(WARN, "InspectorHTTPServer: No reload callback registered\n"); + } + + const char* createdEvt = + "{\"method\":\"Runtime.executionContextCreated\",\"params\":{\"context\":{" + "\"id\":1,\"origin\":\"\",\"name\":\"RDK NativeScript\"}}}"; + soup_websocket_connection_send_text(connection, createdEvt); + + NativeJSLogger::log(INFO, "InspectorHTTPServer: Page.reload complete\n"); + return; + } + else if (method == "Runtime.getProperties") { + response << "{\"id\":" << messageId << ",\"result\":{\"result\":[]}}"; + } + else if (method == "Runtime.getIsolateId") { + response << "{\"id\":" << messageId << ",\"result\":{\"id\":\"isolate-rdknativescript-1\"}}"; + } + else if (method == "Runtime.getHeapUsage") { + response << "{\"id\":" << messageId << ",\"result\":{\"usedSize\":1048576,\"totalSize\":10485760}}"; + } + else if (method == "Inspector.readFile") { + size_t pathPos = msg.find("\"path\":\""); + std::string filePath; + if (pathPos != std::string::npos) { + size_t pathStart = pathPos + 8; + size_t pathEnd = msg.find("\"", pathStart); + if (pathEnd != std::string::npos) { + filePath = msg.substr(pathStart, pathEnd - pathStart); + + size_t pos = 0; + while ((pos = filePath.find("\\\\", pos)) != std::string::npos) { + filePath.replace(pos, 2, "\\"); + pos += 1; + } + } + } + + if (!filePath.empty()) { + NativeJSLogger::log(DEBUG, "InspectorHTTPServer: Reading file: %s\n", filePath.c_str()); + + std::ifstream file(filePath); + if (file.is_open()) { + std::string content((std::istreambuf_iterator(file)), + std::istreambuf_iterator()); + file.close(); + + std::string escapedContent = escapeJSON(content.c_str()); + std::string escapedPath = escapeJSON(filePath.c_str()); + + response << "{\"id\":" << messageId + << ",\"result\":{\"filename\":\"" << escapedPath + << "\",\"content\":\"" << escapedContent << "\"}}"; + } else { + response << "{\"id\":" << messageId + << ",\"result\":{\"filename\":\"" << escapeJSON(filePath.c_str()) + << "\",\"error\":\"File not found or cannot be opened\"}}"; + } + } else { + response << "{\"id\":" << messageId << ",\"result\":{\"error\":\"No path specified\"}}"; + } + } + else if (method == "Debugger.getScriptSource") { + size_t scriptIdPos = msg.find("\"scriptId\":\""); + std::string scriptId; + if (scriptIdPos != std::string::npos) { + size_t idStart = scriptIdPos + 12; + size_t idEnd = msg.find("\"", idStart); + if (idEnd != std::string::npos) { + scriptId = msg.substr(idStart, idEnd - idStart); + } + } + + if (!scriptId.empty()) { + std::lock_guard lock(m_scriptsMutex); + auto sit = m_scripts.find(scriptId); + if (sit != m_scripts.end()) { + response << "{\"id\":" << messageId + << ",\"result\":{\"scriptSource\":\"" + << escapeJSON(sit->second.source.c_str()) << "\"}}"; + } else { + response << "{\"id\":" << messageId + << ",\"result\":{\"scriptSource\":\"// Script id " + << scriptId << " not found\"}}"; + } + } else { + response << "{\"id\":" << messageId << ",\"result\":{\"scriptSource\":\"\"}}"; + } + } + else { + response << "{\"id\":" << messageId << ",\"result\":{}}"; + } + + std::string responseStr = response.str(); + soup_websocket_connection_send_text(connection, responseStr.c_str()); + +} + +#endif // REMOTE_INSPECTOR_ENABLE diff --git a/src/JavaScriptContextBase.cpp b/src/JavaScriptContextBase.cpp index 8bee572..d2b4206 100644 --- a/src/JavaScriptContextBase.cpp +++ b/src/JavaScriptContextBase.cpp @@ -19,6 +19,9 @@ #include #include +#ifdef REMOTE_INSPECTOR_ENABLE +#include +#endif #include #include #include @@ -116,7 +119,12 @@ bool JavaScriptContextBase::runFile(const char *file, const char* args, bool isA fflush(stdout); return false; } - return evaluateScript(scriptToRun.c_str(), isApplication?file:nullptr, args, isApplication); + bool ret = evaluateScript(scriptToRun.c_str(), isApplication?file:nullptr, args, isApplication); +#ifdef REMOTE_INSPECTOR_ENABLE + // Register every file-based script so it shows up in the Sources panel, + if (ret) InspectorHTTPServer::singleton().registerScript(file, scriptToRun.c_str()); +#endif + return ret; } bool JavaScriptContextBase::runScript(const char *script, bool isModule, std::string name, const char *args, bool isApplication) diff --git a/src/jsc/JavaScriptContext.cpp b/src/jsc/JavaScriptContext.cpp index 81bae51..5779bcf 100644 --- a/src/jsc/JavaScriptContext.cpp +++ b/src/jsc/JavaScriptContext.cpp @@ -27,7 +27,9 @@ #include #include #include - +#ifdef REMOTE_INSPECTOR_ENABLE +#include +#endif #include #include #include @@ -77,7 +79,28 @@ JavaScriptContext::JavaScriptContext(JavaScriptContextFeatures& features, std::s mContext = JSGlobalContextCreateInGroup(mContextGroup, nullptr); mPriv = rtJSCContextPrivate::create(mContext); if (!gTopLevelContext) + { gTopLevelContext = mContext; + +#ifdef REMOTE_INSPECTOR_ENABLE + // Register this context with the web inspector server + InspectorHTTPServer::singleton().registerContext(gTopLevelContext, + "RDK NativeScript", + "rdknativescript://main"); + + // Register a reload callback so the inspector's Reload button re-executes + // the application entry script inside the existing JSC context. + InspectorHTTPServer::singleton().setReloadCallback([this]() { + if (!mApplicationUrl.empty()) { + rtLogInfo("Web Inspector: Reloading application: %s\n", mApplicationUrl.c_str()); + runFile(mApplicationUrl.c_str(), nullptr, true); + } else { + rtLogInfo("Web Inspector: Reload requested but mApplicationUrl is empty\n"); + } + }); + rtLogInfo("Web Inspector: Context registered\n"); +#endif + } registerUtils(); registerCommonUtils(); mNetworkMetricsData = new rtMapObject(); @@ -116,6 +139,10 @@ if (mModuleSettings.enablePlayer) { JSSynchronousGarbageCollectForDebugging(gTopLevelContext); } +#ifdef REMOTE_INSPECTOR_ENABLE + InspectorHTTPServer::singleton().setReloadCallback(nullptr); + InspectorHTTPServer::singleton().unregisterContext(gTopLevelContext); +#endif gTopLevelContext = nullptr; } mPriv->releaseAllProtected(); diff --git a/src/jsc/JavaScriptEngine.cpp b/src/jsc/JavaScriptEngine.cpp index 84190ee..84809fa 100644 --- a/src/jsc/JavaScriptEngine.cpp +++ b/src/jsc/JavaScriptEngine.cpp @@ -17,9 +17,6 @@ * limitations under the License. **/ -#ifdef REMOTE_INSPECTOR_ENABLED -#include -#endif #include #include @@ -30,8 +27,11 @@ #define USE(WTF_FEATURE) (defined USE_##WTF_FEATURE && USE_##WTF_FEATURE) #define ENABLE(WTF_FEATURE) (defined ENABLE_##WTF_FEATURE && ENABLE_##WTF_FEATURE) + #include -#include +#ifdef REMOTE_INSPECTOR_ENABLE +#include +#endif #include #include "JavaScriptEngine.h" @@ -52,19 +52,9 @@ extern std::thread::id gMainThreadId; #define NATIVEJS_GC_DEFAULT_INTERVAL 60000 -#ifdef REMOTE_INSPECTOR_ENABLED -#ifdef __cplusplus -extern "C" { -#endif - JS_EXPORT void JSRemoteInspectorStart(void); - JS_EXPORT void JSRemoteInspectorSetLogToSystemConsole(bool logToSystemConsole); - JS_EXPORT void JSRemoteInspectorSetInspectionEnabledByDefault(bool); -#ifdef __cplusplus -} -#endif + extern std::vector gWebSocketServers; -#endif namespace WTF { void initializeMainThread(); }; @@ -92,42 +82,14 @@ bool JavaScriptEngine::initialize() gst_init(0, nullptr); } #endif + setenv("JSC_useBigInt", "1", 1); + WTF::initializeMainThread(); + if (!gMainLoop && g_main_depth() == 0) { gMainLoop = g_main_loop_new (nullptr, false); } -#ifdef REMOTE_INSPECTOR_ENABLED - char* inspectorDetails = getenv("NATIVEJS_INSPECTOR_SERVER"); - if (inspectorDetails) - { - std::string host("0.0.0.0"); - uint32_t port = 9226; - bool isFailedParsing = false; - std::string details(inspectorDetails); - int portIndex = details.find(":"); - if (portIndex != -1) - { - port = atoi(details.substr(portIndex+1).c_str()); - host = details.substr(0, portIndex); - } - else - { - isFailedParsing = true; - NativeJSLogger::log(ERROR, "failed to start remote inspector server due to parsing issues\n"); - } - if (!isFailedParsing) - { - std::stringstream serverDetails; - serverDetails << host.c_str() << ":" << port; - setenv("WEBKIT_INSPECTOR_SERVER", serverDetails.str().c_str(),1); - Inspector::RemoteInspectorServer::singleton().start(host.c_str(), port); - JSRemoteInspectorStart(); - JSRemoteInspectorSetInspectionEnabledByDefault(true); - JSRemoteInspectorSetLogToSystemConsole(true); - } - } -#endif gMainThreadId = std::this_thread::get_id(); JavaScriptEngine* engine = this; char* garbageCollectInterval = getenv("NATIVEJS_GC_INTERVAL"); @@ -145,7 +107,31 @@ bool JavaScriptEngine::initialize() fflush(stdout); return 0; }); - JSRemoteInspectorSetLogToSystemConsole(true); + +#ifdef REMOTE_INSPECTOR_ENABLE + // Start the custom web inspector server with HTTP/WebSocket endpoints + const char* inspectorServer = getenv("NATIVEJS_INSPECTOR_SERVER"); + if (inspectorServer) + { + NativeJSLogger::log(INFO, "Starting Web Inspector Server on: %s\n", inspectorServer); + + // Parse the address (expecting "0.0.0.0:9226" format) + char* colonPos = strchr(const_cast(inspectorServer), ':'); + int port = 9226; // default + if (colonPos) { + port = atoi(colonPos + 1); + } + + // Start the server (context will be registered later when created) + if (InspectorHTTPServer::singleton().start(inspectorServer, port)) { + mInspectorEnabled = true; + NativeJSLogger::log(INFO, "Web Inspector Server started successfully\n"); + } else { + NativeJSLogger::log(ERROR, "Failed to start Web Inspector Server\n"); + } + } +#endif + return true; } @@ -157,6 +143,14 @@ bool JavaScriptEngine::terminate() gst_deinit(); } #endif + +#ifdef REMOTE_INSPECTOR_ENABLE + if (mInspectorEnabled) { + InspectorHTTPServer::singleton().stop(); + NativeJSLogger::log(INFO, "Web Inspector Server stopped\n"); + } +#endif + clearTimeout(mGarbageCollectionTag); return true; } @@ -179,13 +173,14 @@ void JavaScriptEngine::run() ret = g_main_context_iteration(nullptr, false); } while(ret); } + dispatchPending(); #ifdef WS_SERVER_ENABLED for (int i=0; ipoll(); - } + } #endif isProcessing = false; } @@ -198,3 +193,4 @@ void JavaScriptEngine::collectGarbage() JSGarbageCollect(gTopLevelContext); } } + diff --git a/src/jsc/JavaScriptUtils.cpp b/src/jsc/JavaScriptUtils.cpp index cef1dec..7fe4aef 100644 --- a/src/jsc/JavaScriptUtils.cpp +++ b/src/jsc/JavaScriptUtils.cpp @@ -57,7 +57,9 @@ #endif #include "rtHttpRequest.h" #include "rtHttpResponse.h" - +#ifdef REMOTE_INSPECTOR_ENABLE +#include "InspectorHTTPServer.h" +#endif #include #include @@ -868,7 +870,16 @@ static JSValueRef consoleCallbackImpl(JSContextRef ctx, size_t argumentCount, JSStringRelease(jsStr); } } - +#ifdef REMOTE_INSPECTOR_ENABLE + // Forward to inspector + const char* level = "log"; + if (strstr(methodName, ".warn")) level = "warn"; + else if (strstr(methodName, ".error")) level = "error"; + else if (strstr(methodName, ".debug")) level = "debug"; + else if (strstr(methodName, ".info")) level = "info"; + + InspectorHTTPServer::singleton().sendConsoleMessage(ctx, level, oss.str().c_str()); +#endif NativeJSLogger::log(INFO, "%s", oss.str().c_str()); return JSValueMakeUndefined(ctx); } diff --git a/src/jsc/include.cmake b/src/jsc/include.cmake index 93c0ddf..5efa9f5 100644 --- a/src/jsc/include.cmake +++ b/src/jsc/include.cmake @@ -51,6 +51,16 @@ if ( ENABLE_WEBSOCKET_SERVER ) endif (ENABLE_WEBSOCKET_SERVER ) add_definitions("-DRT_PLATFORM_LINUX -DUSE_UV -DNDEBUG -DRELEASE_WITHOUT_OPTIMIZATIONS -fno-rtti -DSTATICALLY_LINKED_WITH_WTF -DUSE_GLIB") +if ( REMOTE_INSPECTOR_ENABLE ) + set (JSRUNTIME_ENGINE_FILES ${JSRUNTIME_ENGINE_FILES} + ${JSRUNTIME_COMMON_SOURCE_DIRECTORY}/InspectorHTTPServer.cpp) + add_definitions("-DREMOTE_INSPECTOR_ENABLE") +endif (REMOTE_INSPECTOR_ENABLE ) + +if (REMOTE_INSPECTOR_ENABLE) + set(JSRUNTIME_ENGINE_LIBRARIES ${JSRUNTIME_ENGINE_LIBRARIES} -lsoup-3.0) +endif (REMOTE_INSPECTOR_ENABLE) + if (NOT BUILD_JSRUNTIME_DESKTOP) set(JAVASCRIPT_CORE_LIBRARIES -lJavaScriptCore -lpthread -ldl -lgio-2.0 -licui18n) else () diff --git a/utils/nativejsinspector.html b/utils/nativejsinspector.html new file mode 100644 index 0000000..7392cac --- /dev/null +++ b/utils/nativejsinspector.html @@ -0,0 +1,574 @@ + + + + + RDK NativeScript Web Inspector + + + +
+

RDK NativeScript Inspector

+
+ +
Connecting...
+
+
+ +
+
Console
+
Sources
+
+ +
+ +
+
+ + + + + + +
+
+
+ + +
+
+ + +
+
+ +
+
+
File System
+
rdkNativeScript +
+
+
+
+
+
+
+ No file selected +
+
+
+

No script selected

+

Scripts will appear automatically as they load.

+
+
+
+
+ +
+ + + + +