From 3d3e6fdf58fd0b3631f7b5286ca5088291ccf1ee Mon Sep 17 00:00:00 2001 From: Robert Sipka Date: Mon, 18 Mar 2019 11:00:15 +0100 Subject: [PATCH] [Debugger] Create a simplified transmission layer (#2781) It supports the raw packet sending over a TCP/IP protocol JerryScript-DCO-1.0-Signed-off-by: Robert Sipka rsipka.uszeged@partner.samsung.com --- docs/07.DEBUGGER.md | 3 +- jerry-debugger/jerry_client.py | 2 +- jerry-debugger/jerry_client_main.py | 21 ++- jerry-debugger/jerry_client_rawpacket.py | 91 +++++++++++ jerry-debugger/jerry_client_websocket.py | 3 +- jerry-ext/debugger/debugger-rp.c | 159 +++++++++++++++++++ jerry-ext/include/jerryscript-ext/debugger.h | 1 + jerry-main/main-unix.c | 42 ++++- tools/run-tests.py | 28 ++-- tools/runners/run-debugger-test.sh | 13 +- 10 files changed, 328 insertions(+), 35 deletions(-) create mode 100644 jerry-debugger/jerry_client_rawpacket.py create mode 100644 jerry-ext/debugger/debugger-rp.c diff --git a/docs/07.DEBUGGER.md b/docs/07.DEBUGGER.md index 3c119f7f0..86df1450e 100644 --- a/docs/07.DEBUGGER.md +++ b/docs/07.DEBUGGER.md @@ -69,7 +69,8 @@ debugger can be enabled by calling `jerryx_debugger_after_connect (jerryx_debugger_tcp_create (debug_port) && jerryx_debugger_ws_create ())` after the `jerry_init ()` function. It initializes the debugger and blocks until a client connects. (Custom transport layers may be -implemented and initialized similarly.) +implemented and initialized similarly. Currently, `jerryx_debugger_rp_create()` for +raw packet transport layer is also available.) The resource name provided to `jerry_parse ()` is used by the client to identify the resource name of the source code. This resource name diff --git a/jerry-debugger/jerry_client.py b/jerry-debugger/jerry_client.py index c2ca5083e..831382be3 100755 --- a/jerry-debugger/jerry_client.py +++ b/jerry-debugger/jerry_client.py @@ -250,7 +250,7 @@ def src_check_args(args): def main(): args = jerry_client_main.arguments_parse() - debugger = jerry_client_main.JerryDebugger(args.address) + debugger = jerry_client_main.JerryDebugger(args.address, args.channel) debugger.non_interactive = args.non_interactive logging.debug("Connected to JerryScript on %d port", debugger.port) diff --git a/jerry-debugger/jerry_client_main.py b/jerry-debugger/jerry_client_main.py index d4f1ddb2b..dc16f364f 100644 --- a/jerry-debugger/jerry_client_main.py +++ b/jerry-debugger/jerry_client_main.py @@ -22,6 +22,8 @@ import select import struct import sys from jerry_client_websocket import WebSocket +from jerry_client_rawpacket import RawPacket +from jerry_client_tcp import Socket # Expected debugger protocol version. JERRY_DEBUGGER_VERSION = 8 @@ -134,7 +136,8 @@ def arguments_parse(): help="set exception config, usage 1: [Enable] or 0: [Disable]") parser.add_argument("--client-source", action="store", default=[], type=str, nargs="+", help="specify a javascript source file to execute") - + parser.add_argument("--channel", choices=["websocket", "rawpacket"], default="websocket", + help="specify the communication channel (default: %(default)s)") args = parser.parse_args() if args.verbose: @@ -262,8 +265,8 @@ class DebuggerAction(object): class JerryDebugger(object): - # pylint: disable=too-many-instance-attributes,too-many-statements,too-many-public-methods,no-self-use - def __init__(self, address): + # pylint: disable=too-many-instance-attributes,too-many-statements,too-many-public-methods,no-self-use,redefined-variable-type + def __init__(self, address, channel): if ":" not in address: self.host = address @@ -301,8 +304,15 @@ class JerryDebugger(object): self.non_interactive = False self.current_out = b"" self.current_log = b"" + self.channel = None - self.channel = WebSocket(address=(self.host, self.port)) + protocol = Socket() + if channel == "websocket": + self.channel = WebSocket(address=(self.host, self.port), protocol=protocol) + elif channel == "rawpacket": + self.channel = RawPacket(address=(self.host, self.port), protocol=protocol) + else: + raise Exception("Unsupported communication channel") config_size = 8 # The server will send the configuration message after connection established @@ -342,7 +352,8 @@ class JerryDebugger(object): logging.debug("Compressed pointer size: %d", self.cp_size) def __del__(self): - self.channel.close() + if self.channel is not None: + self.channel.close() def _exec_command(self, command_id): self.send_command(command_id) diff --git a/jerry-debugger/jerry_client_rawpacket.py b/jerry-debugger/jerry_client_rawpacket.py new file mode 100644 index 000000000..b90fa8a63 --- /dev/null +++ b/jerry-debugger/jerry_client_rawpacket.py @@ -0,0 +1,91 @@ +#!/usr/bin/env python + +# Copyright JS Foundation and other contributors, http://js.foundation +# +# 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. + +import struct + +MAX_BUFFER_SIZE = 256 + +class RawPacket(object): + """ Simplified transmission layer. """ + def __init__(self, address, protocol): + self.protocol = protocol + self.data_buffer = b"" + self.address = address + + def connect(self, config_size): + """ Create connection. """ + self.protocol.connect(self.address) + self.data_buffer = b"" + + # It will return with the Network configurations, which has the following struct: + # header [1] - size[1] + # configuration [config_size] + len_expected = config_size + 1 + + while len(self.data_buffer) < len_expected: + self.data_buffer += self.protocol.receive_data() + + expected = struct.pack("B", config_size) + + if self.data_buffer[0:1] != expected: + raise Exception("Unexpected configuration") + + result = self.data_buffer[1:len_expected] + self.data_buffer = self.data_buffer[len_expected:] + + return result + + def close(self): + """ Close connection. """ + self.protocol.close() + + def send_message(self, _, data): + """ Send message. """ + msg_size = len(data) + + while msg_size > 0: + bytes_send = self.protocol.send_data(data) + if bytes_send < msg_size: + data = data[bytes_send:] + msg_size -= bytes_send + + def get_message(self, blocking): + """ Receive message. """ + + # Connection was closed + if self.data_buffer is None: + return None + + while True: + if len(self.data_buffer) >= 1: + size = ord(self.data_buffer[0]) + if size == 0: + raise Exception("Unexpected data frame") + + if len(self.data_buffer) >= size + 1: + result = self.data_buffer[1:size + 1] + self.data_buffer = self.data_buffer[size + 1:] + return result + + if not blocking and not self.protocol.ready(): + return b'' + + received_data = self.protocol.receive_data(MAX_BUFFER_SIZE) + + if not received_data: + return None + + self.data_buffer += received_data diff --git a/jerry-debugger/jerry_client_websocket.py b/jerry-debugger/jerry_client_websocket.py index 421854034..1e84e55e3 100644 --- a/jerry-debugger/jerry_client_websocket.py +++ b/jerry-debugger/jerry_client_websocket.py @@ -15,14 +15,13 @@ # limitations under the License. import struct -from jerry_client_tcp import Socket MAX_BUFFER_SIZE = 128 WEBSOCKET_BINARY_FRAME = 2 WEBSOCKET_FIN_BIT = 0x80 class WebSocket(object): - def __init__(self, address, protocol=Socket()): + def __init__(self, address, protocol): self.data_buffer = b"" self.protocol = protocol diff --git a/jerry-ext/debugger/debugger-rp.c b/jerry-ext/debugger/debugger-rp.c new file mode 100644 index 000000000..9edcf9d2b --- /dev/null +++ b/jerry-ext/debugger/debugger-rp.c @@ -0,0 +1,159 @@ +/* Copyright JS Foundation and other contributors, http://js.foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "jerryscript-ext/debugger.h" +#include "jext-common.h" + +#ifdef JERRY_DEBUGGER + +/* A simplified transmission layer. */ + +/** + * Size of the raw packet header. + */ +#define JERRYX_DEBUGGER_RAWPACKET_HEADER_SIZE 1 +/** + * Maximum message size with 1 byte size field. + */ +#define JERRYX_DEBUGGER_RAWPACKET_ONE_BYTE_LEN_MAX 255 + +/** + * Header for incoming packets. + */ +typedef struct +{ + uint8_t size; /**< size of the message */ +} jerryx_rawpacket_receive_header_t; + +/** + * Close a tcp connection. + */ +static void +jerryx_debugger_rp_close (jerry_debugger_transport_header_t *header_p) /**< header for the transport interface */ +{ + JERRYX_ASSERT (!jerry_debugger_transport_is_connected ()); + + jerry_heap_free ((void *) header_p, sizeof (jerry_debugger_transport_header_t)); +} /* jerryx_debugger_rp_close */ + +/** + * Send data over a simple raw packet connection. + * + * @return true - if the data has been sent successfully + * false - otherwise + */ +static bool +jerryx_debugger_rp_send (jerry_debugger_transport_header_t *header_p, /**< header for the transport interface */ + uint8_t *message_p, /**< message to be sent */ + size_t message_length) /**< message length in bytes */ +{ + JERRYX_ASSERT (message_length <= JERRYX_DEBUGGER_RAWPACKET_ONE_BYTE_LEN_MAX); + + message_p[-1] = (uint8_t) message_length; + + return header_p->next_p->send (header_p->next_p, message_p - 1, message_length + 1); +} /* jerryx_debugger_rp_send */ + +/** + * Receive data from a rawpacket connection. + * + * @return true - if data has been received successfully + * false - otherwise + */ +static bool +jerryx_debugger_rp_receive (jerry_debugger_transport_header_t *header_p, /**< header for the transport interface */ + jerry_debugger_transport_receive_context_t *receive_context_p) /**< receive context */ +{ + if (!header_p->next_p->receive (header_p->next_p, receive_context_p)) + { + return false; + } + + if (receive_context_p->message_p == NULL) + { + return true; + } + + JERRYX_ASSERT (receive_context_p->message_length >= sizeof (jerryx_rawpacket_receive_header_t)); + + uint8_t *message_p = receive_context_p->message_p; + size_t message_length = (size_t) (message_p[0]); + + size_t message_total_length = receive_context_p->message_total_length; + + if (message_total_length == 0) + { + size_t new_total_length = message_length + sizeof (jerryx_rawpacket_receive_header_t); + receive_context_p->message_total_length = new_total_length; + } + else + { + /* Datagram packet. */ + JERRYX_ASSERT (receive_context_p->message_length == (message_length + sizeof (jerryx_rawpacket_receive_header_t))); + } + + message_p += sizeof (jerryx_rawpacket_receive_header_t); + + receive_context_p->message_p = message_p; + receive_context_p->message_length = message_length; + + return true; +} /* jerryx_debugger_rp_receive */ + +/** + * Initialize a simple raw packet transmission layer. + * + * @return true - if the connection succeeded + * false - otherwise + */ +bool +jerryx_debugger_rp_create (void) +{ + const size_t interface_size = sizeof (jerry_debugger_transport_header_t); + jerry_debugger_transport_header_t *header_p; + header_p = (jerry_debugger_transport_header_t *) jerry_heap_alloc (interface_size); + + if (!header_p) + { + return false; + } + + header_p->close = jerryx_debugger_rp_close; + header_p->send = jerryx_debugger_rp_send; + header_p->receive = jerryx_debugger_rp_receive; + + jerry_debugger_transport_add (header_p, + JERRYX_DEBUGGER_RAWPACKET_HEADER_SIZE, + JERRYX_DEBUGGER_RAWPACKET_ONE_BYTE_LEN_MAX, + JERRYX_DEBUGGER_RAWPACKET_HEADER_SIZE, + JERRYX_DEBUGGER_RAWPACKET_ONE_BYTE_LEN_MAX); + + return true; +} /* jerryx_debugger_rp_create */ + +#else /* !JERRY_DEBUGGER */ + +/** + * Dummy function when debugger is disabled. + * + * @return false + */ +bool +jerryx_debugger_rp_create (void) +{ + return false; +} /* jerryx_debugger_rp_create */ + +#endif /* JERRY_DEBUGGER */ diff --git a/jerry-ext/include/jerryscript-ext/debugger.h b/jerry-ext/include/jerryscript-ext/debugger.h index dd6cf93af..c9fbc7e89 100644 --- a/jerry-ext/include/jerryscript-ext/debugger.h +++ b/jerry-ext/include/jerryscript-ext/debugger.h @@ -35,6 +35,7 @@ bool jerryx_debugger_tcp_create (uint16_t port); * Message encoding interfaces. */ bool jerryx_debugger_ws_create (void); +bool jerryx_debugger_rp_create (void); #ifdef __cplusplus } diff --git a/jerry-main/main-unix.c b/jerry-main/main-unix.c index dafbf4b33..8c0b5156d 100644 --- a/jerry-main/main-unix.c +++ b/jerry-main/main-unix.c @@ -317,6 +317,7 @@ typedef enum OPT_SHOW_RE_OP, OPT_DEBUG_SERVER, OPT_DEBUG_PORT, + OPT_DEBUG_CHANNEL, OPT_DEBUGGER_WAIT_SOURCE, OPT_EXEC_SNAP, OPT_EXEC_SNAP_FUNC, @@ -346,6 +347,8 @@ static const cli_opt_t main_opts[] = .help = "start debug server and wait for a connecting client"), CLI_OPT_DEF (.id = OPT_DEBUG_PORT, .longopt = "debug-port", .meta = "NUM", .help = "debug server port (default: 5001)"), + CLI_OPT_DEF (.id = OPT_DEBUG_CHANNEL, .longopt = "debug-channel", .meta = "[websocket|rawpacket]", + .help = "Specify the debugger transmission channel (default: websocket)"), CLI_OPT_DEF (.id = OPT_DEBUGGER_WAIT_SOURCE, .longopt = "debugger-wait-source", .help = "wait for an executable source from the client"), CLI_OPT_DEF (.id = OPT_EXEC_SNAP, .longopt = "exec-snapshot", .meta = "FILE", @@ -418,14 +421,23 @@ context_alloc (size_t size, */ static void init_engine (jerry_init_flag_t flags, /**< initialized flags for the engine */ - bool debug_server, /**< enable the debugger init or not */ + char *debug_channel, /**< enable the debugger init or not */ uint16_t debug_port) /**< the debugger port */ { jerry_init (flags); - if (debug_server) + if (strcmp (debug_channel, "")) { - jerryx_debugger_after_connect (jerryx_debugger_tcp_create (debug_port) - && jerryx_debugger_ws_create ()); + bool tcp_created = jerryx_debugger_tcp_create (debug_port); + + if (!strcmp (debug_channel, "rawpacket")) + { + jerryx_debugger_after_connect (tcp_created && jerryx_debugger_rp_create ()); + } + else + { + assert (!strcmp (debug_channel, "websocket")); + jerryx_debugger_after_connect (tcp_created && jerryx_debugger_ws_create ()); + } } register_js_function ("assert", jerryx_handler_assert); @@ -451,6 +463,7 @@ main (int argc, bool start_debug_server = false; uint16_t debug_port = 5001; + char *debug_channel = "websocket"; bool is_repl_mode = false; bool is_wait_mode = false; @@ -519,6 +532,16 @@ main (int argc, } break; } + case OPT_DEBUG_CHANNEL: + { + if (check_feature (JERRY_FEATURE_DEBUGGER, cli_state.arg)) + { + debug_channel = (char *) cli_consume_string (&cli_state); + check_usage (!strcmp (debug_channel, "websocket") || !strcmp (debug_channel, "rawpacket"), + argv[0], "Error: invalid value for --debug-channel: ", cli_state.arg); + } + break; + } case OPT_DEBUGGER_WAIT_SOURCE: { if (check_feature (JERRY_FEATURE_DEBUGGER, cli_state.arg)) @@ -612,7 +635,12 @@ main (int argc, #endif /* JERRY_ENABLE_EXTERNAL_CONTEXT */ - init_engine (flags, start_debug_server, debug_port); + if (!start_debug_server) + { + debug_channel = ""; + } + + init_engine (flags, debug_channel, debug_port); jerry_value_t ret_value = jerry_create_undefined (); @@ -730,7 +758,7 @@ main (int argc, break; } - init_engine (flags, true, debug_port); + init_engine (flags, debug_channel, debug_port); ret_value = jerry_create_undefined (); } @@ -774,7 +802,7 @@ main (int argc, jerry_cleanup (); - init_engine (flags, true, debug_port); + init_engine (flags, debug_channel, debug_port); ret_value = jerry_create_undefined (); } diff --git a/tools/run-tests.py b/tools/run-tests.py index 33cc1674a..877cbacf4 100755 --- a/tools/run-tests.py +++ b/tools/run-tests.py @@ -326,21 +326,23 @@ def run_jerry_debugger_tests(options): if ret_build: break - for test_file in os.listdir(settings.DEBUGGER_TESTS_DIR): - if test_file.endswith(".cmd"): - test_case, _ = os.path.splitext(test_file) - test_case_path = os.path.join(settings.DEBUGGER_TESTS_DIR, test_case) - test_cmd = [ - settings.DEBUGGER_TEST_RUNNER_SCRIPT, - get_binary_path(build_dir_path), - settings.DEBUGGER_CLIENT_SCRIPT, - os.path.relpath(test_case_path, settings.PROJECT_DIR) - ] + for channel in ["websocket", "rawpacket"]: + for test_file in os.listdir(settings.DEBUGGER_TESTS_DIR): + if test_file.endswith(".cmd"): + test_case, _ = os.path.splitext(test_file) + test_case_path = os.path.join(settings.DEBUGGER_TESTS_DIR, test_case) + test_cmd = [ + settings.DEBUGGER_TEST_RUNNER_SCRIPT, + get_binary_path(build_dir_path), + channel, + settings.DEBUGGER_CLIENT_SCRIPT, + os.path.relpath(test_case_path, settings.PROJECT_DIR) + ] - if job.test_args: - test_cmd.extend(job.test_args) + if job.test_args: + test_cmd.extend(job.test_args) - ret_test |= run_check(test_cmd) + ret_test |= run_check(test_cmd) return ret_build | ret_test diff --git a/tools/runners/run-debugger-test.sh b/tools/runners/run-debugger-test.sh index 2ccbe861b..48f437a0e 100755 --- a/tools/runners/run-debugger-test.sh +++ b/tools/runners/run-debugger-test.sh @@ -15,8 +15,9 @@ # limitations under the License. JERRY=$1 -DEBUGGER_CLIENT=$2 -TEST_CASE=$3 +CHANNEL=$2 +DEBUGGER_CLIENT=$3 +TEST_CASE=$4 CLIENT_ARGS="" TERM_NORMAL='\033[0m' @@ -24,14 +25,14 @@ TERM_RED='\033[1;31m' TERM_GREEN='\033[1;32m' if [[ $TEST_CASE == *"client_source"* ]]; then - START_DEBUG_SERVER="${JERRY} --start-debug-server --debugger-wait-source &" + START_DEBUG_SERVER="${JERRY} --start-debug-server --debug-channel ${CHANNEL} --debugger-wait-source &" if [[ $TEST_CASE == *"client_source_multiple"* ]]; then CLIENT_ARGS="--client-source ${TEST_CASE}_2.js ${TEST_CASE}_1.js" else CLIENT_ARGS="--client-source ${TEST_CASE}.js" fi else - START_DEBUG_SERVER="${JERRY} ${TEST_CASE}.js --start-debug-server &" + START_DEBUG_SERVER="${JERRY} ${TEST_CASE}.js --start-debug-server --debug-channel ${CHANNEL} &" fi echo "$START_DEBUG_SERVER" @@ -40,11 +41,11 @@ sleep 1s RESULT_TEMP=`mktemp ${TEST_CASE}.out.XXXXXXXXXX` -(cat "${TEST_CASE}.cmd" | ${DEBUGGER_CLIENT} --non-interactive ${CLIENT_ARGS}) >${RESULT_TEMP} 2>&1 +(cat "${TEST_CASE}.cmd" | ${DEBUGGER_CLIENT} --channel ${CHANNEL} --non-interactive ${CLIENT_ARGS}) >${RESULT_TEMP} 2>&1 if [[ $TEST_CASE == *"restart"* ]]; then CONTINUE_CASE=$(sed "s/restart/continue/g" <<< "$TEST_CASE") - (cat "${CONTINUE_CASE}.cmd" | ${DEBUGGER_CLIENT} --non-interactive ${CLIENT_ARGS}) >>${RESULT_TEMP} 2>&1 + (cat "${CONTINUE_CASE}.cmd" | ${DEBUGGER_CLIENT} --channel ${CHANNEL} --non-interactive ${CLIENT_ARGS}) >>${RESULT_TEMP} 2>&1 fi diff -U0 ${TEST_CASE}.expected ${RESULT_TEMP}