Add basics of Chrome Devtools debugger support

- Use routing table for request dispatch in DebuggerHttpRouter, for
  handling choosing which debugger stack to use based on the http
  upgrade request:
    - DebuggerEscargot for the python debugger and VSCode
    - DebuggerDevtools for connecting to Chrome Devtools
- Parse mesasges with 16bit message size
- Reply to the first few messages chrome sends
- Refactor Debugger:
    - Rename DebuggerRemote to DebuggerEscargot
    - DebuggerEscargot and DebuggerDevtools inherit from
      DebuggerTcp which inherits from Debugger
- Add debugger info to README.md

Signed-off-by: Máté Tokodi <mate.tokodi@szteszoftver.hu>
This commit is contained in:
Máté Tokodi 2026-03-20 18:03:32 +01:00 committed by Patrick Kim
commit 97e8115ab1
21 changed files with 2767 additions and 1613 deletions

View file

@ -60,6 +60,7 @@ The following build options are supported when generating build rules using cmak
| **SHADOWREALM** | Enable ShadowRealm support | -ESCARGOT_SHADOWREALM | ON/OFF | OFF |
| **SMALL_CONFIG** | Enable aggressive memory optimizations for tiny devices | -DESCARGOT_SMALL_CONFIG | ON/OFF | OFF |
| **TEST** | Enable additional features used only for testing | -DESCARGOT_TEST | ON/OFF | OFF |
| **DEBUGGER** | Enable Debug server | -DESCARGOT_DEBUGGER | ON/OFF | OFF |
### Linux
@ -135,6 +136,27 @@ cd out
msbuild ESCARGOT.sln /property:Configuration=Release /p:platform=[ Win32 | x64 ]
```
## Debugger
Make sure Escargot is built with the `-DESCARGOT_DEBUGGER=1` flag (off by default) enabled;
then start Escargot with the `--start-debug-server` option.
### Connect using a debugger client
- Escargot python debugger
- run `./tools/debugger/debugger.py`; It will automatically connect to a debug server on the default port `6501`
- run `./tools/debugger/debugger.py --help` for a list of options
- [Visual Studio Code extension](https://github.com/Samsung/escargot-vscode-extension/?tab=readme-ov-file#how-to-use)
- Chrome Devtools `⚠️ Early in development ⚠️`
- Initial setup:
- Navigate to [chrome://inspect](chrome://inspect)
- Make sure *Discover network targets* is enabled; click configure
- Add `localhost:6501` as a target; click Done
- Usage:
- The started debug server will be listed in the *Remote Target* list (If it is not, the page may need to be reloaded using the browser reload button)
- Click `inspect`
- A new window with the Chrome Devtools debugger UI will open
## Testing ✅
Escargot supports various benchmark sets, which can be run using the [tools/run-tests.py](https://github.com/Samsung/escargot/blob/master/tools/run-tests.py) script.

View file

@ -1984,6 +1984,7 @@ bool DebuggerOperationsRef::updateBreakpoint(WeakCodeRef* weakCodeRef, uint32_t
class DebuggerC : public Debugger {
public:
virtual void init(const char* options, Context* context) override {}
virtual void parseCompleted(String* source, String* srcName, size_t originLineOffset, String* error = nullptr) override;
virtual void stopAtBreakpoint(ByteCodeBlock* byteCodeBlock, uint32_t offset, ExecutionState* state) override;
virtual void byteCodeReleaseNotification(ByteCodeBlock* byteCodeBlock) override;
@ -3385,7 +3386,7 @@ void ContextRef::throwException(ValueRef* exceptionValue)
toImpl(this)->throwException(s, toImpl(exceptionValue));
}
bool ContextRef::initDebugger(DebuggerOperationsRef::DebuggerClient* debuggerClient)
bool ContextRef::initDebuggerClient(DebuggerOperationsRef::DebuggerClient* debuggerClient)
{
#ifdef ESCARGOT_DEBUGGER
Context* context = toImpl(this);
@ -3414,10 +3415,10 @@ bool ContextRef::disableDebugger()
#endif
}
bool ContextRef::initDebuggerRemote(const char* options)
bool ContextRef::initDebugger(const char* options)
{
#ifdef ESCARGOT_DEBUGGER
return toImpl(this)->initDebuggerRemote(options);
return toImpl(this)->initDebugger(options);
#else /* !ESCARGOT_DEBUGGER */
return false;
#endif /* ESCARGOT_DEBUGGER */

View file

@ -950,11 +950,11 @@ public:
bool canThrowException();
void throwException(ValueRef* exceptionValue);
bool initDebugger(DebuggerOperationsRef::DebuggerClient* debuggerClient);
bool initDebuggerClient(DebuggerOperationsRef::DebuggerClient* debuggerClient);
bool disableDebugger();
// available options(separator is ';')
// "--port=6501", default for TCP debugger
bool initDebuggerRemote(const char* options);
bool initDebugger(const char* options);
bool isDebuggerRunning();
bool isWaitBeforeExit();
void printDebugger(StringRef* output);

File diff suppressed because it is too large Load diff

View file

@ -28,14 +28,11 @@ namespace Escargot {
#define ESCARGOT_DEBUGGER_MAX_STACK_TRACE_LENGTH 8
/* WebSocket max length encoded in one byte. */
#define ESCARGOT_DEBUGGER_MAX_MESSAGE_LENGTH 125
#define ESCARGOT_DEBUGGER_VERSION 1
#define ESCARGOT_DEBUGGER_MESSAGE_PROCESS_DELAY 10
#define ESCARGOT_DEBUGGER_IN_WAIT_MODE (nullptr)
#define ESCARGOT_DEBUGGER_IN_EVAL_MODE (reinterpret_cast<ExecutionState*>(0x1))
#define ESCARGOT_DEBUGGER_ALWAYS_STOP (reinterpret_cast<ExecutionState*>(0x2))
#define ESCARGOT_DEBUGGER_NO_STACK_TRACE_RESTORE (reinterpret_cast<ExecutionState*>(0x1))
#define ESCARGOT_DEBUGGER_MAX_VARIABLE_LENGTH 128
class Context;
class Object;
@ -158,8 +155,7 @@ public:
m_stopState = stopState;
}
static void createDebuggerRemote(const char* options, Context* context);
virtual void init(const char* options, Context* context) = 0;
virtual void parseCompleted(String* source, String* srcName, size_t originLineOffset, String* error = nullptr) = 0;
virtual void stopAtBreakpoint(ByteCodeBlock* byteCodeBlock, uint32_t offset, ExecutionState* state) = 0;
virtual void byteCodeReleaseNotification(ByteCodeBlock* byteCodeBlock) = 0;
@ -176,6 +172,11 @@ public:
static SavedStackTraceDataVector* saveStackTrace(ExecutionState& state);
void pumpDebuggerEvents(ExecutionState* state);
static void createDebugger(const char* options, Context* context);
void enable(Context* context);
Vector<Object*, GCUtil::gc_malloc_allocator<Object*>> m_activeObjects;
protected:
Debugger()
@ -193,7 +194,6 @@ protected:
return m_context != nullptr;
}
void enable(Context* context);
void disable();
virtual bool processEvents(ExecutionState* state, Optional<ByteCodeBlock*> byteCodeBlock, bool isBlockingRequest = true) = 0;
@ -201,6 +201,7 @@ protected:
uint32_t m_delay;
ExecutionState* m_stopState;
std::vector<BreakpointLocationsInfo*> m_breakpointLocationsVector;
Vector<uintptr_t, GCUtil::gc_malloc_atomic_allocator<uintptr_t>> m_releasedFunctions;
private:
Context* m_context;
@ -212,246 +213,6 @@ private:
bool m_inDebuggingCodeMode;
};
class DebuggerRemote : public Debugger {
public:
// Messages sent by Escargot to the debugger client
enum {
ESCARGOT_MESSAGE_VERSION = 0,
ESCARGOT_MESSAGE_CONFIGURATION = 1,
ESCARGOT_MESSAGE_CLOSE_CONNECTION = 2,
ESCARGOT_MESSAGE_RELEASE_FUNCTION = 3,
ESCARGOT_MESSAGE_PARSE_DONE = 4,
ESCARGOT_MESSAGE_PARSE_ERROR = 5,
// These four must be in the same order.
ESCARGOT_MESSAGE_SOURCE_8BIT = 6,
ESCARGOT_MESSAGE_SOURCE_8BIT_END = 7,
ESCARGOT_MESSAGE_SOURCE_16BIT = 8,
ESCARGOT_MESSAGE_SOURCE_16BIT_END = 9,
// These four must be in the same order.
ESCARGOT_MESSAGE_FILE_NAME_8BIT = 10,
ESCARGOT_MESSAGE_FILE_NAME_8BIT_END = 11,
ESCARGOT_MESSAGE_FILE_NAME_16BIT = 12,
ESCARGOT_MESSAGE_FILE_NAME_16BIT_END = 13,
// These four must be in the same order.
ESCARGOT_MESSAGE_FUNCTION_NAME_8BIT = 14,
ESCARGOT_MESSAGE_FUNCTION_NAME_8BIT_END = 15,
ESCARGOT_MESSAGE_FUNCTION_NAME_16BIT = 16,
ESCARGOT_MESSAGE_FUNCTION_NAME_16BIT_END = 17,
ESCARGOT_MESSAGE_BREAKPOINT_LOCATION = 18,
ESCARGOT_MESSAGE_FUNCTION_PTR = 19,
ESCARGOT_MESSAGE_BREAKPOINT_HIT = 20,
ESCARGOT_MESSAGE_EXCEPTION_HIT = 21,
// These four must be in the same order.
ESCARGOT_MESSAGE_EVAL_RESULT_8BIT = 22,
ESCARGOT_MESSAGE_EVAL_RESULT_8BIT_END = 23,
ESCARGOT_MESSAGE_EVAL_RESULT_16BIT = 24,
ESCARGOT_MESSAGE_EVAL_RESULT_16BIT_END = 25,
// These four must be in the same order.
ESCARGOT_MESSAGE_EVAL_FAILED_8BIT = 26,
ESCARGOT_MESSAGE_EVAL_FAILED_8BIT_END = 27,
ESCARGOT_MESSAGE_EVAL_FAILED_16BIT = 28,
ESCARGOT_MESSAGE_EVAL_FAILED_16BIT_END = 29,
// These four must be in the same order.
ESCARGOT_MESSAGE_WATCH_RESULT_8BIT = 30,
ESCARGOT_MESSAGE_WATCH_RESULT_8BIT_END = 31,
ESCARGOT_MESSAGE_WATCH_RESULT_16BIT = 32,
ESCARGOT_MESSAGE_WATCH_RESULT_16BIT_END = 33,
ESCARGOT_MESSAGE_BACKTRACE_TOTAL = 34,
ESCARGOT_MESSAGE_BACKTRACE = 35,
ESCARGOT_MESSAGE_BACKTRACE_END = 36,
ESCARGOT_MESSAGE_SCOPE_CHAIN = 37,
ESCARGOT_MESSAGE_SCOPE_CHAIN_END = 38,
// These four must be in the same order.
ESCARGOT_MESSAGE_STRING_8BIT = 39,
ESCARGOT_MESSAGE_STRING_8BIT_END = 40,
ESCARGOT_MESSAGE_STRING_16BIT = 41,
ESCARGOT_MESSAGE_STRING_16BIT_END = 42,
ESCARGOT_MESSAGE_VARIABLE = 43,
ESCARGOT_MESSAGE_PRINT = 44,
ESCARGOT_MESSAGE_EXCEPTION = 45,
ESCARGOT_MESSAGE_EXCEPTION_BACKTRACE = 46,
ESCARGOT_DEBUGGER_WAIT_FOR_SOURCE = 47,
ESCARGOT_DEBUGGER_WAITING_AFTER_PENDING = 48,
ESCARGOT_DEBUGGER_WAIT_FOR_WAIT_EXIT = 49
};
// Messages sent by the debugger client to Escargot
enum {
ESCARGOT_MESSAGE_FUNCTION_RELEASED = 0,
ESCARGOT_MESSAGE_UPDATE_BREAKPOINT = 1,
ESCARGOT_MESSAGE_CONTINUE = 2,
ESCARGOT_MESSAGE_STEP = 3,
ESCARGOT_MESSAGE_NEXT = 4,
ESCARGOT_MESSAGE_FINISH = 5,
// These four must be in the same order.
ESCARGOT_MESSAGE_EVAL_8BIT_START = 6,
ESCARGOT_MESSAGE_EVAL_8BIT = 7,
ESCARGOT_MESSAGE_EVAL_16BIT_START = 8,
ESCARGOT_MESSAGE_EVAL_16BIT = 9,
// These four must be in the same order.
ESCARGOT_MESSAGE_EVAL_WITHOUT_STOP_8BIT_START = 10,
ESCARGOT_MESSAGE_EVAL_WITHOUT_STOP_8BIT = 11,
ESCARGOT_MESSAGE_EVAL_WITHOUT_STOP_16BIT_START = 12,
ESCARGOT_MESSAGE_EVAL_WITHOUT_STOP_16BIT = 13,
// These four must be in the same order.
ESCARGOT_MESSAGE_WATCH_8BIT_START = 14,
ESCARGOT_MESSAGE_WATCH_8BIT = 15,
ESCARGOT_MESSAGE_WATCH_16BIT_START = 16,
ESCARGOT_MESSAGE_WATCH_16BIT = 17,
ESCARGOT_MESSAGE_GET_BACKTRACE = 18,
ESCARGOT_MESSAGE_GET_SCOPE_CHAIN = 19,
ESCARGOT_MESSAGE_GET_SCOPE_VARIABLES = 20,
ESCARGOT_MESSAGE_GET_OBJECT = 21,
// These four must be in the same order.
ESCARGOT_DEBUGGER_CLIENT_SOURCE_8BIT_START = 22,
ESCARGOT_DEBUGGER_CLIENT_SOURCE_8BIT = 23,
ESCARGOT_DEBUGGER_CLIENT_SOURCE_16BIT_START = 24,
ESCARGOT_DEBUGGER_CLIENT_SOURCE_16BIT = 25,
ESCARGOT_DEBUGGER_THERE_WAS_NO_SOURCE = 26,
ESCARGOT_DEBUGGER_PENDING_CONFIG = 27,
ESCARGOT_DEBUGGER_PENDING_RESUME = 28,
ESCARGOT_DEBUGGER_WAIT_BEFORE_EXIT = 29,
ESCARGOT_DEBUGGER_STOP = 30
};
// Environment record types
enum {
ESCARGOT_RECORD_GLOBAL_ENVIRONMENT = 0,
ESCARGOT_RECORD_FUNCTION_ENVIRONMENT = 1,
ESCARGOT_RECORD_DECLARATIVE_ENVIRONMENT = 2,
ESCARGOT_RECORD_OBJECT_ENVIRONMENT = 3,
ESCARGOT_RECORD_MODULE_ENVIRONMENT = 4,
ESCARGOT_RECORD_UNKNOWN_ENVIRONMENT = 5,
};
// Variable types
enum {
ESCARGOT_VARIABLE_END = 0,
ESCARGOT_VARIABLE_UNACCESSIBLE = 1,
ESCARGOT_VARIABLE_UNDEFINED = 2,
ESCARGOT_VARIABLE_NULL = 3,
ESCARGOT_VARIABLE_TRUE = 4,
ESCARGOT_VARIABLE_FALSE = 5,
ESCARGOT_VARIABLE_NUMBER = 6,
ESCARGOT_VARIABLE_STRING = 7,
ESCARGOT_VARIABLE_SYMBOL = 8,
ESCARGOT_VARIABLE_BIGINT = 9,
// Only object types should be defined after this point.
ESCARGOT_VARIABLE_OBJECT = 10,
ESCARGOT_VARIABLE_ARRAY = 11,
ESCARGOT_VARIABLE_FUNCTION = 12,
ESCARGOT_VARIABLE_TYPE_MASK = 0x3f,
ESCARGOT_VARIABLE_LONG_NAME = 0x40,
ESCARGOT_VARIABLE_LONG_VALUE = 0x80,
};
inline bool pendingWait(void)
{
return m_pendingWait;
}
inline bool connected(void)
{
return enabled();
}
void sendType(uint8_t type);
void sendSubtype(uint8_t type, uint8_t subType);
void sendString(uint8_t type, const String* string);
void sendPointer(uint8_t type, const void* ptr);
virtual void init(const char* options, Context* context) = 0;
virtual void parseCompleted(String* source, String* srcName, size_t originLineOffset, String* error = nullptr) override;
virtual void stopAtBreakpoint(ByteCodeBlock* byteCodeBlock, uint32_t offset, ExecutionState* state) override;
virtual void byteCodeReleaseNotification(ByteCodeBlock* byteCodeBlock) override;
virtual void exceptionCaught(String* message, SavedStackTraceDataVector& exceptionTrace) override;
virtual void consoleOut(String* output) override;
virtual String* getClientSource(String** sourceName) override;
virtual bool getWaitBeforeExitClient() override;
void sendBacktraceInfo(uint8_t type, ByteCodeBlock* byteCodeBlock, uint32_t line, uint32_t column, uint32_t executionStateDepth);
void sendVariableObjectInfo(uint8_t subType, Object* object);
void waitForResolvingPendingBreakpoints();
protected:
enum CloseReason {
CloseEndConnection,
CloseAbortConnection,
CloseProtocolUnsupported,
CloseProtocolError,
};
DebuggerRemote()
: m_exitClient(false)
, m_pendingWait(false)
, m_waitForResume(false)
, m_watchEval(false)
, m_clientSourceData(nullptr)
, m_clientSourceName(nullptr)
{
}
virtual bool processEvents(ExecutionState* state, Optional<ByteCodeBlock*> byteCodeBlock, bool isBlockingRequest = true) override;
virtual bool send(uint8_t type, const void* buffer, size_t length) = 0;
virtual bool receive(uint8_t* buffer, size_t& length) = 0;
virtual bool isThereAnyEvent() = 0;
virtual void close(CloseReason reason) = 0;
private:
// Packed structure definitions to reduce network traffic
struct MessageVersion {
uint8_t littleEndian;
uint8_t version[sizeof(uint32_t)];
};
struct MessageConfiguration {
uint8_t maxMessageSize;
uint8_t pointerSize;
};
struct FunctionInfo {
uint8_t byteCodeStart[sizeof(void*)];
uint8_t startLine[sizeof(uint32_t)];
uint8_t startColumn[sizeof(uint32_t)];
};
struct BreakpointOffset {
uint8_t byteCodeStart[sizeof(void*)];
uint8_t offset[sizeof(uint32_t)];
};
struct BacktraceInfo {
uint8_t byteCode[sizeof(void*)];
uint8_t line[sizeof(uint32_t)];
uint8_t column[sizeof(uint32_t)];
uint8_t executionStateDepth[sizeof(uint32_t)];
};
struct VariableObjectInfo {
uint8_t subType;
uint8_t index[sizeof(uint32_t)];
};
uint32_t appendToActiveObjects(Object* object);
bool doEval(ExecutionState* state, Optional<ByteCodeBlock*> byteCodeBlock, uint8_t* buffer, size_t length);
void getBacktrace(ExecutionState* state, uint32_t minDepth, uint32_t maxDepth, bool getTotal);
void getScopeChain(ExecutionState* state, uint32_t stateIndex);
void getScopeVariables(ExecutionState* state, uint32_t stateIndex, uint32_t index);
bool m_exitClient : 1;
bool m_pendingWait : 1;
bool m_waitForResume : 1;
bool m_watchEval : 1;
String* m_clientSourceData;
String* m_clientSourceName;
Vector<uintptr_t, GCUtil::gc_malloc_atomic_allocator<uintptr_t>> m_releasedFunctions;
Vector<Object*, GCUtil::gc_malloc_allocator<Object*>> m_activeObjects;
};
} // namespace Escargot
#endif /* ESCARGOT_DEBUGGER */

View file

@ -0,0 +1,410 @@
/*
* Copyright (c) 2026-present Samsung Electronics Co., Ltd
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
* USA
*/
#include "Escargot.h"
#include "DebuggerTcp.h"
#include "DebuggerDevtools.h"
#include "DebuggerHttpRouter.h"
#include "DebuggerDevtoolsMessageBuilder.h"
#include "interpreter/ByteCode.h"
#include "rapidjson/document.h"
#include "rapidjson/prettywriter.h"
#include "rapidjson/stringbuffer.h"
#include "rapidjson/error/en.h"
#ifdef ESCARGOT_DEBUGGER
namespace Escargot {
bool DebuggerDevtools::sendMessage(const std::string& msg, const int length)
{
if (UNLIKELY(!m_networkEnabled || !m_debuggerEnabled || !m_runtimeEnabled)) {
m_pendingMessages.emplace_back(msg);
return true;
}
ESCARGOT_LOG_INFO("Sending message: %s\n", msg.c_str());
return send(0, msg.c_str(), length == -1 ? msg.length() : length);
}
void DebuggerDevtools::init(const char* options, Context* context)
{
// ESCARGOT_LOG_INFO("Implement this: DebuggerDevtools::init\n");
const char* msg = "{\"method\":\"Runtime.executionContextCreated\",\"params\":{"
"\"context\":{"
"\"id\":1,"
"\"origin\":\"\","
"\"name\":\"escargot\","
"\"auxData\":{\"isDefault\":true}"
"}"
"}}";
sendMessage(msg);
}
bool DebuggerDevtools::skipSourceCode(String* srcName) const
{
ESCARGOT_LOG_INFO("Implement this: DebuggerDevtools::skipSourceCode\n");
return false;
}
uint8_t DebuggerDevtools::registerScript(String* source, String* srcName)
{
std::string url(reinterpret_cast<const char*>(srcName->characters8()), srcName->length());
auto it = m_scriptIdByUrl.find(url);
if (it != m_scriptIdByUrl.end()) {
return it->second;
}
uint8_t newId = m_nextScriptId++;
m_scriptsById.emplace(newId, ScriptInfo{ newId, srcName, source });
m_scriptIdByUrl.emplace(url, newId);
return newId;
}
void DebuggerDevtools::parseCompleted(String* source, String* srcName, size_t originLineOffset, String* error)
{
if (!enabled()) {
return;
}
if (!source || !srcName || !source->is8Bit() || !srcName->is8Bit()) {
ESCARGOT_LOG_ERROR("Only 8 bit characters are supported right now...");
return;
}
uint8_t scriptId = registerScript(source, srcName);
sendMessage(DebuggerDevtoolsMessageBuilder::buildScriptParsedMessage(scriptId, source, srcName));
}
void DebuggerDevtools::sendPausedEvent(ByteCodeBlock* byteCodeBlock, uint32_t offset, ExecutionState* state)
{
// TODO: Placeholder info
const char* msg = "{\"method\":\"Debugger.paused\","
"\"params\":{"
"\"callFrames\":[{"
"\"callFrameId\":\"frame:0\","
"\"functionName\":\"\","
"\"location\":{"
"\"scriptId\":\"1\","
"\"lineNumber\":0,"
"\"columnNumber\":0"
"},"
"\"url\":\"hello.js\","
"\"scopeChain\":[{"
"\"type\":\"global\","
"\"object\":{"
"\"type\":\"object\","
"\"className\":\"global\","
"\"description\":\"global\","
"\"objectId\":\"global:1\""
"}"
"}],"
"\"this\":{"
"\"type\":\"undefined\""
"}"
"}],"
"\"reason\":\"breakpoint\","
"\"hitBreakpoints\":[\"breakpoint:1\"]"
"}"
"}";
sendMessage(msg);
}
void DebuggerDevtools::stopAtBreakpoint(ByteCodeBlock* byteCodeBlock, uint32_t offset, ExecutionState* state)
{
if (m_stopState == ESCARGOT_DEBUGGER_IN_EVAL_MODE) {
m_delay--;
if (m_delay == 0) {
processEvents(state, byteCodeBlock);
}
return;
}
uint8_t* byteCodeStart = byteCodeBlock->m_code.data();
sendPausedEvent(byteCodeBlock, offset, state);
if (!enabled()) {
return;
}
ASSERT(m_activeObjects.empty());
m_stopState = ESCARGOT_DEBUGGER_IN_WAIT_MODE;
while (processEvents(state, byteCodeBlock))
;
m_activeObjects.clear();
m_delay = ESCARGOT_DEBUGGER_MESSAGE_PROCESS_DELAY;
}
void DebuggerDevtools::byteCodeReleaseNotification(ByteCodeBlock* byteCodeBlock)
{
}
void DebuggerDevtools::exceptionCaught(String* message, SavedStackTraceDataVector& exceptionTrace)
{
}
void DebuggerDevtools::consoleOut(String* output)
{
}
String* DebuggerDevtools::getClientSource(String** sourceName)
{
return nullptr;
}
bool DebuggerDevtools::getWaitBeforeExitClient()
{
while (processEvents(nullptr, nullptr))
;
return true;
}
template <size_t N>
constexpr MessageType messageType(const char (&methodName)[N], const MessageHandler handler)
{
return MessageType{ methodName, handler };
}
bool DebuggerDevtools::resume(rapidjson::Document& jsonMessage)
{
replyOK(jsonMessage);
if (m_stopState != ESCARGOT_DEBUGGER_IN_WAIT_MODE) {
return true;
}
m_stopState = nullptr;
return false;
}
bool DebuggerDevtools::sendProperties(rapidjson::Document& jsonMessage)
{
uint32_t requestId = jsonMessage["id"].GetUint();
char buffer[4096];
// TODO: placeholder info
snprintf(buffer, sizeof(buffer),
"{\"id\":%u,\"result\":{"
"\"result\":["
"{"
"\"name\":\"print\","
"\"configurable\":true,"
"\"enumerable\":true,"
"\"value\":{"
"\"type\":\"function\","
"\"description\":\"function print()\""
"}"
"},"
"{"
"\"name\":\"c\","
"\"configurable\":true,"
"\"enumerable\":true,"
"\"value\":{"
"\"type\":\"number\","
"\"value\":8,"
"\"description\":\"8\""
"}"
"},"
"{"
"\"name\":\"globalVar\","
"\"configurable\":true,"
"\"enumerable\":true,"
"\"value\":{"
"\"type\":\"string\","
"\"value\":\"escargot\","
"\"description\":\"escargot\""
"}"
"}"
"]"
"}}",
requestId);
sendMessage(buffer);
return true;
}
bool DebuggerDevtools::sendSourceCode(rapidjson::Document& jsonMessage)
{
const uint32_t requestId = jsonMessage["id"].GetUint();
const uint32_t scriptId = std::stoi(jsonMessage["params"]["scriptId"].GetString());
auto it = m_scriptsById.find(scriptId);
if (it == m_scriptsById.end()) {
return false;
}
String* source = it->second.source;
if (!source->is8Bit()) {
ESCARGOT_LOG_ERROR("Only 8 bit characters are supported right now...");
return false;
}
std::string message = DebuggerDevtoolsMessageBuilder::buildSourceCodeMessage(requestId, source);
return sendMessage(message);
}
bool DebuggerDevtools::replyOK(rapidjson::Document& jsonMessage)
{
char reply[32];
const int jsonReplyStringLength = snprintf(reply, sizeof(reply),
R"({"id": %d, "result": {}})",
jsonMessage["id"].GetInt());
sendMessage(reply, jsonReplyStringLength);
return true;
}
bool DebuggerDevtools::enableNetwork(rapidjson::Document& jsonMessage)
{
this->m_networkEnabled = true;
return replyOK(jsonMessage);
}
bool DebuggerDevtools::enableDebugger(rapidjson::Document& jsonMessage)
{
this->m_debuggerEnabled = true;
return replyOK(jsonMessage);
}
bool DebuggerDevtools::enableRuntime(rapidjson::Document& jsonMessage)
{
this->m_runtimeEnabled = true;
return replyOK(jsonMessage);
}
bool DebuggerDevtools::enableProfiler(rapidjson::Document& jsonMessage)
{
this->m_profilerEnabled = true;
return replyOK(jsonMessage);
}
bool DebuggerDevtools::setPauseOnExceptions(rapidjson::Document& jsonMessage)
{
this->m_pauseOnExceptions = true;
return replyOK(jsonMessage);
}
bool DebuggerDevtools::replyMethodNotFound(rapidjson::Document& jsonMessage)
{
char reply[256];
const int jsonReplyStringLength = snprintf(reply, sizeof(reply),
R"({"id":%d,"error":{"code":-32601,"message":"'%s' wasn't found"}})",
jsonMessage["id"].GetInt(),
jsonMessage["method"].GetString());
sendMessage(reply, jsonReplyStringLength);
return true;
}
bool DebuggerDevtools::processEvents(ExecutionState* state, Optional<ByteCodeBlock*> byteCodeBlock, bool isBlockingRequest)
{
uint8_t buffer[ESCARGOT_WS_MAX_MESSAGE_LENGTH];
size_t length;
// NOTE: keep sorted
static constexpr MessageType messageTypes[] = {
messageType("Debugger.enable", &DebuggerDevtools::enableDebugger),
messageType("Debugger.getScriptSource", &DebuggerDevtools::sendSourceCode),
messageType("Debugger.resume", &DebuggerDevtools::resume),
messageType("Debugger.setAsyncCallStackDepth", &DebuggerDevtools::replyOK), // we may be able to set something for this one
messageType("Debugger.setBlackboxPatterns", &DebuggerDevtools::replyOK), // we ignore this for now, but if needed set skipSourceName in DebuggerTcp
messageType("Debugger.setPauseOnExceptions", &DebuggerDevtools::setPauseOnExceptions),
messageType("Network.clearAcceptedEncodingsOverride", &DebuggerDevtools::replyMethodNotFound),
messageType("Network.emulateNetworkConditionsByRule", &DebuggerDevtools::replyMethodNotFound),
messageType("Network.enable", &DebuggerDevtools::enableNetwork),
messageType("Network.overrideNetworkState", &DebuggerDevtools::replyMethodNotFound),
messageType("Network.setAttachDebugStack", &DebuggerDevtools::replyMethodNotFound),
messageType("Network.setBlockedURLs", &DebuggerDevtools::replyMethodNotFound),
messageType("Profiler.enable", &DebuggerDevtools::enableProfiler),
messageType("Runtime.enable", &DebuggerDevtools::enableRuntime),
messageType("Runtime.getProperties", &DebuggerDevtools::sendProperties),
messageType("Runtime.runIfWaitingForDebugger", &DebuggerDevtools::resume),
};
while (true) {
if (isBlockingRequest) {
if (!receive(buffer, length)) {
break;
}
} else {
if (isThereAnyEvent()) {
if (!receive(buffer, length)) {
break;
}
} else {
return false;
}
}
printf("MESSAGE: %.*s\n", static_cast<int>(length), reinterpret_cast<const char*>(buffer));
rapidjson::Document document;
document.Parse(reinterpret_cast<const rapidjson::GenericDocument<rapidjson::UTF8<>>::Ch*>(buffer));
rapidjson::Document jsonMessage;
if (UNLIKELY(jsonMessage.Parse(reinterpret_cast<const char*>(buffer)).HasParseError())) {
ESCARGOT_LOG_ERROR("Json Message parsing error: %s at offset: %d\n", GetParseError_En(jsonMessage.GetParseError()), static_cast<int32_t>(jsonMessage.GetErrorOffset()));
return false;
}
const char* methodName = jsonMessage["method"].GetString();
if (UNLIKELY(methodName == nullptr)) {
ESCARGOT_LOG_ERROR("Debugger method not provided: %s\n", reinterpret_cast<const char*>(buffer));
return false;
}
const uint32_t methodNameLength = strlen(methodName);
for (const auto& message : messageTypes) {
// TODO: it would be better to calculate the lengths of each of the method name strings at compile time instead of using strlen()
if (!DebuggerHttpRouter::requestStartsWith(reinterpret_cast<const uint8_t*>(methodName), methodNameLength, message.methodName, strlen(message.methodName))) {
continue;
}
return (this->*message.handler)(jsonMessage);
}
ESCARGOT_LOG_ERROR("Debugger function not supported: %s\n", reinterpret_cast<const char*>(buffer));
this->replyMethodNotFound(jsonMessage); // maybe reply with not supported instead? if there is such a reply in the spec
}
if (!m_pendingMessages.empty() && m_networkEnabled && m_debuggerEnabled && m_runtimeEnabled) {
for (const std::string& msg : m_pendingMessages) {
sendMessage(msg);
}
m_pendingMessages.clear();
}
return enabled();
}
} // namespace Escargot
#endif /* ESCARGOT_DEBUGGER */

View file

@ -0,0 +1,105 @@
/*
* Copyright (c) 2026-present Samsung Electronics Co., Ltd
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
* USA
*/
#ifndef __DebuggerDevtools__
#define __DebuggerDevtools__
#include "Debugger.h"
#include "DebuggerTcp.h"
#include "rapidjson/document.h"
#include "rapidjson/rapidjson.h"
#ifdef ESCARGOT_DEBUGGER
namespace Escargot {
#ifdef WIN32
#include <winsock2.h>
typedef SOCKET EscargotSocket;
#else /* !WIN32 */
typedef int EscargotSocket;
#endif /* WIN32 */
struct ScriptInfo {
uint8_t scriptId;
String* url;
String* source;
};
class DebuggerDevtools : public DebuggerTcp {
public:
DebuggerDevtools(EscargotSocket socket, String* skipSource)
: DebuggerTcp(socket, skipSource, ESCARGOT_WS_BUFFER_SIZE, ESCARGOT_DEBUGGER_WEBSOCKET_TEXT_FRAME)
{
}
bool sendMessage(const std::string& msg, const int length = -1);
void init(const char* options, Context* context) override;
bool skipSourceCode(String* srcName) const override;
void parseCompleted(String* source, String* srcName, size_t originLineOffset, String* error = nullptr) override;
void stopAtBreakpoint(ByteCodeBlock* byteCodeBlock, uint32_t offset, ExecutionState* state) override;
void byteCodeReleaseNotification(ByteCodeBlock* byteCodeBlock) override;
void exceptionCaught(String* message, SavedStackTraceDataVector& exceptionTrace) override;
void consoleOut(String* output) override;
String* getClientSource(String** sourceName) override;
bool getWaitBeforeExitClient() override;
void sendPausedEvent(ByteCodeBlock* byteCodeBlock, uint32_t offset, ExecutionState* state);
protected:
bool processEvents(ExecutionState* state, Optional<ByteCodeBlock*> byteCodeBlock, bool isBlockingRequest = true) override;
private:
bool sendProperties(rapidjson::Document& jsonMessage);
bool resume(rapidjson::Document& jsonMessage);
bool sendSourceCode(rapidjson::Document& jsonMessage);
bool replyOK(rapidjson::Document& jsonMessage);
bool enableNetwork(rapidjson::Document& jsonMessage);
bool enableDebugger(rapidjson::Document& jsonMessage);
bool enableRuntime(rapidjson::Document& jsonMessage);
bool enableProfiler(rapidjson::Document& jsonMessage);
bool setPauseOnExceptions(rapidjson::Document& jsonMessage);
bool replyMethodNotFound(rapidjson::Document& jsonMessage);
uint8_t registerScript(String* url, String* source);
bool m_networkEnabled = false;
bool m_debuggerEnabled = false;
bool m_runtimeEnabled = false;
bool m_profilerEnabled = false;
bool m_pauseOnExceptions = false;
std::unordered_map<uint8_t, ScriptInfo> m_scriptsById;
std::unordered_map<std::string, uint8_t> m_scriptIdByUrl;
uint8_t m_nextScriptId = 1;
std::vector<std::string> m_pendingMessages;
};
using MessageHandler = bool (DebuggerDevtools::*)(rapidjson::Document&);
struct MessageType {
const char* methodName;
MessageHandler handler;
};
} // namespace Escargot
#endif /* ESCARGOT_DEBUGGER */
#endif

View file

@ -0,0 +1,164 @@
/*
* Copyright (c) 2026-present Samsung Electronics Co., Ltd
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
* USA
*/
#include "DebuggerDevtoolsMessageBuilder.h"
#ifdef ESCARGOT_DEBUGGER
#include "Escargot.h"
#include "runtime/String.h" // for split function
#include "interpreter/ByteCode.h"
#include "rapidjson/document.h"
#include "rapidjson/prettywriter.h"
#include "rapidjson/stringbuffer.h"
namespace Escargot {
static std::string jsonEscape(const LChar* src, size_t len)
{
std::string out;
out.reserve(len + 16);
for (size_t i = 0; i < len; ++i) {
LChar c = src[i];
switch (c) {
case '\"':
out += "\\\"";
break;
case '\\':
out += "\\\\";
break;
case '\n':
out += "\\n";
break;
case '\r':
out += "\\r";
break;
case '\t':
out += "\\t";
break;
default:
out += c;
break;
}
}
return out;
}
static void computeEndLocation(const LChar* src, size_t length, uint32_t& endLine, uint32_t& endColumn)
{
for (size_t i = 0; i < length; i++) {
if (src[i] == '\n') {
endLine++;
endColumn = 0;
} else {
endColumn++;
}
}
}
std::string DebuggerDevtoolsMessageBuilder::buildEmptyMessage(const uint32_t id)
{
char buffer[64];
const int written = snprintf(buffer, sizeof(buffer),
R"({"id":%u,"result":{}})",
id);
if (written < 0) {
return {};
}
return { buffer, static_cast<size_t>(written) };
}
std::string DebuggerDevtoolsMessageBuilder::buildScriptParsedMessage(const uint8_t scriptId, const String* source, const String* srcName)
{
if (!source || !srcName || !source->is8Bit() || !srcName->is8Bit()) {
return {};
}
const LChar* sourceName = srcName->characters8();
const LChar* sourceCode = source->characters8();
size_t sourceNameLength = srcName->length();
size_t sourceCodeLength = source->length();
uint32_t endLine = 0;
uint32_t endColumn = 0;
computeEndLocation(sourceCode, sourceCodeLength, endLine, endColumn);
char buffer[512];
int written = snprintf(buffer, sizeof(buffer),
"{\"method\":\"Debugger.scriptParsed\","
"\"params\":{"
"\"scriptId\":\"%u\","
"\"url\":\"%.*s\","
"\"startLine\":0,"
"\"startColumn\":0,"
"\"endLine\":%u,"
"\"endColumn\":%u,"
"\"executionContextId\":1"
"}"
"}",
scriptId,
static_cast<int>(sourceNameLength),
reinterpret_cast<const char*>(sourceName),
endLine,
endColumn);
if (written < 0 || static_cast<size_t>(written) >= sizeof(buffer)) {
return {};
}
return { buffer, static_cast<size_t>(written) };
}
std::string DebuggerDevtoolsMessageBuilder::buildSourceCodeMessage(const uint8_t requestId, const String* source)
{
if (!source || !source->is8Bit()) {
return {};
}
const LChar* sourceCode = source->characters8();
size_t sourceCodeLength = source->length();
// Special characters in source code must be escaped.
std::string escapedSource = jsonEscape(sourceCode, sourceCodeLength);
// FIXME: buffer size depends on length of source.
char buffer[4096];
int written = snprintf(buffer, sizeof(buffer),
"{\"id\":%u,"
"\"result\":{"
"\"scriptSource\":\"%s\""
"}"
"}",
requestId,
escapedSource.c_str());
if (written < 0 || static_cast<size_t>(written) >= sizeof(buffer)) {
return {};
}
return { buffer, static_cast<size_t>(written) };
}
} // namespace Escargot
#endif /* ESCARGOT_DEBUGGER */

View file

@ -0,0 +1,41 @@
/*
* Copyright (c) 2026-present Samsung Electronics Co., Ltd
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
* USA
*/
#ifndef __DebuggerDevtoolsMessageBuilder__
#define __DebuggerDevtoolsMessageBuilder__
#include "rapidjson/document.h"
#include "rapidjson/rapidjson.h"
#ifdef ESCARGOT_DEBUGGER
namespace Escargot {
class String;
class DebuggerDevtoolsMessageBuilder {
public:
static std::string buildEmptyMessage(uint32_t id);
static std::string buildScriptParsedMessage(uint8_t scriptId, const String* source, const String* srcName);
static std::string buildSourceCodeMessage(uint8_t requestId, const String* source);
};
} // namespace Escargot
#endif /* ESCARGOT_DEBUGGER */
#endif

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,267 @@
/*
* Copyright (c) 2016-present Samsung Electronics Co., Ltd
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
* USA
*/
#ifndef __DebuggerEscargot__
#define __DebuggerEscargot__
#include "Debugger.h"
#include "DebuggerTcp.h"
#ifdef ESCARGOT_DEBUGGER
#define ESCARGOT_DEBUGGER_VERSION 1
#define ESCARGOT_DEBUGGER_MAX_VARIABLE_LENGTH 128
namespace Escargot {
class Context;
class Object;
class String;
class ExecutionState;
class ByteCodeBlock;
class InterpretedCodeBlock;
class DebuggerEscargot : public DebuggerTcp {
public:
// Messages sent by Escargot to the debugger client
enum {
ESCARGOT_MESSAGE_VERSION = 0,
ESCARGOT_MESSAGE_CONFIGURATION = 1,
ESCARGOT_MESSAGE_CLOSE_CONNECTION = 2,
ESCARGOT_MESSAGE_RELEASE_FUNCTION = 3,
ESCARGOT_MESSAGE_PARSE_DONE = 4,
ESCARGOT_MESSAGE_PARSE_ERROR = 5,
// These four must be in the same order.
ESCARGOT_MESSAGE_SOURCE_8BIT = 6,
ESCARGOT_MESSAGE_SOURCE_8BIT_END = 7,
ESCARGOT_MESSAGE_SOURCE_16BIT = 8,
ESCARGOT_MESSAGE_SOURCE_16BIT_END = 9,
// These four must be in the same order.
ESCARGOT_MESSAGE_FILE_NAME_8BIT = 10,
ESCARGOT_MESSAGE_FILE_NAME_8BIT_END = 11,
ESCARGOT_MESSAGE_FILE_NAME_16BIT = 12,
ESCARGOT_MESSAGE_FILE_NAME_16BIT_END = 13,
// These four must be in the same order.
ESCARGOT_MESSAGE_FUNCTION_NAME_8BIT = 14,
ESCARGOT_MESSAGE_FUNCTION_NAME_8BIT_END = 15,
ESCARGOT_MESSAGE_FUNCTION_NAME_16BIT = 16,
ESCARGOT_MESSAGE_FUNCTION_NAME_16BIT_END = 17,
ESCARGOT_MESSAGE_BREAKPOINT_LOCATION = 18,
ESCARGOT_MESSAGE_FUNCTION_PTR = 19,
ESCARGOT_MESSAGE_BREAKPOINT_HIT = 20,
ESCARGOT_MESSAGE_EXCEPTION_HIT = 21,
// These four must be in the same order.
ESCARGOT_MESSAGE_EVAL_RESULT_8BIT = 22,
ESCARGOT_MESSAGE_EVAL_RESULT_8BIT_END = 23,
ESCARGOT_MESSAGE_EVAL_RESULT_16BIT = 24,
ESCARGOT_MESSAGE_EVAL_RESULT_16BIT_END = 25,
// These four must be in the same order.
ESCARGOT_MESSAGE_EVAL_FAILED_8BIT = 26,
ESCARGOT_MESSAGE_EVAL_FAILED_8BIT_END = 27,
ESCARGOT_MESSAGE_EVAL_FAILED_16BIT = 28,
ESCARGOT_MESSAGE_EVAL_FAILED_16BIT_END = 29,
// These four must be in the same order.
ESCARGOT_MESSAGE_WATCH_RESULT_8BIT = 30,
ESCARGOT_MESSAGE_WATCH_RESULT_8BIT_END = 31,
ESCARGOT_MESSAGE_WATCH_RESULT_16BIT = 32,
ESCARGOT_MESSAGE_WATCH_RESULT_16BIT_END = 33,
ESCARGOT_MESSAGE_BACKTRACE_TOTAL = 34,
ESCARGOT_MESSAGE_BACKTRACE = 35,
ESCARGOT_MESSAGE_BACKTRACE_END = 36,
ESCARGOT_MESSAGE_SCOPE_CHAIN = 37,
ESCARGOT_MESSAGE_SCOPE_CHAIN_END = 38,
// These four must be in the same order.
ESCARGOT_MESSAGE_STRING_8BIT = 39,
ESCARGOT_MESSAGE_STRING_8BIT_END = 40,
ESCARGOT_MESSAGE_STRING_16BIT = 41,
ESCARGOT_MESSAGE_STRING_16BIT_END = 42,
ESCARGOT_MESSAGE_VARIABLE = 43,
ESCARGOT_MESSAGE_PRINT = 44,
ESCARGOT_MESSAGE_EXCEPTION = 45,
ESCARGOT_MESSAGE_EXCEPTION_BACKTRACE = 46,
ESCARGOT_DEBUGGER_WAIT_FOR_SOURCE = 47,
ESCARGOT_DEBUGGER_WAITING_AFTER_PENDING = 48,
ESCARGOT_DEBUGGER_WAIT_FOR_WAIT_EXIT = 49
};
// Messages sent by the debugger client to Escargot
enum {
ESCARGOT_MESSAGE_FUNCTION_RELEASED = 0,
ESCARGOT_MESSAGE_UPDATE_BREAKPOINT = 1,
ESCARGOT_MESSAGE_CONTINUE = 2,
ESCARGOT_MESSAGE_STEP = 3,
ESCARGOT_MESSAGE_NEXT = 4,
ESCARGOT_MESSAGE_FINISH = 5,
// These four must be in the same order.
ESCARGOT_MESSAGE_EVAL_8BIT_START = 6,
ESCARGOT_MESSAGE_EVAL_8BIT = 7,
ESCARGOT_MESSAGE_EVAL_16BIT_START = 8,
ESCARGOT_MESSAGE_EVAL_16BIT = 9,
// These four must be in the same order.
ESCARGOT_MESSAGE_EVAL_WITHOUT_STOP_8BIT_START = 10,
ESCARGOT_MESSAGE_EVAL_WITHOUT_STOP_8BIT = 11,
ESCARGOT_MESSAGE_EVAL_WITHOUT_STOP_16BIT_START = 12,
ESCARGOT_MESSAGE_EVAL_WITHOUT_STOP_16BIT = 13,
// These four must be in the same order.
ESCARGOT_MESSAGE_WATCH_8BIT_START = 14,
ESCARGOT_MESSAGE_WATCH_8BIT = 15,
ESCARGOT_MESSAGE_WATCH_16BIT_START = 16,
ESCARGOT_MESSAGE_WATCH_16BIT = 17,
ESCARGOT_MESSAGE_GET_BACKTRACE = 18,
ESCARGOT_MESSAGE_GET_SCOPE_CHAIN = 19,
ESCARGOT_MESSAGE_GET_SCOPE_VARIABLES = 20,
ESCARGOT_MESSAGE_GET_OBJECT = 21,
// These four must be in the same order.
ESCARGOT_DEBUGGER_CLIENT_SOURCE_8BIT_START = 22,
ESCARGOT_DEBUGGER_CLIENT_SOURCE_8BIT = 23,
ESCARGOT_DEBUGGER_CLIENT_SOURCE_16BIT_START = 24,
ESCARGOT_DEBUGGER_CLIENT_SOURCE_16BIT = 25,
ESCARGOT_DEBUGGER_THERE_WAS_NO_SOURCE = 26,
ESCARGOT_DEBUGGER_PENDING_CONFIG = 27,
ESCARGOT_DEBUGGER_PENDING_RESUME = 28,
ESCARGOT_DEBUGGER_WAIT_BEFORE_EXIT = 29,
ESCARGOT_DEBUGGER_STOP = 30
};
// Environment record types
enum {
ESCARGOT_RECORD_GLOBAL_ENVIRONMENT = 0,
ESCARGOT_RECORD_FUNCTION_ENVIRONMENT = 1,
ESCARGOT_RECORD_DECLARATIVE_ENVIRONMENT = 2,
ESCARGOT_RECORD_OBJECT_ENVIRONMENT = 3,
ESCARGOT_RECORD_MODULE_ENVIRONMENT = 4,
ESCARGOT_RECORD_UNKNOWN_ENVIRONMENT = 5,
};
// Variable types
enum {
ESCARGOT_VARIABLE_END = 0,
ESCARGOT_VARIABLE_UNACCESSIBLE = 1,
ESCARGOT_VARIABLE_UNDEFINED = 2,
ESCARGOT_VARIABLE_NULL = 3,
ESCARGOT_VARIABLE_TRUE = 4,
ESCARGOT_VARIABLE_FALSE = 5,
ESCARGOT_VARIABLE_NUMBER = 6,
ESCARGOT_VARIABLE_STRING = 7,
ESCARGOT_VARIABLE_SYMBOL = 8,
ESCARGOT_VARIABLE_BIGINT = 9,
// Only object types should be defined after this point.
ESCARGOT_VARIABLE_OBJECT = 10,
ESCARGOT_VARIABLE_ARRAY = 11,
ESCARGOT_VARIABLE_FUNCTION = 12,
ESCARGOT_VARIABLE_TYPE_MASK = 0x3f,
ESCARGOT_VARIABLE_LONG_NAME = 0x40,
ESCARGOT_VARIABLE_LONG_VALUE = 0x80,
};
inline bool pendingWait(void)
{
return m_pendingWait;
}
inline bool connected(void)
{
return enabled();
}
void sendType(uint8_t type);
void sendSubtype(uint8_t type, uint8_t subType);
void sendString(uint8_t type, const String* string);
void sendPointer(uint8_t type, const void* ptr);
void init(const char* options, Context* context) override;
void parseCompleted(String* source, String* srcName, size_t originLineOffset, String* error = nullptr) override;
void stopAtBreakpoint(ByteCodeBlock* byteCodeBlock, uint32_t offset, ExecutionState* state) override;
void byteCodeReleaseNotification(ByteCodeBlock* byteCodeBlock) override;
void exceptionCaught(String* message, SavedStackTraceDataVector& exceptionTrace) override;
void consoleOut(String* output) override;
String* getClientSource(String** sourceName) override;
bool getWaitBeforeExitClient() override;
void sendBacktraceInfo(uint8_t type, ByteCodeBlock* byteCodeBlock, uint32_t line, uint32_t column, uint32_t executionStateDepth);
void sendVariableObjectInfo(uint8_t subType, Object* object);
void waitForResolvingPendingBreakpoints();
DebuggerEscargot(EscargotSocket socket, String* skipSource)
: DebuggerTcp(socket, skipSource, ESCARGOT_WS_HEADER_BASE_SIZE + ESCARGOT_WS_MASK_SIZE + ESCARGOT_DEBUGGER_MAX_MESSAGE_LENGTH, ESCARGOT_DEBUGGER_WEBSOCKET_BINARY_FRAME)
, m_exitClient(false)
, m_pendingWait(false)
, m_waitForResume(false)
, m_watchEval(false)
, m_clientSourceData(nullptr)
, m_clientSourceName(nullptr)
{
}
bool processEvents(ExecutionState* state, Optional<ByteCodeBlock*> byteCodeBlock, bool isBlockingRequest = true) override;
private:
// Packed structure definitions to reduce network traffic
struct MessageVersion {
uint8_t littleEndian;
uint8_t version[sizeof(uint32_t)];
};
struct MessageConfiguration {
uint8_t maxMessageSize;
uint8_t pointerSize;
};
struct FunctionInfo {
uint8_t byteCodeStart[sizeof(void*)];
uint8_t startLine[sizeof(uint32_t)];
uint8_t startColumn[sizeof(uint32_t)];
};
struct BreakpointOffset {
uint8_t byteCodeStart[sizeof(void*)];
uint8_t offset[sizeof(uint32_t)];
};
struct BacktraceInfo {
uint8_t byteCode[sizeof(void*)];
uint8_t line[sizeof(uint32_t)];
uint8_t column[sizeof(uint32_t)];
uint8_t executionStateDepth[sizeof(uint32_t)];
};
struct VariableObjectInfo {
uint8_t subType;
uint8_t index[sizeof(uint32_t)];
};
bool doEval(ExecutionState* state, Optional<ByteCodeBlock*> byteCodeBlock, uint8_t* buffer, size_t length);
void getBacktrace(ExecutionState* state, uint32_t minDepth, uint32_t maxDepth, bool getTotal);
void getScopeChain(ExecutionState* state, uint32_t stateIndex);
void getScopeVariables(ExecutionState* state, uint32_t stateIndex, uint32_t index);
bool m_exitClient : 1;
bool m_pendingWait : 1;
bool m_waitForResume : 1;
bool m_watchEval : 1;
String* m_clientSourceData;
String* m_clientSourceName;
};
} // namespace Escargot
#endif /* ESCARGOT_DEBUGGER */
#endif

View file

@ -0,0 +1,321 @@
/*
* Copyright (c) 2026-present Samsung Electronics Co., Ltd
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
* USA
*/
#include "Escargot.h"
#include "DebuggerHttpRouter.h"
#include "DebuggerTcp.h"
#ifdef ESCARGOT_DEBUGGER
namespace Escargot {
#ifdef WIN32
#include <winsock2.h>
#include <ws2tcpip.h>
#else
#include <arpa/inet.h>
#endif
static uint8_t toBase64Character(uint8_t value)
{
if (value < 26) {
return static_cast<uint8_t>(value + 'A');
}
if (value < 52) {
return static_cast<uint8_t>(value - 26 + 'a');
}
if (value < 62) {
return static_cast<uint8_t>(value - 52 + '0');
}
if (value == 62) {
return '+';
}
return '/';
}
/**
* Encode a byte sequence into Base64 string.
*/
static void toBase64(const uint8_t* source, uint8_t* destination, size_t length)
{
while (length >= 3) {
uint8_t value = (source[0] >> 2);
destination[0] = toBase64Character(value);
value = static_cast<uint8_t>(((source[0] << 4) | (source[1] >> 4)) & 0x3f);
destination[1] = toBase64Character(value);
value = static_cast<uint8_t>(((source[1] << 2) | (source[2] >> 6)) & 0x3f);
destination[2] = toBase64Character(value);
value = static_cast<uint8_t>(source[2] & 0x3f);
destination[3] = toBase64Character(value);
source += 3;
destination += 4;
length -= 3;
}
}
bool DebuggerHttpRouter::requestStartsWith(const uint8_t* buffer, const size_t length, const char* prefix, const size_t prefixLength)
{
if (length < prefixLength)
return false;
return memcmp(buffer, prefix, prefixLength) == 0;
}
static bool buildHttpResponse(const char* body, uint8_t* buffer, size_t bufferSize, size_t& outLen)
{
size_t bodyLen = strlen(body);
int len = snprintf(reinterpret_cast<char*>(buffer), bufferSize,
"HTTP/1.1 200 OK\r\n"
"Content-Type: application/json; charset=UTF-8\r\n"
"Cache-Control: no-cache\r\n"
"Content-Length: %zu\r\n"
"Connection: close\r\n"
"\r\n"
"%s",
bodyLen,
body);
if (len < 0 || static_cast<size_t>(len) >= bufferSize)
return false;
outLen = static_cast<size_t>(len);
return true;
}
static bool buildVersionResponse(uint8_t* buffer, size_t bufferSize, size_t& outLen)
{
static constexpr const char body[] = "{"
"\"Browser\":\"Escargot/1.0\","
"\"Protocol-Version\":\"1.3\""
"}";
return buildHttpResponse(body, buffer, bufferSize, outLen);
}
static bool buildListResponse(uint8_t* buffer, size_t bufferSize, const char* ip, uint16_t port, size_t& outLen)
{
char body[512];
int bodyLen = snprintf(body, sizeof(body),
"[{"
"\"description\":\"Escargot CDP target\","
"\"devtoolsFrontendUrl\":\"/devtools/inspector.html?ws=%s:%u/devtools/page/1\","
"\"id\":\"1\","
"\"title\":\"Escargot\","
"\"type\":\"node\","
"\"url\":\"file:///\","
"\"webSocketDebuggerUrl\":\"ws://%s:%u/devtools/page/1\""
"}]",
ip, static_cast<unsigned>(port),
ip, static_cast<unsigned>(port));
if (bodyLen < 0 || static_cast<size_t>(bodyLen) >= sizeof(body)) {
return false;
}
return buildHttpResponse(body, buffer, bufferSize, outLen);
}
static bool webSocketHandshake(EscargotSocket socket, uint8_t* buffer, size_t length)
{
uint8_t* websocketKey = buffer;
constexpr char expectedWebsocketKey[] = "Sec-WebSocket-Key:";
constexpr size_t expectedWebsocketKeyLength = sizeof(expectedWebsocketKey) - 1;
while (true) {
if (length < expectedWebsocketKeyLength) {
ESCARGOT_LOG_ERROR("Sec-WebSocket-Key not found.\n");
return false;
}
if (websocketKey[0] == 'S'
&& websocketKey[-1] == '\n'
&& websocketKey[-2] == '\r'
&& memcmp(websocketKey, expectedWebsocketKey, expectedWebsocketKeyLength) == 0) {
websocketKey += expectedWebsocketKeyLength;
break;
}
websocketKey++;
}
/* String terminated by double newlines. */
while (*websocketKey == ' ') {
websocketKey++;
}
uint8_t* websocketKeyEnd = websocketKey;
while (*websocketKeyEnd > ' ') {
websocketKeyEnd++;
}
/* Since the buffer is not needed anymore it can
* be reused for storing the SHA-1 key and Base64 string. */
constexpr size_t sha1Length = 20;
DebuggerTcp::computeSha1(websocketKey,
static_cast<size_t>(websocketKeyEnd - websocketKey),
reinterpret_cast<const uint8_t*>("258EAFA5-E914-47DA-95CA-C5AB0DC85B11"),
36,
buffer);
/* The SHA-1 key is 20 bytes long but toBase64 expects a length
* divisible by 3 so an extra 0 is appended at the end. */
buffer[sha1Length] = 0;
toBase64(buffer, buffer + sha1Length + 1, sha1Length + 1);
/* Last value must be replaced by equal sign. */
constexpr uint8_t responsePrefix[] = "HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: ";
if (!DebuggerTcp::tcpSend(socket, responsePrefix, sizeof(responsePrefix) - 1)
|| !DebuggerTcp::tcpSend(socket, buffer + sha1Length + 1, 27)) {
return false;
}
constexpr uint8_t responseSuffix[] = "=\r\n\r\n";
return DebuggerTcp::tcpSend(socket, responseSuffix, sizeof(responseSuffix) - 1);
}
bool DebuggerHttpRouter::webSocketEstablished() const
{
return m_client != DebuggerClient::None;
}
DebuggerClient DebuggerHttpRouter::client() const
{
return m_client;
}
bool handleWebSocketRequest(const RequestContext& ctx)
{
return webSocketHandshake(ctx.socket, ctx.request, ctx.requestLength);
}
bool handleJsonVersion(const RequestContext& ctx)
{
uint8_t buffer[1024];
size_t responseLength = 0;
if (!buildVersionResponse(buffer, sizeof(buffer), responseLength)) {
return false;
}
return DebuggerTcp::tcpSend(ctx.socket, buffer, responseLength);
}
bool handleJsonList(const RequestContext& ctx)
{
struct sockaddr_in addr{};
socklen_t len = sizeof(addr);
getsockname(ctx.socket, reinterpret_cast<struct sockaddr*>(&addr), &len);
char ip[INET_ADDRSTRLEN];
inet_ntop(AF_INET, &addr.sin_addr, ip, sizeof(ip));
const uint16_t port = ntohs(addr.sin_port);
uint8_t buffer[1024];
size_t responseLength = 0;
if (!buildListResponse(buffer, sizeof(buffer), ip, port, responseLength)) {
return false;
}
return DebuggerTcp::tcpSend(ctx.socket, buffer, responseLength);
}
static bool readHttpMessage(EscargotSocket socket, uint8_t* buffer, size_t capacity, size_t& messageLength)
{
size_t remainingLength = capacity;
uint8_t* message = buffer;
while (true) {
size_t receivedLength = 0;
if (!DebuggerTcp::tcpReceive(socket, message, remainingLength, &receivedLength)) {
return false;
}
message += receivedLength;
remainingLength -= receivedLength;
if (message > (buffer + 4) && memcmp(message - 4, "\r\n\r\n", 4) == 0) {
messageLength = static_cast<size_t>(message - buffer);
return true;
}
if (remainingLength == 0) {
ESCARGOT_LOG_ERROR("WebSocket Error: Request too long\n");
return false;
}
}
}
template <size_t N>
constexpr Route route(const char (&prefix)[N], DebuggerClient client, RouteHandler handler)
{
return Route{ prefix, N - 1, client, handler };
}
bool DebuggerHttpRouter::handleHttpRequest(EscargotSocket socket)
{
uint8_t buffer[1024];
size_t messageLength = 0;
if (!readHttpMessage(socket, buffer, sizeof(buffer), messageLength)) {
return false;
}
static constexpr Route routes[] = {
route("GET /escargot-debugger", DebuggerClient::Escargot, handleWebSocketRequest),
route("GET /devtools/page/1", DebuggerClient::DevTools, handleWebSocketRequest),
route("GET /json/version", DebuggerClient::None, handleJsonVersion),
route("GET /json/list", DebuggerClient::None, handleJsonList),
route("GET /json", DebuggerClient::None, handleJsonList)
};
for (const auto& route : routes) {
if (!requestStartsWith(buffer, messageLength, route.prefix, route.prefixLength)) {
continue;
}
m_client = route.client;
// Skip the matched route prefix
uint8_t* remainder = buffer + route.prefixLength;
size_t remainderLength = messageLength - route.prefixLength;
return route.handler(RequestContext{ socket, remainder, remainderLength });
}
ESCARGOT_LOG_ERROR("Unsupported http request\n");
return false;
}
} // namespace Escargot
#endif /* ESCARGOT_DEBUGGER */

View file

@ -0,0 +1,70 @@
/*
* Copyright (c) 2026-present Samsung Electronics Co., Ltd
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
* USA
*/
#ifndef __DebuggerHttpRouter__
#define __DebuggerHttpRouter__
#ifdef ESCARGOT_DEBUGGER
namespace Escargot {
#ifdef WIN32
#include <winsock2.h>
typedef SOCKET EscargotSocket;
#else /* !WIN32 */
typedef int EscargotSocket;
#endif /* WIN32 */
enum class DebuggerClient : uint8_t {
None,
Escargot,
DevTools
};
struct RequestContext {
EscargotSocket socket;
uint8_t* request;
size_t requestLength;
};
using RouteHandler = bool (*)(const RequestContext&);
struct Route {
const char* prefix;
size_t prefixLength;
DebuggerClient client;
RouteHandler handler;
};
class DebuggerHttpRouter {
public:
DebuggerHttpRouter() = default;
static bool requestStartsWith(const uint8_t* buffer, size_t length, const char* prefix, size_t prefixLength);
bool handleHttpRequest(EscargotSocket socket);
bool webSocketEstablished() const;
DebuggerClient client() const;
private:
DebuggerClient m_client{ DebuggerClient::None };
};
} // namespace Escargot
#endif /* ESCARGOT_DEBUGGER */
#endif

View file

@ -19,6 +19,9 @@
#include "Escargot.h"
#include "DebuggerTcp.h"
#include "DebuggerDevtools.h"
#include "DebuggerEscargot.h"
#include "DebuggerHttpRouter.h"
#include "runtime/String.h" // for split function
#ifdef ESCARGOT_DEBUGGER
@ -110,7 +113,7 @@ static inline void tcpCloseSocket(EscargotSocket socket)
#endif /* WIN32 */
}
static bool tcpSend(EscargotSocket socket, const uint8_t* message, size_t messageLength)
bool DebuggerTcp::tcpSend(EscargotSocket socket, const uint8_t* message, size_t messageLength)
{
do {
#ifdef OS_POSIX
@ -122,7 +125,7 @@ static bool tcpSend(EscargotSocket socket, const uint8_t* message, size_t messag
}
#endif /* OS_POSIX */
ssize_t sentBytes = send(socket, message, messageLength, 0);
ssize_t sentBytes = Escargot::send(socket, message, messageLength, 0);
if (sentBytes < 0) {
int errorNumber = tcpGetErrno();
@ -142,7 +145,7 @@ static bool tcpSend(EscargotSocket socket, const uint8_t* message, size_t messag
return true;
}
static bool tcpReceive(EscargotSocket socket, uint8_t* message, size_t maxLength, size_t* receivedLength)
bool DebuggerTcp::tcpReceive(EscargotSocket socket, uint8_t* message, size_t maxLength, size_t* receivedLength)
{
*receivedLength = 0;
@ -162,152 +165,169 @@ static bool tcpReceive(EscargotSocket socket, uint8_t* message, size_t maxLength
return true;
}
static uint8_t toBase64Character(uint8_t value)
bool DebuggerTcp::send(const uint8_t type, const void* buffer, const size_t length)
{
if (value < 26) {
return (uint8_t)(value + 'A');
}
ASSERT(enabled());
ASSERT(m_websocketMessageType == ESCARGOT_DEBUGGER_WEBSOCKET_TEXT_FRAME
|| (m_websocketMessageType == ESCARGOT_DEBUGGER_WEBSOCKET_BINARY_FRAME && length <= ESCARGOT_DEBUGGER_MAX_MESSAGE_LENGTH));
if (value < 52) {
return (uint8_t)(value - 26 + 'a');
}
if (value < 62) {
return (uint8_t)(value - 52 + '0');
}
if (value == 62) {
return (uint8_t)'+';
}
return (uint8_t)'/';
}
/**
* Encode a byte sequence into Base64 string.
*/
static void toBase64(const uint8_t* source, uint8_t* destination, size_t length)
{
while (length >= 3) {
uint8_t value = (source[0] >> 2);
destination[0] = toBase64Character(value);
value = (uint8_t)(((source[0] << 4) | (source[1] >> 4)) & 0x3f);
destination[1] = toBase64Character(value);
value = (uint8_t)(((source[1] << 2) | (source[2] >> 6)) & 0x3f);
destination[2] = toBase64Character(value);
value = (uint8_t)(source[2] & 0x3f);
destination[3] = toBase64Character(value);
source += 3;
destination += 4;
length -= 3;
}
}
static bool webSocketHandshake(EscargotSocket socket)
{
uint8_t buffer[1024];
size_t remainingLength = sizeof(buffer);
uint8_t* message = buffer;
while (true) {
size_t receivedLength;
if (!tcpReceive(socket, message, remainingLength, &receivedLength)) {
return false;
}
message += receivedLength;
remainingLength -= receivedLength;
if (message > (buffer + 4) && memcmp(message - 4, "\r\n\r\n", 4) == 0) {
break;
}
if (remainingLength == 0) {
ESCARGOT_LOG_ERROR("WebSocket Error: Request too long\n");
return false;
}
}
/* Check protocol. */
const char expectedProtocol[] = "GET /escargot-debugger";
const size_t expectedProtocolLength = sizeof(expectedProtocol) - 1;
if ((size_t)(message - buffer) < expectedProtocolLength
|| memcmp(buffer, expectedProtocol, expectedProtocolLength) != 0) {
ESCARGOT_LOG_ERROR("WebSocket Error: Invalid handshake format.\n");
if (length > ESCARGOT_WS_MAX_MESSAGE_LENGTH) {
ESCARGOT_LOG_ERROR("Cannot send WebSocket payload: 64-bit payload length is not supported.\n");
close(CloseAbortConnection);
return false;
}
uint8_t* websocketKey = buffer + expectedProtocolLength;
size_t headerLength = 0;
uint8_t message[ESCARGOT_WS_HEADER_BASE_SIZE + ESCARGOT_WS_EXT_LEN16_SIZE + length];
message[0] = ESCARGOT_DEBUGGER_WEBSOCKET_FIN_BIT | m_websocketMessageType;
const char expectedWebsocketKey[] = "Sec-WebSocket-Key:";
const size_t expectedWebsocketKeyLength = sizeof(expectedWebsocketKey) - 1;
// Server-to-client WebSocket frames are not masked,
// therefore the masking key is not included in the header.
if (length <= ESCARGOT_DEBUGGER_MAX_MESSAGE_LENGTH) {
const uint8_t type_byte = (m_websocketMessageType == ESCARGOT_DEBUGGER_WEBSOCKET_TEXT_FRAME ? 0 : 1);
headerLength = ESCARGOT_WS_HEADER_SIZE - ESCARGOT_WS_MASK_SIZE + type_byte;
while (true) {
if ((size_t)(message - websocketKey) < expectedWebsocketKeyLength) {
ESCARGOT_LOG_ERROR("Sec-WebSocket-Key not found.\n");
return false;
message[1] = static_cast<uint8_t>(length + type_byte);
if (type_byte) {
message[2] = type;
}
} else {
headerLength = ESCARGOT_WS_HEADER_LEN16_SIZE - ESCARGOT_WS_MASK_SIZE;
if (websocketKey[0] == 'S'
&& websocketKey[-1] == '\n'
&& websocketKey[-2] == '\r'
&& memcmp(websocketKey, expectedWebsocketKey, expectedWebsocketKeyLength) == 0) {
websocketKey += expectedWebsocketKeyLength;
break;
}
websocketKey++;
message[1] = ESCARGOT_WS_MESSAGE_16BIT_LENGTH_MARKER;
message[2] = static_cast<uint8_t>((length >> 8) & 0xFF);
message[3] = static_cast<uint8_t>(length & 0xFF);
}
/* String terminated by double newlines. */
while (*websocketKey == ' ') {
websocketKey++;
}
memcpy(message + headerLength, buffer, length);
uint8_t* websocketKeyEnd = websocketKey;
while (*websocketKeyEnd > ' ') {
websocketKeyEnd++;
}
/* Since the buffer is not needed anymore it can
* be reused for storing the SHA-1 key and Base64 string. */
const size_t sha1Length = 20;
DebuggerTcp::computeSha1(websocketKey,
(size_t)(websocketKeyEnd - websocketKey),
(const uint8_t*)"258EAFA5-E914-47DA-95CA-C5AB0DC85B11",
36,
buffer);
/* The SHA-1 key is 20 bytes long but toBase64 expects a length
* divisible by 3 so an extra 0 is appended at the end. */
buffer[sha1Length] = 0;
toBase64(buffer, buffer + sha1Length + 1, sha1Length + 1);
/* Last value must be replaced by equal sign. */
const uint8_t responsePrefix[] = "HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: ";
if (!tcpSend(socket, responsePrefix, sizeof(responsePrefix) - 1)
|| !tcpSend(socket, buffer + sha1Length + 1, 27)) {
if (!tcpSend(m_socket, message, headerLength + length)) {
ESCARGOT_LOG_ERROR("Failed to send data via WebSocket connection.\n");
close(CloseAbortConnection);
return false;
}
const uint8_t responseSuffix[] = "=\r\n\r\n";
return tcpSend(socket, responseSuffix, sizeof(responseSuffix) - 1);
// ESCARGOT_LOG_INFO("Sent message: %s\n", static_cast<const char*>(buffer));
return true;
}
void DebuggerTcp::init(const char* options, Context* context)
bool DebuggerTcp::receive(uint8_t* buffer, size_t& length)
{
size_t receivedLength = 0;
if (m_payloadLength == 0 || m_receiveBufferFill < m_headerLength + m_payloadLength) {
/* Cannot extract a whole message from the buffer. */
if (!tcpReceive(m_socket,
m_receiveBuffer + m_receiveBufferFill,
m_bufferSize - m_receiveBufferFill,
&receivedLength)) {
ESCARGOT_LOG_ERROR("Failed to receive data from WebSocket connection.\n");
close(CloseAbortConnection);
return false;
}
if (receivedLength == 0 && m_receiveBufferFill < m_headerLength) {
// ESCARGOT_LOG_INFO("Incomplete WebSocket frame header, waiting for more data.\n");
return false;
}
m_receiveBufferFill = static_cast<uint32_t>(m_receiveBufferFill + receivedLength);
}
if (m_payloadLength == 0) {
if (m_receiveBufferFill < ESCARGOT_WS_HEADER_BASE_SIZE) {
return false;
}
if ((m_receiveBuffer[0] & ESCARGOT_DEBUGGER_WEBSOCKET_OPCODE_MASK) != m_websocketMessageType) {
if ((m_receiveBuffer[0] & ESCARGOT_DEBUGGER_WEBSOCKET_OPCODE_MASK) == ESCARGOT_DEBUGGER_WEBSOCKET_CLOSE_FRAME) {
close(CloseEndConnection);
return false;
}
ESCARGOT_LOG_ERROR("Unsupported Websocket opcode.\n");
close(CloseProtocolUnsupported);
return false;
}
if ((m_receiveBuffer[0] & ~ESCARGOT_DEBUGGER_WEBSOCKET_OPCODE_MASK) != ESCARGOT_DEBUGGER_WEBSOCKET_FIN_BIT
|| !(m_receiveBuffer[1] & ESCARGOT_DEBUGGER_WEBSOCKET_MASK_BIT)) {
ESCARGOT_LOG_ERROR("Unsupported Websocket message.\n");
close(CloseProtocolUnsupported);
return false;
}
uint8_t payloadLengthField = m_receiveBuffer[1] & ESCARGOT_DEBUGGER_WEBSOCKET_LENGTH_MASK;
if (payloadLengthField > ESCARGOT_WS_MESSAGE_16BIT_LENGTH_MARKER) {
ESCARGOT_LOG_ERROR("64-bit WebSocket payload length is not supported.\n");
close(CloseProtocolUnsupported);
return false;
}
if (payloadLengthField <= ESCARGOT_DEBUGGER_MAX_MESSAGE_LENGTH) {
m_payloadLength = payloadLengthField;
m_headerLength = ESCARGOT_WS_HEADER_SIZE;
} else {
ASSERT(payloadLengthField == ESCARGOT_WS_MESSAGE_16BIT_LENGTH_MARKER);
// Ensure that the extended payload length field is available
if (m_receiveBufferFill < ESCARGOT_WS_HEADER_LEN16_SIZE) {
return false;
}
m_headerLength = ESCARGOT_WS_HEADER_LEN16_SIZE;
m_payloadLength = (static_cast<uint16_t>(m_receiveBuffer[2]) << 8) | static_cast<uint16_t>(m_receiveBuffer[3]);
}
if (m_payloadLength == 0) {
ESCARGOT_LOG_ERROR("Invalid WebSocket payload length: zero-length messages are not supported.\n");
close(CloseProtocolUnsupported);
return false;
}
}
const size_t totalSize = m_headerLength + m_payloadLength;
if (m_receiveBufferFill < totalSize) {
return false;
}
const uint8_t* mask = m_receiveBuffer + m_headerLength - ESCARGOT_WS_MASK_SIZE;
const uint8_t* mask_end = mask + ESCARGOT_WS_MASK_SIZE;
const uint8_t* source = mask_end;
uint8_t* buffer_end = buffer + m_payloadLength;
while (buffer < buffer_end) {
*buffer++ = *source++ ^ *mask++;
if (mask >= mask_end) {
mask -= 4;
}
}
*buffer_end = 0;
length = m_payloadLength;
m_headerLength = ESCARGOT_WS_HEADER_SIZE;
m_payloadLength = 0;
if (m_receiveBufferFill == totalSize) {
m_receiveBufferFill = 0;
return true;
}
m_receiveBufferFill = static_cast<uint32_t>(m_receiveBufferFill - totalSize);
memmove(m_receiveBuffer, m_receiveBuffer + totalSize, m_receiveBufferFill);
return true;
}
Debugger* DebuggerTcp::createDebugger(const char* options, Context* context)
{
uint16_t port = 6501;
int timeout = -1;
String* skipSourceName = nullptr;
EscargotSocket clientSocket = 0;
if (options) {
auto v = split(options, ';');
const char portOption[] = "--port=";
@ -325,31 +345,29 @@ void DebuggerTcp::init(const char* options, Context* context)
} else if (s.find(skipOption) == 0) {
const char* skipStr = const_cast<const char*>(s.data() + sizeof(skipOption) - 1);
size_t skipLen = strlen(skipStr);
m_skipSourceName = String::fromASCII(skipStr, skipLen);
skipSourceName = String::fromASCII(skipStr, skipLen);
}
}
}
ASSERT(enabled() == false);
#ifdef WIN32
WSADATA wsaData;
int wsa_init_status = WSAStartup(MAKEWORD(2, 2), &wsaData);
if (wsa_init_status != NO_ERROR) {
return;
return nullptr;
}
#endif /* WIN32*/
EscargotSocket serverSocket = socket(AF_INET, SOCK_STREAM, 0);
if (m_socket == ESCARGOT_INVALID_SOCKET) {
return;
if (serverSocket == ESCARGOT_INVALID_SOCKET) {
return nullptr;
}
if (!tcpConfigureSocket(serverSocket, port)) {
int error = tcpGetErrno();
tcpCloseSocket(serverSocket);
tcpLogError(error);
return;
return nullptr;
}
ESCARGOT_LOG_INFO("Waiting for client connection 0.0.0.0:%hd\n", port);
@ -372,58 +390,72 @@ void DebuggerTcp::init(const char* options, Context* context)
if (timeout < 0) {
ESCARGOT_LOG_ERROR("Waiting for client connection error: timeout reached\n");
tcpCloseSocket(serverSocket);
return;
return nullptr;
}
}
sockaddr_in addr;
socklen_t sinSize = sizeof(sockaddr_in);
DebuggerHttpRouter httpRouter;
m_socket = accept(serverSocket, (sockaddr*)&addr, &sinSize);
while (true) {
clientSocket = accept(serverSocket, (sockaddr*)&addr, &sinSize);
if (clientSocket == ESCARGOT_INVALID_SOCKET) {
tcpLogError(tcpGetErrno());
return nullptr;
}
#ifdef WIN32
u_long nonblockingEnabled = 1;
/* Set non-blocking mode. */
if (ioctlsocket(clientSocket, FIONBIO, &nonblockingEnabled) != NO_ERROR) {
tcpCloseSocket(clientSocket);
return nullptr;
}
#else /* !WIN32 */
int socketFlags = fcntl(clientSocket, F_GETFL, 0);
if (socketFlags < 0) {
tcpCloseSocket(clientSocket);
return nullptr;
}
/* Set non-blocking mode. */
if (fcntl(clientSocket, F_SETFL, socketFlags | O_NONBLOCK) == -1) {
tcpCloseSocket(clientSocket);
return nullptr;
}
#endif /* WIN32 */
ESCARGOT_LOG_INFO("Connected from: %s\n", inet_ntoa(addr.sin_addr));
if (!httpRouter.handleHttpRequest(clientSocket)) {
tcpCloseSocket(clientSocket);
return nullptr;
}
if (httpRouter.webSocketEstablished()) {
break;
}
// Close connection, waiting for the next request
tcpCloseSocket(clientSocket);
}
tcpCloseSocket(serverSocket);
if (m_socket == ESCARGOT_INVALID_SOCKET) {
tcpLogError(tcpGetErrno());
return;
Debugger* client;
if (httpRouter.client() == DebuggerClient::Escargot) {
client = new DebuggerEscargot(clientSocket, skipSourceName);
} else {
client = new DebuggerDevtools(clientSocket, skipSourceName);
}
#ifdef WIN32
u_long nonblockingEnabled = 1;
/* Set non-blocking mode. */
if (ioctlsocket(m_socket, FIONBIO, &nonblockingEnabled) != NO_ERROR) {
tcpCloseSocket(m_socket);
return;
}
#else /* !WIN32 */
int socketFlags = fcntl(m_socket, F_GETFL, 0);
if (socketFlags < 0) {
tcpCloseSocket(m_socket);
return;
}
/* Set non-blocking mode. */
if (fcntl(m_socket, F_SETFL, socketFlags | O_NONBLOCK) == -1) {
tcpCloseSocket(m_socket);
return;
}
#endif /* WIN32 */
ESCARGOT_LOG_INFO("Connected from: %s\n", inet_ntoa(addr.sin_addr));
if (!webSocketHandshake(m_socket)) {
tcpCloseSocket(m_socket);
return;
}
m_receiveBufferFill = 0;
m_messageLength = 0;
enable(context);
return DebuggerRemote::init(nullptr, context);
client->enable(context);
return client;
}
bool DebuggerTcp::skipSourceCode(String* srcName) const
@ -436,34 +468,6 @@ bool DebuggerTcp::skipSourceCode(String* srcName) const
return srcName->contains(m_skipSourceName);
}
#define ESCARGOT_DEBUGGER_WEBSOCKET_FIN_BIT 0x80
#define ESCARGOT_DEBUGGER_WEBSOCKET_BINARY_FRAME 2
#define ESCARGOT_DEBUGGER_WEBSOCKET_CLOSE_FRAME 8
#define ESCARGOT_DEBUGGER_WEBSOCKET_OPCODE_MASK 0x0f
#define ESCARGOT_DEBUGGER_WEBSOCKET_LENGTH_MASK 0x7f
#define ESCARGOT_DEBUGGER_WEBSOCKET_ONE_BYTE_LEN_MAX 125
#define ESCARGOT_DEBUGGER_WEBSOCKET_MASK_BIT 0x80
bool DebuggerTcp::send(uint8_t type, const void* buffer, size_t length)
{
ASSERT(enabled());
ASSERT(length < ESCARGOT_DEBUGGER_MAX_MESSAGE_LENGTH);
uint8_t message[ESCARGOT_DEBUGGER_MAX_MESSAGE_LENGTH + 2];
message[0] = ESCARGOT_DEBUGGER_WEBSOCKET_FIN_BIT | ESCARGOT_DEBUGGER_WEBSOCKET_BINARY_FRAME;
message[1] = (uint8_t)(length + 1);
message[2] = type;
memcpy(message + 3, buffer, length);
if (tcpSend(m_socket, message, length + 3)) {
return true;
}
close(CloseAbortConnection);
return false;
}
bool DebuggerTcp::isThereAnyEvent()
{
// if there is remained receive buffer data,
@ -484,91 +488,6 @@ bool DebuggerTcp::isThereAnyEvent()
return true;
}
bool DebuggerTcp::receive(uint8_t* buffer, size_t& length)
{
size_t receivedLength;
if (m_messageLength == 0 || m_receiveBufferFill < 2 + sizeof(uint32_t) + m_messageLength) {
/* Cannot extract a whole message from the buffer. */
if (!tcpReceive(m_socket,
m_receiveBuffer + m_receiveBufferFill,
ESCARGOT_DEBUGGER_MAX_MESSAGE_LENGTH + 2 + sizeof(uint32_t) - m_receiveBufferFill,
&receivedLength)) {
close(CloseAbortConnection);
return false;
}
if (receivedLength == 0 && m_receiveBufferFill < (2 + sizeof(uint32_t))) {
return false;
}
m_receiveBufferFill = (uint8_t)(m_receiveBufferFill + receivedLength);
}
if (m_messageLength == 0) {
if (m_receiveBufferFill < 3) {
return false;
}
if ((m_receiveBuffer[0] & ESCARGOT_DEBUGGER_WEBSOCKET_OPCODE_MASK) != ESCARGOT_DEBUGGER_WEBSOCKET_BINARY_FRAME) {
if ((m_receiveBuffer[0] & ESCARGOT_DEBUGGER_WEBSOCKET_OPCODE_MASK) == ESCARGOT_DEBUGGER_WEBSOCKET_CLOSE_FRAME) {
close(CloseEndConnection);
return false;
}
ESCARGOT_LOG_ERROR("Unsupported Websocket opcode.\n");
close(CloseProtocolUnsupported);
return false;
}
if ((m_receiveBuffer[0] & ~ESCARGOT_DEBUGGER_WEBSOCKET_OPCODE_MASK) != ESCARGOT_DEBUGGER_WEBSOCKET_FIN_BIT
|| !(m_receiveBuffer[1] & ESCARGOT_DEBUGGER_WEBSOCKET_MASK_BIT)) {
ESCARGOT_LOG_ERROR("Unsupported Websocket message.\n");
close(CloseProtocolUnsupported);
return false;
}
m_messageLength = (uint8_t)(m_receiveBuffer[1] & ESCARGOT_DEBUGGER_WEBSOCKET_LENGTH_MASK);
if (m_messageLength == 0 || m_messageLength > ESCARGOT_DEBUGGER_WEBSOCKET_ONE_BYTE_LEN_MAX) {
ESCARGOT_LOG_ERROR("Unsupported Websocket message size.\n");
close(CloseProtocolUnsupported);
return false;
}
}
size_t totalSize = 2 + sizeof(uint32_t) + m_messageLength;
if (m_receiveBufferFill < totalSize) {
return false;
}
uint8_t* mask = m_receiveBuffer + 2;
uint8_t* mask_end = mask + sizeof(uint32_t);
uint8_t* source = mask_end;
uint8_t* buffer_end = buffer + m_messageLength;
while (buffer < buffer_end) {
*buffer++ = *source++ ^ *mask++;
if (mask >= mask_end) {
mask -= 4;
}
}
length = m_messageLength;
m_messageLength = 0;
if (m_receiveBufferFill == totalSize) {
m_receiveBufferFill = 0;
return true;
}
m_receiveBufferFill = (uint8_t)(m_receiveBufferFill - totalSize);
memmove(m_receiveBuffer, m_receiveBuffer + totalSize, m_receiveBufferFill);
return true;
}
void DebuggerTcp::close(CloseReason reason)
{
if (!enabled()) {

View file

@ -31,39 +31,75 @@ typedef SOCKET EscargotSocket;
#else /* !WIN32 */
typedef int EscargotSocket;
#endif /* WIN32 */
#define ESCARGOT_DEBUGGER_MAX_MESSAGE_LENGTH 125
class DebuggerTcp : public DebuggerRemote {
#define ESCARGOT_DEBUGGER_WEBSOCKET_FIN_BIT 0x80
#define ESCARGOT_DEBUGGER_WEBSOCKET_TEXT_FRAME 1
#define ESCARGOT_DEBUGGER_WEBSOCKET_BINARY_FRAME 2
#define ESCARGOT_DEBUGGER_WEBSOCKET_CLOSE_FRAME 8
#define ESCARGOT_DEBUGGER_WEBSOCKET_OPCODE_MASK 0x0f
#define ESCARGOT_DEBUGGER_WEBSOCKET_LENGTH_MASK 0x7f
#define ESCARGOT_DEBUGGER_WEBSOCKET_ONE_BYTE_LEN_MAX 125
#define ESCARGOT_DEBUGGER_WEBSOCKET_MASK_BIT 0x80
#define ESCARGOT_WS_HEADER_BASE_SIZE 2
#define ESCARGOT_WS_EXT_LEN16_SIZE 2
#define ESCARGOT_WS_MASK_SIZE 4
#define ESCARGOT_WS_MESSAGE_16BIT_LENGTH_MARKER 126
#define ESCARGOT_WS_MAX_MESSAGE_LENGTH 65535
#define ESCARGOT_WS_HEADER_SIZE (ESCARGOT_WS_HEADER_BASE_SIZE + ESCARGOT_WS_MASK_SIZE)
#define ESCARGOT_WS_HEADER_LEN16_SIZE (ESCARGOT_WS_HEADER_SIZE + ESCARGOT_WS_EXT_LEN16_SIZE)
#define ESCARGOT_WS_BUFFER_SIZE (ESCARGOT_WS_HEADER_LEN16_SIZE + ESCARGOT_WS_MAX_MESSAGE_LENGTH)
class DebuggerTcp : public Debugger {
public:
DebuggerTcp()
: m_socket(0)
, m_receiveBuffer{}
DebuggerTcp(const EscargotSocket socket, String* skipSource, const uint32_t bufferSize, const uint8_t type)
: m_socket(socket)
, m_bufferSize(bufferSize)
, m_receiveBuffer(new uint8_t[bufferSize])
, m_payloadLength(0)
, m_receiveBufferFill(0)
, m_messageLength(0)
, m_skipSourceName(nullptr)
, m_headerLength(0)
, m_websocketMessageType(type)
, m_skipSourceName(skipSource)
{
}
virtual void init(const char* options, Context* context) override;
static Debugger* createDebugger(const char* options, Context* context);
void init(const char* options, Context* context) override {}
virtual bool skipSourceCode(String* srcName) const override;
bool skipSourceCode(String* srcName) const override;
static void computeSha1(const uint8_t* source1, size_t source1Length,
const uint8_t* source2, size_t source2Length,
uint8_t destination[20]);
protected:
virtual bool send(uint8_t type, const void* buffer, size_t length) override;
virtual bool receive(uint8_t* buffer, size_t& length) override;
virtual bool isThereAnyEvent() override;
virtual void close(CloseReason reason) override;
static bool tcpReceive(EscargotSocket socket, uint8_t* message, size_t maxLength, size_t* receivedLength);
static bool tcpSend(EscargotSocket socket, const uint8_t* message, size_t messageLength);
private:
void receiveData();
protected:
enum CloseReason {
CloseEndConnection,
CloseAbortConnection,
CloseProtocolUnsupported,
CloseProtocolError,
};
bool send(uint8_t type, const void* buffer, size_t length);
bool receive(uint8_t* buffer, size_t& length);
bool isThereAnyEvent();
void close(CloseReason reason);
EscargotSocket m_socket;
uint8_t m_receiveBuffer[2 + sizeof(uint32_t) + ESCARGOT_DEBUGGER_MAX_MESSAGE_LENGTH];
uint32_t m_bufferSize;
uint8_t* m_receiveBuffer;
uint16_t m_payloadLength;
uint8_t m_receiveBufferFill;
uint8_t m_messageLength;
uint8_t m_headerLength;
uint8_t m_websocketMessageType;
// skip generating debugging bytecode for source code whose name contains m_skipSourceName
String* m_skipSourceName;

View file

@ -158,14 +158,14 @@ ASTAllocator& Context::astAllocator()
#ifdef ESCARGOT_DEBUGGER
bool Context::initDebuggerRemote(const char* options)
bool Context::initDebugger(const char* options)
{
if (debuggerEnabled()) {
// debugger cannot be re-initialized
return false;
}
Debugger::createDebuggerRemote(options, this);
Debugger::createDebugger(options, this);
return m_debugger != nullptr;
}

View file

@ -323,7 +323,7 @@ public:
return m_debugger;
}
bool initDebuggerRemote(const char* options);
bool initDebugger(const char* options);
void initDebugger(Debugger* debugger);
void removeDebugger();
bool debuggerEnabled() const;

View file

@ -294,7 +294,8 @@ private:
// http://www.ecma-international.org/ecma-262/6.0/index.html#sec-global-environment-records
class GlobalEnvironmentRecord : public EnvironmentRecord {
#ifdef ESCARGOT_DEBUGGER
friend class DebuggerRemote;
friend class DebuggerEscargot;
friend class DebuggerDevtools;
friend class DebuggerAPI;
#endif /* ESCARGOT_DEBUGGER */
public:
@ -587,7 +588,8 @@ public:
// DeclarativeEnvironmentRecordNotIndexed record does not create binding self likes FunctionEnvironmentRecord
class DeclarativeEnvironmentRecordNotIndexed : public DeclarativeEnvironmentRecord {
#ifdef ESCARGOT_DEBUGGER
friend class DebuggerRemote;
friend class DebuggerEscargot;
friend class DebuggerDevtools;
friend class DebuggerAPI;
#endif /* ESCARGOT_DEBUGGER */
public:
@ -1202,7 +1204,8 @@ public:
class ModuleEnvironmentRecord : public DeclarativeEnvironmentRecord {
#ifdef ESCARGOT_DEBUGGER
friend class DebuggerRemote;
friend class DebuggerEscargot;
friend class DebuggerDevtools;
friend class DebuggerAPI;
#endif /* ESCARGOT_DEBUGGER */
public:

View file

@ -1202,7 +1202,7 @@ int main(int argc, char* argv[])
continue;
}
if (strcmp(argv[i], "--start-debug-server") == 0) {
context->initDebuggerRemote(nullptr);
context->initDebugger(nullptr);
continue;
}
if (strcmp(argv[i], "--debugger-wait-source") == 0) {

View file

@ -2752,7 +2752,7 @@ TEST(Debugger, Basic)
DebuggerTest* debuggerTest = new DebuggerTest();
StringRef* fileName = StringRef::createFromUTF8(debuggerFileNameString, sizeof(debuggerFileNameString) - 1);
context->initDebugger(debuggerTest);
context->initDebuggerClient(debuggerTest);
StringRef* source = StringRef::createFromUTF8(debuggerSourceString1, sizeof(debuggerSourceString1) - 1);
evalScript(context, source, fileName, false);
@ -2778,7 +2778,7 @@ TEST(Debugger, RemoteOption)
PersistentRefHolder<VMInstanceRef> instance = VMInstanceRef::create();
PersistentRefHolder<ContextRef> context = createEscargotContext(instance.get());
// 100ms
EXPECT_FALSE(context->initDebuggerRemote("--accept-timeout=100"));
EXPECT_FALSE(context->initDebugger("--accept-timeout=100"));
EXPECT_FALSE(context->isDebuggerRunning());
EXPECT_FALSE(context->isWaitBeforeExit());
@ -2873,7 +2873,7 @@ TEST(Debugger, ObjectStore)
DebuggerTest* debuggerTest = new DebuggerTest();
StringRef* fileName = StringRef::createFromUTF8(debuggerFileNameString, sizeof(debuggerFileNameString) - 1);
context->initDebugger(debuggerTest);
context->initDebuggerClient(debuggerTest);
StringRef* source = StringRef::createFromUTF8(debuggerSourceString1, sizeof(debuggerSourceString1) - 1);
evalScript(context, source, fileName, false);

View file

@ -2001,7 +2001,7 @@ public:
\param stackCapacity Optional initial capacity of stack in bytes.
\param stackAllocator Optional allocator for allocating memory for stack.
*/
#if defined(ESCARGOT) && !defined(COMPILER_MSVC)
#if defined(ESCARGOT) && (defined(COMPILER_GCC) || defined(COMPILER_CLANG))
__attribute__((__noinline__)) GenericDocument(Allocator* allocator = 0, size_t stackCapacity = kDefaultStackCapacity, StackAllocator* stackAllocator = 0)
:
#else