diff --git a/.gitignore b/.gitignore index a706da96..d6fa6194 100644 --- a/.gitignore +++ b/.gitignore @@ -48,3 +48,5 @@ bld # MacOS specific files *.DS_Store + +.cache/ diff --git a/CMakeLists.txt b/CMakeLists.txt index 9e1648fd..8187ba70 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -164,6 +164,10 @@ configure_kernel("/share/jupyter/kernels/xc11/") configure_kernel("/share/jupyter/kernels/xc17/") configure_kernel("/share/jupyter/kernels/xc23/") +if(NOT EMSCRIPTEN AND NOT WIN32) + configure_kernel("/share/jupyter/kernels/xcpp17-debugger/") +endif() + # Source files # ============ @@ -178,6 +182,7 @@ set(XEUS_CPP_HEADERS include/xeus-cpp/xmagics.hpp include/xeus-cpp/xoptions.hpp include/xeus-cpp/xpreamble.hpp + include/xeus-cpp/xdebugger.hpp #src/xinspect.hpp #src/xsystem.hpp #src/xparser.hpp @@ -192,6 +197,9 @@ set(XEUS_CPP_SRC src/xparser.cpp src/xutils.cpp src/xmagics/os.cpp + src/xdebugger.cpp + src/xdebuglldb_client.cpp + src/xinternal_utils.cpp ) if(NOT EMSCRIPTEN) diff --git a/include/xeus-cpp/xdebugger.hpp b/include/xeus-cpp/xdebugger.hpp new file mode 100644 index 00000000..46979fae --- /dev/null +++ b/include/xeus-cpp/xdebugger.hpp @@ -0,0 +1,118 @@ +/************************************************************************************ + * Copyright (c) 2023, xeus-cpp contributors * + * * + * Distributed under the terms of the BSD 3-Clause License. * + * * + * The full license is in the file LICENSE, distributed with this software. * + ************************************************************************************/ + +#ifndef XEUS_CPP_XDEBUGGER_HPP +#define XEUS_CPP_XDEBUGGER_HPP + +#ifdef __GNUC__ +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wattributes" +#endif + +#include + +#include "nlohmann/json.hpp" +#include "xeus-cpp/xinterpreter.hpp" +#include "xeus-zmq/xdebugger_base.hpp" +#include "xeus_cpp_config.hpp" + +namespace xcpp +{ + class xdebuglldb_client; + + class XEUS_CPP_API debugger : public xeus::xdebugger_base + { + public: + + using base_type = xeus::xdebugger_base; + + debugger( + xeus::xcontext& context, + const xeus::xconfiguration& config, + const std::string& user_name, + const std::string& session_id, + const nl::json& debugger_config + ); + + virtual ~debugger(); + + private: + + nl::json inspect_variables_request(const nl::json& message); + nl::json stack_trace_request(const nl::json& message); + nl::json attach_request(const nl::json& message); + nl::json configuration_done_request(const nl::json& message); + nl::json set_breakpoints_request(const nl::json& message); + nl::json dump_cell_request(const nl::json& message); + nl::json rich_inspect_variables_request(const nl::json& message); + nl::json copy_to_globals_request(const nl::json& message); + nl::json source_request(const nl::json& message); + + bool get_variable_info_from_lldb( + const std::string& var_name, + int frame_id, + nl::json& data, + nl::json& metadata, + int sequence + ); + void get_container_info( + const std::string& var_name, + int frame_id, + const std::string& var_type, + nl::json& data, + nl::json& metadata, + int sequence, + int size + ); + bool is_container_type(const std::string& type) const; + + bool start_lldb(); + bool start() override; + void stop() override; + xeus::xdebugger_info get_debugger_info() const override; + virtual std::string get_cell_temporary_file(const std::string& code) const override; + + bool connect_to_lldb_tcp(); + std::string send_dap_message(const nl::json& message); + std::string receive_dap_response(); + + xdebuglldb_client* p_debuglldb_client; + std::string m_lldb_host; + std::string m_lldb_port; + nl::json m_debugger_config; + bool m_is_running; + int m_tcp_socket; + bool m_tcp_connected; + pid_t jit_process_pid; + + using breakpoint_list_t = std::map>; + breakpoint_list_t m_breakpoint_list; + + xcpp::interpreter* m_interpreter; + + std::unordered_map> m_hash_to_sequential; + std::unordered_map m_sequential_to_hash; + + bool m_copy_to_globals_available; + }; + + XEUS_CPP_API + std::unique_ptr make_cpp_debugger( + xeus::xcontext& context, + const xeus::xconfiguration& config, + const std::string& user_name, + const std::string& session_id, + const nl::json& debugger_config + ); +} + +#ifdef __GNUC__ +#pragma GCC diagnostic pop +#endif + +#endif \ No newline at end of file diff --git a/include/xeus-cpp/xinterpreter.hpp b/include/xeus-cpp/xinterpreter.hpp index 25eeb049..f35c0f4e 100644 --- a/include/xeus-cpp/xinterpreter.hpp +++ b/include/xeus-cpp/xinterpreter.hpp @@ -15,6 +15,7 @@ #include #include #include +#include #include "clang/Interpreter/CppInterOp.h" // from CppInterOp package @@ -39,6 +40,27 @@ namespace xcpp void publish_stdout(const std::string&); void publish_stderr(const std::string&); + static int32_t get_current_pid(); + + std::vector get_execution_count(const std::string& code) const + { + auto it = m_code_to_execution_count_map.find(code); + if (it != m_code_to_execution_count_map.end()) + { + return it->second; + } + return {}; + } + + std::string get_code_from_execution_count(int execution_count) const + { + auto it = m_execution_count_to_code_map.find(execution_count); + if(it != m_execution_count_to_code_map.end()) + { + return it->second; + } + return ""; + } private: @@ -85,6 +107,9 @@ namespace xcpp xoutput_buffer m_cout_buffer; xoutput_buffer m_cerr_buffer; + + std::map> m_code_to_execution_count_map; + std::map m_execution_count_to_code_map; }; } diff --git a/notebooks/xeus-cpp_output.ipynb b/notebooks/xeus-cpp_output.ipynb new file mode 100644 index 00000000..a179ba03 --- /dev/null +++ b/notebooks/xeus-cpp_output.ipynb @@ -0,0 +1,1202 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "7afcde8b", + "metadata": { + "tags": [ + "papermill-error-cell-tag" + ] + }, + "source": [ + "An Exception was encountered at 'In [4]'." + ] + }, + { + "cell_type": "markdown", + "id": "aa9d4f33-5bd8-42f2-918a-e654d4363577", + "metadata": { + "papermill": { + "duration": 0.012522, + "end_time": "2025-10-30T18:09:12.375607", + "exception": false, + "start_time": "2025-10-30T18:09:12.363085", + "status": "completed" + }, + "tags": [] + }, + "source": [ + "
\n", + "

C++ kernel based on xeus

\n", + "
\n", + "\n", + "A Jupyter kernel for C++ based on the `CppInterOp`, a clang based C++ Interoperability Library and the `xeus` native implementation of the Jupyter protocol, xeus.\n", + "\n", + "- GitHub repository: https://github.com/compiler-research/xeus-cpp\n", + "- Documentation: https://xeus-cpp.readthedocs.io/en/latest/" + ] + }, + { + "cell_type": "markdown", + "id": "9af8aa44-7786-47b6-b94b-8579470acb3a", + "metadata": { + "papermill": { + "duration": 0.005621, + "end_time": "2025-10-30T18:09:12.388519", + "exception": false, + "start_time": "2025-10-30T18:09:12.382898", + "status": "completed" + }, + "tags": [] + }, + "source": [ + "## Usage\n", + "\n", + "
\n", + " \n", + " \n", + "
\n", + " To run the selected code cell, hit
Shift + Enter
\n", + "
\n", + "
" + ] + }, + { + "cell_type": "markdown", + "id": "be1c8c6c-3fbe-4f53-9deb-8496c43d26ad", + "metadata": { + "papermill": { + "duration": 0.005724, + "end_time": "2025-10-30T18:09:12.400387", + "exception": false, + "start_time": "2025-10-30T18:09:12.394663", + "status": "completed" + }, + "tags": [] + }, + "source": [ + "## Output and error streams\n", + "\n", + "`std::cout` and `std::cerr` are redirected to the notebook frontend." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "9bd7f767-6c22-4a1b-a1e2-cd4184fd0367", + "metadata": { + "execution": { + "iopub.execute_input": "2025-10-30T18:09:12.410016Z", + "iopub.status.busy": "2025-10-30T18:09:12.409954Z", + "iopub.status.idle": "2025-10-30T18:09:13.332068Z", + "shell.execute_reply": "2025-10-30T18:09:13.332034Z" + }, + "papermill": { + "duration": 0.928161, + "end_time": "2025-10-30T18:09:13.333430", + "exception": false, + "start_time": "2025-10-30T18:09:12.405269", + "status": "completed" + }, + "tags": [], + "vscode": { + "languageId": "c++" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "some output\n" + ] + } + ], + "source": [ + "#include \n", + "\n", + "std::cout << \"some output\" << std::endl;" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "9622f20f-5925-4544-a97b-aada3a14209a", + "metadata": { + "execution": { + "iopub.execute_input": "2025-10-30T18:09:13.338627Z", + "iopub.status.busy": "2025-10-30T18:09:13.338586Z", + "iopub.status.idle": "2025-10-30T18:09:13.351259Z", + "shell.execute_reply": "2025-10-30T18:09:13.351198Z" + }, + "papermill": { + "duration": 0.01596, + "end_time": "2025-10-30T18:09:13.352040", + "exception": false, + "start_time": "2025-10-30T18:09:13.336080", + "status": "completed" + }, + "tags": [], + "vscode": { + "languageId": "c++" + } + }, + "outputs": [], + "source": [ + "std::cerr << \"some error\" << std::endl;" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "78383629-fd24-488a-be54-20248c34cb9f", + "metadata": { + "execution": { + "iopub.execute_input": "2025-10-30T18:09:13.357482Z", + "iopub.status.busy": "2025-10-30T18:09:13.357440Z", + "iopub.status.idle": "2025-10-30T18:09:13.358090Z", + "shell.execute_reply": "2025-10-30T18:09:13.358060Z" + }, + "papermill": { + "duration": 0.003817, + "end_time": "2025-10-30T18:09:13.358648", + "exception": false, + "start_time": "2025-10-30T18:09:13.354831", + "status": "completed" + }, + "tags": [], + "vscode": { + "languageId": "c++" + } + }, + "outputs": [], + "source": [ + "#include " + ] + }, + { + "cell_type": "markdown", + "id": "e99744a5", + "metadata": { + "tags": [ + "papermill-error-cell-tag" + ] + }, + "source": [ + "Execution using papermill encountered an exception here and stopped:" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "945b7054-ed4d-4270-ae9f-c7696392a5d3", + "metadata": { + "execution": { + "iopub.execute_input": "2025-10-30T18:09:13.363616Z", + "iopub.status.busy": "2025-10-30T18:09:13.363585Z", + "iopub.status.idle": "2025-10-30T18:09:13.367272Z", + "shell.execute_reply": "2025-10-30T18:09:13.367226Z" + }, + "papermill": { + "duration": 0.007177, + "end_time": "2025-10-30T18:09:13.368304", + "exception": true, + "start_time": "2025-10-30T18:09:13.361127", + "status": "failed" + }, + "tags": [], + "vscode": { + "languageId": "c++" + } + }, + "outputs": [ + { + "ename": "Standard Exception: ", + "evalue": "Unknown exception", + "output_type": "error", + "traceback": [ + "Standard Exception: Unknown exception" + ] + } + ], + "source": [ + "throw std::runtime_error(\"Unknown exception\");" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7af0c962-17dc-47d4-9772-b8a06e2bda3a", + "metadata": { + "papermill": { + "duration": null, + "end_time": null, + "exception": null, + "start_time": null, + "status": "pending" + }, + "tags": [], + "vscode": { + "languageId": "c++" + } + }, + "outputs": [], + "source": [ + "int j = 5;\n", + "std::cout << j << std::endl;" + ] + }, + { + "cell_type": "markdown", + "id": "526a7dba-4001-47d5-a116-95423118e100", + "metadata": { + "papermill": { + "duration": null, + "end_time": null, + "exception": null, + "start_time": null, + "status": "pending" + }, + "tags": [] + }, + "source": [ + "# Interpreting the C++ programming language\n", + "\n", + "You can define functions, classes, templates, etc ..." + ] + }, + { + "cell_type": "markdown", + "id": "e5b116ce-ced1-4aa4-b14e-ef7d2606202e", + "metadata": { + "papermill": { + "duration": null, + "end_time": null, + "exception": null, + "start_time": null, + "status": "pending" + }, + "tags": [] + }, + "source": [ + "## Functions" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "86b08f22-e16c-4eac-917d-ae6eeb7ec7cb", + "metadata": { + "papermill": { + "duration": null, + "end_time": null, + "exception": null, + "start_time": null, + "status": "pending" + }, + "tags": [], + "vscode": { + "languageId": "c++" + } + }, + "outputs": [], + "source": [ + "double sqr(double a)\n", + "{\n", + " return a * a;\n", + "}" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5aff6711-54bf-4280-a496-c9f7c683eee5", + "metadata": { + "papermill": { + "duration": null, + "end_time": null, + "exception": null, + "start_time": null, + "status": "pending" + }, + "tags": [], + "vscode": { + "languageId": "c++" + } + }, + "outputs": [], + "source": [ + "double a = 2.5;\n", + "double asqr = sqr(a);\n", + "std::cout << asqr << std::endl;" + ] + }, + { + "cell_type": "markdown", + "id": "5b3959b0-9dc7-41a4-bba1-e20abd0765f7", + "metadata": { + "papermill": { + "duration": null, + "end_time": null, + "exception": null, + "start_time": null, + "status": "pending" + }, + "tags": [] + }, + "source": [ + "## Classes" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d981a53b-8185-49c5-8a30-02453cc1b9e9", + "metadata": { + "papermill": { + "duration": null, + "end_time": null, + "exception": null, + "start_time": null, + "status": "pending" + }, + "tags": [], + "vscode": { + "languageId": "c++" + } + }, + "outputs": [], + "source": [ + "class Foo\n", + "{\n", + "public:\n", + "\n", + " virtual ~Foo() {}\n", + " \n", + " virtual void print(double value) const\n", + " {\n", + " std::cout << \"Foo value = \" << value << std::endl;\n", + " }\n", + "};" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "195d513f-d5cb-4e3d-a6cb-ae99dfcd9aab", + "metadata": { + "papermill": { + "duration": null, + "end_time": null, + "exception": null, + "start_time": null, + "status": "pending" + }, + "tags": [], + "vscode": { + "languageId": "c++" + } + }, + "outputs": [], + "source": [ + "Foo bar;\n", + "bar.print(1.2);" + ] + }, + { + "cell_type": "markdown", + "id": "9ecc1588-cb6e-4676-bb16-b9938e980b06", + "metadata": { + "papermill": { + "duration": null, + "end_time": null, + "exception": null, + "start_time": null, + "status": "pending" + }, + "tags": [] + }, + "source": [ + "## Polymorphism" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4df90bea-5c9e-462e-bd20-d80fd169b7b6", + "metadata": { + "papermill": { + "duration": null, + "end_time": null, + "exception": null, + "start_time": null, + "status": "pending" + }, + "tags": [], + "vscode": { + "languageId": "c++" + } + }, + "outputs": [], + "source": [ + "class Bar : public Foo\n", + "{\n", + "public:\n", + "\n", + " virtual ~Bar() {}\n", + " \n", + " virtual void print(double value) const\n", + " {\n", + " std::cout << \"Bar value = \" << 2 * value << std::endl;\n", + " }\n", + "};" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f7dbbcc2-0f00-48eb-8bb9-94e871afa2a7", + "metadata": { + "papermill": { + "duration": null, + "end_time": null, + "exception": null, + "start_time": null, + "status": "pending" + }, + "tags": [], + "vscode": { + "languageId": "c++" + } + }, + "outputs": [], + "source": [ + "Foo* bar2 = new Bar;\n", + "bar2->print(1.2);\n", + "delete bar2;" + ] + }, + { + "cell_type": "markdown", + "id": "094f4ca7-0aa5-4121-bfff-bf5db1d42c5d", + "metadata": { + "papermill": { + "duration": null, + "end_time": null, + "exception": null, + "start_time": null, + "status": "pending" + }, + "tags": [] + }, + "source": [ + "## Templates" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0df4f3a5-25a1-4548-ba63-54887c770dad", + "metadata": { + "papermill": { + "duration": null, + "end_time": null, + "exception": null, + "start_time": null, + "status": "pending" + }, + "tags": [], + "vscode": { + "languageId": "c++" + } + }, + "outputs": [], + "source": [ + "#include \n", + "\n", + "template \n", + "class FooT\n", + "{\n", + "public:\n", + " \n", + " explicit FooT(const T& t) : m_t(t) {}\n", + " \n", + " void print() const\n", + " {\n", + " std::cout << typeid(T).name() << \" m_t = \" << m_t << std::endl;\n", + " }\n", + " \n", + "private:\n", + " \n", + " T m_t;\n", + "};" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e7bcab70-b9db-409c-aa04-9c64b413e266", + "metadata": { + "papermill": { + "duration": null, + "end_time": null, + "exception": null, + "start_time": null, + "status": "pending" + }, + "tags": [], + "vscode": { + "languageId": "c++" + } + }, + "outputs": [], + "source": [ + "FooT foot(1.2);\n", + "foot.print();" + ] + }, + { + "cell_type": "markdown", + "id": "9c61e669-5bc6-42de-bc9d-cc086d8d4779", + "metadata": { + "papermill": { + "duration": null, + "end_time": null, + "exception": null, + "start_time": null, + "status": "pending" + }, + "tags": [] + }, + "source": [ + "## Documentation\n", + "\n", + " - Documentation for types of the standard library is retrieved on cppreference.com." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "978e592f-301b-4fea-9e12-5636d8b74ecd", + "metadata": { + "papermill": { + "duration": null, + "end_time": null, + "exception": null, + "start_time": null, + "status": "pending" + }, + "tags": [], + "vscode": { + "languageId": "c++" + } + }, + "outputs": [], + "source": [ + "?std::vector" + ] + }, + { + "cell_type": "markdown", + "id": "6c8561b1-23e7-425d-b333-8a47b507e764", + "metadata": { + "papermill": { + "duration": null, + "end_time": null, + "exception": null, + "start_time": null, + "status": "pending" + }, + "tags": [] + }, + "source": [ + "## Using the `display_data` mechanism" + ] + }, + { + "cell_type": "markdown", + "id": "cb1ec203-98b0-40e5-bd78-31e6f40abe11", + "metadata": { + "papermill": { + "duration": null, + "end_time": null, + "exception": null, + "start_time": null, + "status": "pending" + }, + "tags": [] + }, + "source": [ + "For a user-defined type `T`, the rich rendering in the notebook and JupyterLab can be enabled by by implementing the function `nl::json mime_bundle_repr(const T& im)`, which returns the JSON mime bundle for that type." + ] + }, + { + "cell_type": "markdown", + "id": "066fcd12-0f5e-4d8b-bf37-e55a41e85c57", + "metadata": { + "papermill": { + "duration": null, + "end_time": null, + "exception": null, + "start_time": null, + "status": "pending" + }, + "tags": [] + }, + "source": [ + "### Image example" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1a5069c5-9d66-42b4-8de7-3337a6e53460", + "metadata": { + "papermill": { + "duration": null, + "end_time": null, + "exception": null, + "start_time": null, + "status": "pending" + }, + "tags": [], + "vscode": { + "languageId": "c++" + } + }, + "outputs": [], + "source": [ + "#include \n", + "#include \n", + "\n", + "#include \"nlohmann/json.hpp\"\n", + "\n", + "#include \"xeus/xbase64.hpp\"\n", + "\n", + "namespace nl = nlohmann;\n", + "\n", + "namespace im\n", + "{\n", + " struct image\n", + " { \n", + " inline image(const std::string& filename)\n", + " {\n", + " std::ifstream fin(filename, std::ios::binary); \n", + " m_buffer << fin.rdbuf();\n", + " }\n", + " \n", + " std::stringstream m_buffer;\n", + " };\n", + " \n", + " nl::json mime_bundle_repr(const image& i)\n", + " {\n", + " auto bundle = nl::json::object();\n", + " bundle[\"image/png\"] = xeus::base64encode(i.m_buffer.str());\n", + " return bundle;\n", + " }\n", + "}" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "24c9b7e1-fbdb-4afc-8c13-ec0bcda595a9", + "metadata": { + "papermill": { + "duration": null, + "end_time": null, + "exception": null, + "start_time": null, + "status": "pending" + }, + "tags": [], + "vscode": { + "languageId": "c++" + } + }, + "outputs": [], + "source": [ + "im::image marie(\"images/marie.png\");" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "de6ba06d-ed19-40b6-b31d-b8782fb81174", + "metadata": { + "papermill": { + "duration": null, + "end_time": null, + "exception": null, + "start_time": null, + "status": "pending" + }, + "tags": [], + "vscode": { + "languageId": "c++" + } + }, + "outputs": [], + "source": [ + "#include \"xcpp/xdisplay.hpp\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8f08d536-1023-4f30-b0e3-9d6b5dfbf1be", + "metadata": { + "papermill": { + "duration": null, + "end_time": null, + "exception": null, + "start_time": null, + "status": "pending" + }, + "tags": [], + "vscode": { + "languageId": "c++" + } + }, + "outputs": [], + "source": [ + "xcpp::display(marie);" + ] + }, + { + "cell_type": "markdown", + "id": "387d6a7a-7ca4-41d6-9809-4040641db338", + "metadata": { + "papermill": { + "duration": null, + "end_time": null, + "exception": null, + "start_time": null, + "status": "pending" + }, + "tags": [] + }, + "source": [ + "### Audio example" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5aab5534-8420-4341-bf59-546d5f24bbed", + "metadata": { + "papermill": { + "duration": null, + "end_time": null, + "exception": null, + "start_time": null, + "status": "pending" + }, + "tags": [], + "vscode": { + "languageId": "c++" + } + }, + "outputs": [], + "source": [ + "#include \n", + "#include \n", + "\n", + "#include \"nlohmann/json.hpp\"\n", + "\n", + "#include \"xeus/xbase64.hpp\"\n", + "\n", + "namespace nl = nlohmann;\n", + "\n", + "namespace au\n", + "{\n", + " struct audio\n", + " { \n", + " inline audio(const std::string& filename)\n", + " {\n", + " std::ifstream fin(filename, std::ios::binary); \n", + " m_buffer << fin.rdbuf();\n", + " }\n", + " \n", + " std::stringstream m_buffer;\n", + " };\n", + " \n", + " nl::json mime_bundle_repr(const audio& a)\n", + " {\n", + " auto bundle = nl::json::object();\n", + " bundle[\"text/html\"] =\n", + " std::string(\"\";\n", + " return bundle;\n", + " }\n", + "}" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0b9e8220-fe5a-498e-93a1-a93e71bda629", + "metadata": { + "papermill": { + "duration": null, + "end_time": null, + "exception": null, + "start_time": null, + "status": "pending" + }, + "tags": [], + "vscode": { + "languageId": "c++" + } + }, + "outputs": [], + "source": [ + "au::audio drums(\"audio/audio.wav\");" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "faee9bb7-22e4-49ad-8483-38cc1a044b19", + "metadata": { + "papermill": { + "duration": null, + "end_time": null, + "exception": null, + "start_time": null, + "status": "pending" + }, + "tags": [], + "vscode": { + "languageId": "c++" + } + }, + "outputs": [], + "source": [ + "xcpp::display(drums);" + ] + }, + { + "cell_type": "markdown", + "id": "d32274d3-61eb-4763-9b10-b3d4db09ae5c", + "metadata": { + "papermill": { + "duration": null, + "end_time": null, + "exception": null, + "start_time": null, + "status": "pending" + }, + "tags": [] + }, + "source": [ + "### Update-display" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5ba5126e-ea3a-4393-84a1-99fac70ae5e4", + "metadata": { + "papermill": { + "duration": null, + "end_time": null, + "exception": null, + "start_time": null, + "status": "pending" + }, + "tags": [], + "vscode": { + "languageId": "c++" + } + }, + "outputs": [], + "source": [ + "#include \n", + "#include \"xcpp/xdisplay.hpp\"\n", + "\n", + "#include \"nlohmann/json.hpp\"\n", + "\n", + "namespace nl = nlohmann;\n", + "\n", + "namespace ht\n", + "{\n", + " struct html\n", + " { \n", + " inline html(const std::string& content)\n", + " {\n", + " m_content = content;\n", + " }\n", + " std::string m_content;\n", + " };\n", + "\n", + " nl::json mime_bundle_repr(const html& a)\n", + " {\n", + " auto bundle = nl::json::object();\n", + " bundle[\"text/html\"] = a.m_content;\n", + " return bundle;\n", + " }\n", + "}\n", + "\n", + "// A blue rectangle\n", + "ht::html rect(R\"(\n", + "
\n", + "Original\n", + "
)\");" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a3de3009-bf70-4adf-9471-900be0bc3d2d", + "metadata": { + "papermill": { + "duration": null, + "end_time": null, + "exception": null, + "start_time": null, + "status": "pending" + }, + "tags": [], + "vscode": { + "languageId": "c++" + } + }, + "outputs": [], + "source": [ + "xcpp::display(rect, \"some_display_id\");" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "629fa3d5-6c5d-40ad-937d-6257ae6c827d", + "metadata": { + "papermill": { + "duration": null, + "end_time": null, + "exception": null, + "start_time": null, + "status": "pending" + }, + "tags": [], + "vscode": { + "languageId": "c++" + } + }, + "outputs": [], + "source": [ + "// Update the rectangle to be red\n", + "rect.m_content = R\"(\n", + "
\n", + "Updated\n", + "
)\";\n", + "\n", + "xcpp::display(rect, \"some_display_id\", true);" + ] + }, + { + "cell_type": "markdown", + "id": "d4828c73-b061-4f8f-9966-f45d570c6567", + "metadata": { + "papermill": { + "duration": null, + "end_time": null, + "exception": null, + "start_time": null, + "status": "pending" + }, + "tags": [] + }, + "source": [ + "### Clear output" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8391fa79-f365-4792-b1f6-b2f68d79a3d6", + "metadata": { + "papermill": { + "duration": null, + "end_time": null, + "exception": null, + "start_time": null, + "status": "pending" + }, + "tags": [], + "vscode": { + "languageId": "c++" + } + }, + "outputs": [], + "source": [ + "#include \n", + "#include \n", + "#include \n", + "\n", + "#include \"xcpp/xdisplay.hpp\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "16ab8930-6639-4c8d-855f-58ec70a62c17", + "metadata": { + "papermill": { + "duration": null, + "end_time": null, + "exception": null, + "start_time": null, + "status": "pending" + }, + "tags": [], + "vscode": { + "languageId": "c++" + } + }, + "outputs": [], + "source": [ + "std::cout << \"hello\" << std::endl;\n", + "std::this_thread::sleep_for(std::chrono::seconds(1));\n", + "xcpp::clear_output(); // will flicker when replacing \"hello\" with \"goodbye\"\n", + "std::this_thread::sleep_for(std::chrono::seconds(1));\n", + "std::cout << \"goodbye\" << std::endl;" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "347331f2-98cf-4111-989c-7e34b3d0a309", + "metadata": { + "papermill": { + "duration": null, + "end_time": null, + "exception": null, + "start_time": null, + "status": "pending" + }, + "tags": [], + "vscode": { + "languageId": "c++" + } + }, + "outputs": [], + "source": [ + "std::cout << \"hello\" << std::endl;\n", + "std::this_thread::sleep_for(std::chrono::seconds(1));\n", + "xcpp::clear_output(true); // prevents flickering\n", + "std::this_thread::sleep_for(std::chrono::seconds(1));\n", + "std::cout << \"goodbye\" << std::endl;" + ] + }, + { + "cell_type": "markdown", + "id": "92b12afd-2dbd-41df-acd0-25171c6dbb0d", + "metadata": { + "papermill": { + "duration": null, + "end_time": null, + "exception": null, + "start_time": null, + "status": "pending" + }, + "tags": [], + "vscode": { + "languageId": "c++" + } + }, + "source": [ + "# Taking input from the user" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e9a85de0-df46-4850-b88a-4b752f29c1a4", + "metadata": { + "papermill": { + "duration": null, + "end_time": null, + "exception": null, + "start_time": null, + "status": "pending" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "std::string name;\n", + "std::cin >> name;" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f95eece8-cd43-443e-8e27-9b901afefb20", + "metadata": { + "papermill": { + "duration": null, + "end_time": null, + "exception": null, + "start_time": null, + "status": "pending" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "std::cout << \"Your name is\" << name;" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "C++20", + "language": "cpp", + "name": "xcpp20" + }, + "language_info": { + "codemirror_mode": "text/x-c++src", + "file_extension": ".cpp", + "mimetype": "text/x-c++src", + "name": "C++", + "version": "" + }, + "papermill": { + "default_parameters": {}, + "duration": 1.526501, + "end_time": "2025-10-30T18:09:13.485553", + "environment_variables": {}, + "exception": true, + "input_path": "Notebooks/xeus-cpp.ipynb", + "output_path": "Notebooks/xeus-cpp_output.ipynb", + "parameters": {}, + "start_time": "2025-10-30T18:09:11.959052", + "version": "2.6.0" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} \ No newline at end of file diff --git a/share/jupyter/kernels/xcpp17-debugger/kernel.json.in b/share/jupyter/kernels/xcpp17-debugger/kernel.json.in new file mode 100644 index 00000000..d9a27220 --- /dev/null +++ b/share/jupyter/kernels/xcpp17-debugger/kernel.json.in @@ -0,0 +1,21 @@ +{ + "display_name": "C++17-Debugger", + "env": { + "PATH":"@XEUS_CPP_PATH@", + "LD_LIBRARY_PATH":"@XEUS_CPP_LD_LIBRARY_PATH@" + }, + "argv": [ + "@XEUS_CPP_KERNELSPEC_PATH@xcpp", + "-f", + "{connection_file}", + "-g", + "-O0", + "--use-oop-jit", + "-resource-dir", "@XEUS_CPP_RESOURCE_DIR@", + "-I", "@XEUS_CPP_INCLUDE_DIR@", + "-std=c++17" + ], + "language": "cpp", + "metadata": {"debugger": true + } +} \ No newline at end of file diff --git a/share/jupyter/kernels/xcpp17-debugger/logo-32x32.png b/share/jupyter/kernels/xcpp17-debugger/logo-32x32.png new file mode 100644 index 00000000..c09c4585 Binary files /dev/null and b/share/jupyter/kernels/xcpp17-debugger/logo-32x32.png differ diff --git a/share/jupyter/kernels/xcpp17-debugger/logo-64x64.png b/share/jupyter/kernels/xcpp17-debugger/logo-64x64.png new file mode 100644 index 00000000..396c2446 Binary files /dev/null and b/share/jupyter/kernels/xcpp17-debugger/logo-64x64.png differ diff --git a/share/jupyter/kernels/xcpp17-debugger/logo-svg.svg b/share/jupyter/kernels/xcpp17-debugger/logo-svg.svg new file mode 100644 index 00000000..5e117077 --- /dev/null +++ b/share/jupyter/kernels/xcpp17-debugger/logo-svg.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + diff --git a/share/jupyter/kernels/xcpp17-debugger/wasm_kernel.json.in b/share/jupyter/kernels/xcpp17-debugger/wasm_kernel.json.in new file mode 100644 index 00000000..fc2a3d61 --- /dev/null +++ b/share/jupyter/kernels/xcpp17-debugger/wasm_kernel.json.in @@ -0,0 +1,17 @@ +{ + "display_name": "C++17", + "argv": [ + "@XEUS_CPP_KERNELSPEC_PATH@xcpp", + "-resource-dir", "/lib/clang/@CPPINTEROP_LLVM_VERSION_MAJOR@", + "-std=c++17", "-mllvm", "-enable-emscripten-cxx-exceptions", + "-mllvm", "-enable-emscripten-sjlj", "-msimd128" + ], + "language": "cpp", + "metadata": { + "debugger": false, + "shared": { + "libxeus.so": "lib/libxeus.so", + "libclangCppInterOp.so": "lib/libclangCppInterOp.so" + } + } +} diff --git a/src/main.cpp b/src/main.cpp index 00d52d28..569d2e8e 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -19,16 +19,19 @@ #include #endif +#include "nlohmann/json.hpp" #include "xeus/xhelper.hpp" #include #include #include "xeus-zmq/xzmq_context.hpp" #include +#include "xeus-zmq/xserver_zmq_split.hpp" #include "xeus-cpp/xeus_cpp_config.hpp" #include "xeus-cpp/xinterpreter.hpp" #include "xeus-cpp/xutils.hpp" +#include "xeus-cpp/xdebugger.hpp" int main(int argc, char* argv[]) { @@ -62,6 +65,12 @@ int main(int argc, char* argv[]) auto interpreter = std::make_unique(argc, argv); std::unique_ptr context = xeus::make_zmq_context(); + nl::json debugger_config; + debugger_config["lldb"]["initCommands"] = { + "settings set plugin.jit-loader.gdb.enable on" + }; + debugger_config["interpreter_ptr"] = reinterpret_cast(interpreter.get()); + if (!file_name.empty()) { xeus::xconfiguration config = xeus::load_configuration(file_name); @@ -72,12 +81,14 @@ int main(int argc, char* argv[]) xeus::get_user_name(), std::move(context), std::move(interpreter), - xeus::make_xserver_default, + xeus::make_xserver_shell_main, xeus::make_in_memory_history_manager(), xeus::make_console_logger( xeus::xlogger::msg_type, xeus::make_file_logger(xeus::xlogger::content, "xeus.log") - ) + ), + xcpp::make_cpp_debugger, + debugger_config ); std::clog << "Starting xcpp kernel...\n\n" @@ -94,12 +105,14 @@ int main(int argc, char* argv[]) xeus::get_user_name(), std::move(context), std::move(interpreter), - xeus::make_xserver_default, + xeus::make_xserver_shell_main, xeus::make_in_memory_history_manager(), xeus::make_console_logger( xeus::xlogger::msg_type, xeus::make_file_logger(xeus::xlogger::content, "xeus.log") - ) + ), + xcpp::make_cpp_debugger, + debugger_config ); std::cout << "Getting config" << std::endl; diff --git a/src/xdebugger.cpp b/src/xdebugger.cpp new file mode 100644 index 00000000..9786d15b --- /dev/null +++ b/src/xdebugger.cpp @@ -0,0 +1,723 @@ +/************************************************************************************ + * Copyright (c) 2025, xeus-cpp contributors * + * * + * Distributed under the terms of the BSD 3-Clause License. * + * * + * The full license is in the file LICENSE, distributed with this software. * + ************************************************************************************/ +#include "xeus-cpp/xdebugger.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "nlohmann/json.hpp" +#include "xdebuglldb_client.hpp" +#include "xeus-cpp/xinterpreter.hpp" +#include "xeus-zmq/xmiddleware.hpp" +#include "xeus/xsystem.hpp" +#include "xinternal_utils.hpp" + +using namespace std::placeholders; + +namespace xcpp +{ + debugger::debugger( + xeus::xcontext& context, + const xeus::xconfiguration& config, + const std::string& user_name, + const std::string& session_id, + const nl::json& debugger_config + ) + : xdebugger_base(context) + , p_debuglldb_client(new xdebuglldb_client( + context, + config, + xeus::get_socket_linger(), + xdap_tcp_configuration(xeus::dap_tcp_type::client, xeus::dap_init_type::parallel, user_name, session_id), + get_event_callback() + )) + , m_lldb_host("127.0.0.1") + , m_lldb_port("") + , m_debugger_config(debugger_config) + , m_interpreter(reinterpret_cast( + debugger_config["interpreter_ptr"].get() + )) + { + // Register request handlers + // std::cout << "DEBUGGER CONSTRUCTOR" << std::endl; + register_request_handler( + "inspectVariables", + std::bind(&debugger::inspect_variables_request, this, _1), + false + ); + register_request_handler("attach", std::bind(&debugger::attach_request, this, _1), true); + register_request_handler( + "configurationDone", + std::bind(&debugger::configuration_done_request, this, _1), + true + ); + register_request_handler("richInspectVariables", std::bind(&debugger::rich_inspect_variables_request, this, _1), true); + register_request_handler("setBreakpoints", std::bind(&debugger::set_breakpoints_request, this, _1), true); + register_request_handler("dumpCell", std::bind(&debugger::dump_cell_request, this, _1), true); + register_request_handler("copyToGlobals", std::bind(&debugger::copy_to_globals_request, this, _1), true); + register_request_handler("stackTrace", std::bind(&debugger::stack_trace_request, this, _1), true); + register_request_handler("source", std::bind(&debugger::source_request, this, _1), true); + } + + debugger::~debugger() + { + delete p_debuglldb_client; + p_debuglldb_client = nullptr; + } + + bool debugger::start_lldb() + { + jit_process_pid = interpreter::get_current_pid(); + m_lldb_port = xeus::find_free_port(100, 9999, 10099); + + if (m_lldb_port.empty()) + { + std::cerr << "Failed to find a free port for LLDB-DAP" << std::endl; + return false; + } + if (std::getenv("XEUS_LOG")) + { + std::ofstream log("xeus.log", std::ios::app); + log << "===== DEBUGGER CONFIG =====\n"; + log << m_debugger_config.dump(4) << '\n'; + } + + std::vector lldb_args = { + "lldb-dap", + "--connection", + "listen://localhost:" + m_lldb_port + }; + + std::string log_dir = xeus::get_temp_directory_path() + "/xcpp_debug_logs_" + + std::to_string(xeus::get_current_pid()); + xeus::create_directory(log_dir); + std::string log_file = log_dir + "/lldb-dap.log"; + pid_t pid = fork(); + if (pid == 0) + { + int fd = open(log_file.c_str(), O_WRONLY | O_CREAT | O_TRUNC, 0644); + if (fd != -1) + { + dup2(fd, STDOUT_FILENO); + dup2(fd, STDERR_FILENO); + close(fd); + } + + int null_fd = open("/dev/null", O_RDONLY); + if (null_fd != -1) + { + dup2(null_fd, STDIN_FILENO); + close(null_fd); + } + std::vector argv; + for (auto& arg : lldb_args) + { + argv.push_back(const_cast(arg.c_str()));; + } + argv.push_back(nullptr); + execvp("lldb-dap", argv.data()); + std::cerr << "Failed to execute lldb-dap" << std::endl; + std::exit(1); + } + else if (pid > 0) + { + int status; + pid_t wait_result = waitpid(pid, &status, WNOHANG); + if (wait_result != 0) + { + std::cerr << "LLDB-DAP process exited prematurely." << std::endl; + return false; + } + m_is_running = true; + m_copy_to_globals_available = true; + return true; + } + else + { + std::cerr << "fork() failed" << std::endl; + return false; + } + } + + bool debugger::start() + { + bool lldb_started = start_lldb(); + if (!lldb_started) + { + std::cerr << "Failed to start LLDB-DAP" << std::endl; + return false; + } + + std::string controller_end_point = xeus::get_controller_end_point("debugger"); + std::string controller_header_end_point = xeus::get_controller_end_point("debugger_header"); + std::string publisher_end_point = xeus::get_publisher_end_point(); + + bind_sockets(controller_header_end_point, controller_end_point); + std::string lldb_endpoint = "tcp://" + m_lldb_host + ":" + m_lldb_port; + std::thread client( + &xdap_tcp_client::start_debugger, + p_debuglldb_client, + lldb_endpoint, + publisher_end_point, + controller_end_point, + controller_header_end_point + ); + client.detach(); + + send_recv_request("REQ"); + std::string tmp_folder = get_tmp_prefix(); + xeus::create_directory(tmp_folder); + return true; + } + + nl::json debugger::attach_request(const nl::json& message) + { + nl::json attach_request = { + {"seq", message["seq"]}, + {"type", "request"}, + {"command", "attach"}, + {"arguments", + { + {"pid", jit_process_pid}, + {"initCommands", {"settings set plugin.jit-loader.gdb.enable on" }} + } + }, + }; + nl::json reply = forward_message(attach_request); + return reply; + } + + nl::json debugger::inspect_variables_request(const nl::json& message) + { + nl::json inspect_request = { + {"seq", message["seq"]}, + {"type", "request"}, + {"command", "variables"}, + {"arguments", {{"variablesReference", 0}}} + }; + nl::json dummy_reply = forward_message(inspect_request); + return dummy_reply; + } + + nl::json debugger::set_breakpoints_request(const nl::json& message) + { + std::string source = message["arguments"]["source"]["path"].get(); + m_breakpoint_list.erase(source); + nl::json bp_json = message["arguments"]["breakpoints"]; + std::vector bp_list(bp_json.begin(), bp_json.end()); + std::clog << "Setting breakpoints in " << source << std::endl; + std::string sequential_source = m_hash_to_sequential[source][0]; + m_breakpoint_list.insert(std::make_pair(std::move(source), std::move(bp_list))); + nl::json mod_message = message; + mod_message["arguments"]["source"]["path"] = sequential_source; + nl::json breakpoint_reply = forward_message(mod_message); + if (breakpoint_reply.contains("body") && breakpoint_reply["body"].contains("breakpoints")) + { + for (auto& bp : breakpoint_reply["body"]["breakpoints"]) + { + if (bp.contains("source") && bp["source"].contains("path")) + { + std::string seq_path = bp["source"]["path"].get(); + if (m_sequential_to_hash.find(seq_path) != m_sequential_to_hash.end()) + { + bp["source"]["path"] = m_sequential_to_hash[seq_path]; + } + } + } + } + return breakpoint_reply; + } + + nl::json debugger::copy_to_globals_request(const nl::json& message) + { + if (!m_copy_to_globals_available) + { + nl::json reply = { + {"type", "response"}, + {"request_seq", message["seq"]}, + {"success", false}, + {"command", message["command"]}, + {"body", "The debugpy version must be greater than or equal 1.6.5 to allow copying a variable to the global scope."} + }; + return reply; + } + + std::string src_var_name = message["arguments"]["srcVariableName"].get(); + std::string dst_var_name = message["arguments"]["dstVariableName"].get(); + int src_frame_id = message["arguments"]["srcFrameId"].get(); + + int seq = message["seq"].get(); + std::string expression = "globals()['" + dst_var_name + "']"; + nl::json request = { + {"type", "request"}, + {"command", "setExpression"}, + {"seq", seq+1}, + {"arguments", { + {"expression", expression}, + {"value", src_var_name}, + {"frameId", src_frame_id} + }} + }; + return forward_message(request); + } + + + nl::json debugger::configuration_done_request(const nl::json& message) + { + nl::json reply = forward_message(message); + return reply; + } + + void debugger::get_container_info(const std::string& var_name, int frame_id, + const std::string& var_type, nl::json& data, nl::json& metadata, int sequence, int size) + { + nl::json container_tree = { + {"type", "container"}, + {"name", var_name}, + {"containerType", var_type}, + {"size", size}, + {"children", nl::json::array()}, + {"expanded", false} + }; + + // Add elements as children + for (int i = 0; i < size; ++i) { + nl::json elem_request = { + {"type", "request"}, + {"command", "evaluate"}, + {"seq", sequence++}, + {"arguments", { + {"expression", var_name + "[" + std::to_string(i) + "]"}, + {"frameId", frame_id}, + {"context", "watch"} + }} + }; + + nl::json elem_reply = forward_message(elem_request); + if (elem_reply["success"].get()) { + std::string elem_value = elem_reply["body"]["result"].get(); + std::string elem_type = elem_reply["body"].contains("type") ? + elem_reply["body"]["type"].get() : "unknown"; + + nl::json child_node = { + {"type", "element"}, + {"name", "[" + std::to_string(i) + "]"}, + {"value", elem_value}, + {"valueType", elem_type}, + {"index", i}, + {"leaf", true} + }; + + // Check if element is also a container + if (is_container_type(elem_type)) { + child_node["leaf"] = false; + child_node["hasChildren"] = true; + int nested_size = 0; + std::smatch match; + std::regex size_regex(R"(size\s*=\s*(\d+))"); + if (std::regex_search(elem_value, match, size_regex) && match.size() > 1) { + nested_size = std::stoi(match[1]); + } + child_node["size"] = nested_size; + } + + container_tree["children"].push_back(child_node); + } + } + data["application/json"] = container_tree; + } + + bool debugger::is_container_type(const std::string& type) const + { + return type.find("std::vector") != std::string::npos || + type.find("std::array") != std::string::npos || + type.find("std::list") != std::string::npos || + type.find("std::deque") != std::string::npos || + type.find("std::map") != std::string::npos || + type.find("std::set") != std::string::npos || + type.find("std::unordered_map") != std::string::npos || + type.find("std::unordered_set") != std::string::npos; + } + + bool debugger::get_variable_info_from_lldb(const std::string& var_name, int frame_id, nl::json& data, nl::json& metadata, int sequence) { + try { + nl::json eval_request = { + {"type", "request"}, + {"command", "evaluate"}, + {"seq", sequence}, + {"arguments", { + {"expression", var_name}, + {"frameId", frame_id}, + {"context", "watch"} + }} + }; + + nl::json eval_reply = forward_message(eval_request); + if (!eval_reply["success"].get()) { + return false; + } + + std::string var_value = eval_reply["body"]["result"].get(); + std::string var_type = eval_reply["body"].contains("type") ? + eval_reply["body"]["type"].get() : "unknown"; + + // Create a unified JSON structure for all variable types + nl::json variable_info = { + {"type", "variable"}, + {"name", var_name}, + {"value", var_value}, + {"valueType", var_type}, + {"frameId", frame_id}, + {"timestamp", std::time(nullptr)} + }; + + if(is_container_type(var_type)) { + // Extract size from var_value, e.g., "size=2" + int size = 0; + std::smatch match; + std::regex size_regex(R"(size\s*=\s*(\d+))"); + if (std::regex_search(var_value, match, size_regex) && match.size() > 1) { + size = std::stoi(match[1]); + } + + variable_info["isContainer"] = true; + variable_info["size"] = size; + variable_info["leaf"] = false; + variable_info["hasChildren"] = size > 0; + + // Get detailed container info + get_container_info(var_name, frame_id, var_type, data, metadata, sequence + 1, size); + + // Add container summary to the main variable info + if (data["application/json"].contains("children")) { + variable_info["children"] = data["application/json"]["children"]; + variable_info["expanded"] = false; + } + } else { + variable_info["isContainer"] = false; + variable_info["leaf"] = true; + variable_info["hasChildren"] = false; + } + + // Store everything in application/json format + data["application/json"] = variable_info; + + // Metadata for the frontend renderer + metadata["application/json"] = { + {"version", "1.0"}, + {"renderer", "variable-inspector"}, + {"expandable", variable_info["hasChildren"]}, + {"rootVariable", var_name}, + {"capabilities", { + {"tree", true}, + {"search", true}, + {"filter", true}, + {"export", true} + }} + }; + + return true; + } catch (const std::exception& e) { + return false; + } + } + + nl::json debugger::rich_inspect_variables_request(const nl::json& message) + { + nl::json reply = { + {"type", "response"}, + {"request_seq", message["seq"]}, + {"success", false}, + {"command", message["command"]} + }; + + std::string var_name; + try { + var_name = message["arguments"]["variableName"].get(); + } catch (const nl::json::exception& e) { + reply["body"] = { + {"data", { + {"application/json", { + {"type", "error"}, + {"message", "Invalid variable name in request"}, + {"details", std::string(e.what())} + }} + }}, + {"metadata", { + {"application/json", { + {"error", true}, + {"errorType", "invalid_request"} + }} + }} + }; + return reply; + } + + int frame_id = 0; + if(message["arguments"].contains("frameId")) { + frame_id = message["arguments"]["frameId"].get(); + } + + nl::json var_data; + nl::json var_metadata; + bool success = false; + + auto stopped_threads = base_type::get_stopped_threads(); + if(!stopped_threads.empty()) { + success = get_variable_info_from_lldb(var_name, frame_id, var_data, var_metadata, message["seq"].get() + 1); + } else { + // When there is no breakpoint hit, return a placeholder structure + var_data["application/json"] = { + {"type", "unavailable"}, + {"name", var_name}, + {"message", "Variable not accessible - no active debugging session"}, + {"suggestions", { + "Set a breakpoint and run the program", + "Ensure the variable is in scope", + "Check if the program is currently stopped" + }} + }; + var_metadata["application/json"] = { + {"available", false}, + {"reason", "no_stopped_threads"} + }; + success = true; + } + + if(success) { + reply["body"] = { + {"data", var_data}, + {"metadata", var_metadata} + }; + reply["success"] = true; + } else { + reply["body"] = { + {"data", { + {"application/json", { + {"type", "error"}, + {"name", var_name}, + {"message", "Variable '" + var_name + "' not found or not accessible"}, + {"frameId", frame_id}, + {"suggestions", { + "Check variable name spelling", + "Ensure variable is in current scope", + "Verify the frame ID is correct" + }} + }} + }}, + {"metadata", { + {"application/json", { + {"error", true}, + {"errorType", "variable_not_found"} + }} + }} + }; + } + return reply; + } + + nl::json debugger::stack_trace_request(const nl::json& message) + { + int requested_thread_id = message["arguments"]["threadId"]; + auto stopped_threads = base_type::get_stopped_threads(); + + nl::json reply = forward_message(message); + + if (!reply.contains("body") || !reply["body"].contains("stackFrames")) { + return reply; + } + + auto& stack_frames = reply["body"]["stackFrames"]; + nl::json filtered_frames = nl::json::array(); + + for (auto& frame : stack_frames) + { + if (frame.contains("source") && frame["source"].contains("path")) + { + std::string path = frame["source"]["path"]; + std::string name = frame["source"]["name"]; + // Check if path is in the format input_line_ + if (name.find("input_line_") != std::string::npos) + { + // Map sequential filename to hash if mapping exists + auto it = m_sequential_to_hash.find(name); + if (it != m_sequential_to_hash.end()) + { + frame["source"]["path"] = it->second; + // Set name to last part of path (filename) + std::string filename = it->second.substr(it->second.find_last_of("/\\") + 1); + frame["source"]["name"] = filename; + } else { + std::string code, hash_file_name; + // Extract execution count number from "input_line_" + int exec_count = -1; + std::string prefix = "input_line_"; + if (name.compare(0, prefix.size(), prefix) == 0) + { + try + { + exec_count = std::stoi(name.substr(prefix.size())); + } + catch (...) + { + exec_count = 0; + } + } + + if (exec_count != -1) { + code = m_interpreter->get_code_from_execution_count(exec_count - 1); + hash_file_name = get_cell_temporary_file(code); + frame["source"]["path"] = hash_file_name; + frame["source"]["name"] = hash_file_name.substr(hash_file_name.find_last_of("/\\") + 1); + // Update mappings if not already present + m_hash_to_sequential[hash_file_name].push_back(name); + m_sequential_to_hash[name] = hash_file_name; + } + } + filtered_frames.push_back(frame); + } + } + } + + reply["body"]["stackFrames"] = filtered_frames; + return reply; + } + + void debugger::stop() + { + // std::cout << "STOP" << std::endl; + std::string controller_end_point = xeus::get_controller_end_point("debugger"); + std::string controller_header_end_point = xeus::get_controller_end_point("debugger_header"); + unbind_sockets(controller_header_end_point, controller_end_point); + } + + xeus::xdebugger_info debugger::get_debugger_info() const + { + return xeus::xdebugger_info( + xeus::get_tmp_hash_seed(), + get_tmp_prefix(), + get_tmp_suffix(), + true, // Supports exceptions + {"C++ Exceptions"}, + true // Supports breakpoints + ); + } + + nl::json debugger::dump_cell_request(const nl::json& message) + { + std::string code; + try + { + code = message["arguments"]["code"].get(); + // // std::cout << "Execution Count is: " << m_interpreter->get_execution_count(code)[0] << std::endl; + } + catch (nl::json::type_error& e) + { + std::clog << e.what() << std::endl; + } + catch (...) + { + std::clog << "XDEBUGGER: Unknown issue" << std::endl; + } + std::string hash_file_name = get_cell_temporary_file(code); + for(auto& exec_count: m_interpreter->get_execution_count(code)) + { + m_hash_to_sequential[hash_file_name].push_back("input_line_" + std::to_string(exec_count + 1)); + m_sequential_to_hash["input_line_" + std::to_string(exec_count + 1)] = hash_file_name; + std::clog << "Mapping: " << hash_file_name << " <-> " << "input_line_" + std::to_string(exec_count + 1) << std::endl; + } + std::fstream fs(hash_file_name, std::ios::in); + if (!fs.is_open()) + { + fs.clear(); + fs.open(hash_file_name, std::ios::out); + fs << code; + } + + nl::json reply = { + {"type", "response"}, + {"request_seq", message["seq"]}, + {"success", true}, + {"command", message["command"]}, + {"body", {{"sourcePath", hash_file_name}}} + }; + return reply; + } + + nl::json debugger::source_request(const nl::json& message) + { + std::string sourcePath; + try + { + sourcePath = message["arguments"]["source"]["path"].get(); + } + catch(nl::json::type_error& e) + { + std::clog << e.what() << std::endl; + } + catch(...) + { + std::clog << "XDEBUGGER: Unknown issue" << std::endl; + } + + std::ifstream ifs(sourcePath, std::ios::in); + if(!ifs.is_open()) + { + nl::json reply = { + {"type", "response"}, + {"request_seq", message["seq"]}, + {"success", false}, + {"command", message["command"]}, + {"message", "source unavailable"}, + {"body", {{}}} + }; + return reply; + } + + std::string content((std::istreambuf_iterator(ifs)), std::istreambuf_iterator()); + + nl::json reply = { + {"type", "response"}, + {"request_seq", message["seq"]}, + {"success", true}, + {"command", message["command"]}, + {"body", { + {"content", content} + }} + }; + return reply; + } + + std::string debugger::get_cell_temporary_file(const std::string& code) const + { + return get_cell_tmp_file(code); + } + + std::unique_ptr make_cpp_debugger( + xeus::xcontext& context, + const xeus::xconfiguration& config, + const std::string& user_name, + const std::string& session_id, + const nl::json& debugger_config + ) + { + return std::unique_ptr( + new debugger(context, config, user_name, session_id, debugger_config) + ); + } +} \ No newline at end of file diff --git a/src/xdebuglldb_client.cpp b/src/xdebuglldb_client.cpp new file mode 100644 index 00000000..9664c7d9 --- /dev/null +++ b/src/xdebuglldb_client.cpp @@ -0,0 +1,65 @@ +/************************************************************************************ + * Copyright (c) 2025, xeus-cpp contributors * + * * + * Distributed under the terms of the BSD 3-Clause License. * + * * + * The full license is in the file LICENSE, distributed with this software. * + ************************************************************************************/ +#include "xdebuglldb_client.hpp" + +#include +#include +#include +#include +#include +#include +#include + +#include "nlohmann/json.hpp" +#include "xeus/xmessage.hpp" +#include +#include + +namespace nl = nlohmann; + +namespace xcpp +{ + xdebuglldb_client::xdebuglldb_client( + xeus::xcontext& context, + const xeus::xconfiguration& config, + int socket_linger, + const xdap_tcp_configuration& dap_config, + const event_callback& cb + ) + : base_type(context, config, socket_linger, dap_config, cb) + { + std::cout << "xdebuglldb_client initialized" << std::endl; + } + + + void xdebuglldb_client::handle_event(nl::json message) + { + forward_event(std::move(message)); + } + + nl::json xdebuglldb_client::get_stack_frames(int thread_id, int seq) + { + nl::json request = { + {"type", "request"}, + {"seq", seq}, + {"command", "stackTrace"}, + {"arguments", {{"threadId", thread_id}}} + }; + + send_dap_request(std::move(request)); + + nl::json reply = wait_for_message( + [](const nl::json& message) + { + return message["type"] == "response" && message["command"] == "stackTrace"; + } + ); + + return reply["body"]["stackFrames"]; + } +} diff --git a/src/xdebuglldb_client.hpp b/src/xdebuglldb_client.hpp new file mode 100644 index 00000000..fb3aea70 --- /dev/null +++ b/src/xdebuglldb_client.hpp @@ -0,0 +1,40 @@ +/************************************************************************************ + * Copyright (c) 2025, xeus-cpp contributors * + * * + * Distributed under the terms of the BSD 3-Clause License. * + * * + * The full license is in the file LICENSE, distributed with this software. * + ************************************************************************************/ +#ifndef XEUS_CPP_DEBUGLLDB_CLIENT_HPP +#define XEUS_CPP_DEBUGLLDB_CLIENT_HPP + +#include "xeus-zmq/xdap_tcp_client.hpp" + +namespace xcpp +{ + using xeus::xdap_tcp_client; + using xeus::xdap_tcp_configuration; + + class xdebuglldb_client : public xdap_tcp_client + { + public: + + using base_type = xdap_tcp_client; + using event_callback = base_type::event_callback; + + xdebuglldb_client(xeus::xcontext& context, + const xeus::xconfiguration& config, + int socket_linger, + const xdap_tcp_configuration& dap_config, + const event_callback& cb); + + virtual ~xdebuglldb_client() = default; + + private: + + void handle_event(nl::json message) override; + nl::json get_stack_frames(int thread_id, int seq); + }; +} + +#endif diff --git a/src/xinternal_utils.cpp b/src/xinternal_utils.cpp new file mode 100644 index 00000000..3b63ae09 --- /dev/null +++ b/src/xinternal_utils.cpp @@ -0,0 +1,34 @@ +/************************************************************************************ + * Copyright (c) 2025, xeus-cpp contributors * + * * + * Distributed under the terms of the BSD 3-Clause License. * + * * + * The full license is in the file LICENSE, distributed with this software. * + ************************************************************************************/ +#include "xinternal_utils.hpp" + +#ifdef _WIN32 +#include +#else +#include +#endif + +namespace xcpp +{ + std::string get_tmp_prefix() + { + return xeus::get_tmp_prefix("xcpp"); + } + + std::string get_tmp_suffix() + { + return ".cpp"; + } + + std::string get_cell_tmp_file(const std::string& content) + { + return xeus::get_cell_tmp_file(get_tmp_prefix(), + content, + get_tmp_suffix()); + } +} diff --git a/src/xinternal_utils.hpp b/src/xinternal_utils.hpp new file mode 100644 index 00000000..9a70b970 --- /dev/null +++ b/src/xinternal_utils.hpp @@ -0,0 +1,23 @@ +/************************************************************************************ + * Copyright (c) 2025, xeus-cpp contributors * + * * + * Distributed under the terms of the BSD 3-Clause License. * + * * + * The full license is in the file LICENSE, distributed with this software. * + ************************************************************************************/ +#ifndef XEUS_CPP_INTERNAL_UTILS_HPP +#define XEUS_CPP_INTERNAL_UTILS_HPP + +#include + +#include "xeus/xsystem.hpp" + +namespace xcpp +{ + // Temporary file utilities for Jupyter cells + std::string get_tmp_prefix(); + std::string get_tmp_suffix(); + std::string get_cell_tmp_file(const std::string& content); +} + +#endif diff --git a/src/xinterpreter.cpp b/src/xinterpreter.cpp index 14972324..e5537166 100644 --- a/src/xinterpreter.cpp +++ b/src/xinterpreter.cpp @@ -24,6 +24,10 @@ #endif #include "xparser.hpp" #include "xsystem.hpp" +#ifndef WIN32 +#include +#include +#endif using Args = std::vector; @@ -73,6 +77,11 @@ namespace xcpp xeus::register_interpreter(this); } + int32_t interpreter::get_current_pid() + { + return Cpp::GetExecutorPID(); + } + static std::string get_stdopt() { // We need to find what's the C++ version the interpreter runs with. @@ -109,7 +118,7 @@ __get_cxx_version () { //NOLINTNEXTLINE (cppcoreguidelines-pro-bounds-pointer-arithmetic) createInterpreter(Args(argv ? argv + 1 : argv, argv + argc)); - m_version = get_stdopt(); + // m_version = get_stdopt(); redirect_output(); init_preamble(); init_magic(); @@ -122,7 +131,7 @@ __get_cxx_version () void interpreter::execute_request_impl( send_reply_callback cb, - int /*execution_count*/, + int execution_count, const std::string& code, xeus::execute_request_config config, nl::json /*user_expressions*/ @@ -164,6 +173,10 @@ __get_cxx_version () std::string err; + m_code_to_execution_count_map[code].push_back(execution_count); + m_execution_count_to_code_map[execution_count] = code; + std::clog << "In [" << execution_count << "]: " << code << std::endl; + // Attempt normal evaluation try { @@ -312,6 +325,7 @@ __get_cxx_version () "\n" " xeus-cpp: a C++ Jupyter kernel - based on Clang-repl\n"; result["banner"] = banner; + result["debugger"] = true; result["language_info"]["name"] = "C++"; result["language_info"]["version"] = m_version; result["language_info"]["mimetype"] = "text/x-c++src"; diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 54869f4a..b34fe468 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -114,7 +114,7 @@ else() INSTALL_RPATH_USE_LINK_PATH TRUE ) - target_link_libraries(test_xeus_cpp xeus-cpp doctest::doctest ${CMAKE_THREAD_LIBS_INIT}) + target_link_libraries(test_xeus_cpp xeus-cpp xeus-zmq doctest::doctest ${CMAKE_THREAD_LIBS_INIT}) target_include_directories(test_xeus_cpp PRIVATE ${XEUS_CPP_INCLUDE_DIR}) add_custom_target(xtest COMMAND test_xeus_cpp DEPENDS test_xeus_cpp) diff --git a/test/test_debugger.py b/test/test_debugger.py new file mode 100644 index 00000000..ebfc78ca --- /dev/null +++ b/test/test_debugger.py @@ -0,0 +1,512 @@ +"""Test xeus-cpp debugger functionality using LLDB""" + +import pytest +import time +from queue import Empty +from subprocess import STDOUT +from jupyter_client import manager +from contextlib import contextmanager +from time import time + +KM: manager.KernelManager = None # type:ignore + +seq = 0 +TIMEOUT=100 +KERNEL_NAME = "xcpp17-debugger" + +def get_reply(kc, msg_id, timeout=TIMEOUT, channel="shell"): + t0 = time() + while True: + get_msg = getattr(kc, f"get_{channel}_msg") + reply = get_msg(timeout=timeout) + if reply["parent_header"]["msg_id"] == msg_id: + break + # Allow debugging ignored replies + print(f"Ignoring reply not to {msg_id}: {reply}") + t1 = time() + timeout -= t1 - t0 + t0 = t1 + return reply + +@contextmanager +def new_kernel(argv=None, kernel_name=KERNEL_NAME): + """Context manager for a new kernel in a subprocess + + Should only be used for tests where the kernel must not be reused. + + Args: + argv: additional command-line arguments + kernel_name: xeus-cpp kernel variant (xcpp11, xcpp14, xcpp17, xcpp20) + + Yields: + kernel_client: connected KernelClient instance + """ + kwargs = {"stderr": STDOUT, "kernel_name": kernel_name} + if argv is not None: + kwargs["extra_arguments"] = argv + with manager.run_kernel(**kwargs) as kc: + yield kc + +# xeus-cpp uses LLDB for debugging +# Test if xeus-cpp kernel with debugger support is available +try: + from jupyter_client import kernelspec + ksm = kernelspec.KernelSpecManager() + available_kernels = ksm.find_kernel_specs() + print(f"Available kernels: {available_kernels}") + HAS_XCPP_DEBUGGER = any(KERNEL_NAME in k for k in available_kernels.keys()) + print(f"xeus-cpp debugger available: {HAS_XCPP_DEBUGGER}") +except Exception: + HAS_XCPP_DEBUGGER = False + + +def send_debug_request(kernel, command, arguments=None, timeout=TIMEOUT): + """Send a debug request and get reply""" + global seq + seq += 1 + + msg = kernel.session.msg( + "debug_request", + { + "type": "request", + "seq": seq, + "command": command, + "arguments": arguments or {}, + }, + ) + + kernel.control_channel.send(msg) + reply = get_reply(kernel, msg["header"]["msg_id"], channel="control", timeout=timeout) + return reply.get("content", {}) + +def send_initialize_request(kc): + """Send Initialize request""" + + reply = send_debug_request( + kc, + "initialize", + { + "clientID": "test-client", + "clientName": "testClient", + "adapterID": "lldb", + "pathFormat": "path", + "linesStartAt1": True, + "columnsStartAt1": True, + "supportsVariableType": True, + "supportsVariablePaging": False, + "supportsRunInTerminalRequest": False, + "locale": "en", + }, + ) + + return reply + + +@pytest.fixture() +def kernel(): + """Create a fresh xeus-cpp kernel""" + with new_kernel(kernel_name=KERNEL_NAME) as kc: + # Give kernel time to fully start + time.sleep(1) + yield kc + + +def test_kernel_starts(): + """Test that kernel can start""" + with new_kernel(kernel_name=KERNEL_NAME) as kc: + msg_id = kc.execute("int test = 1;") + reply = kc.get_shell_msg(timeout=TIMEOUT) + assert reply["content"]["status"] == "ok" + +def test_debug_initialize(): + """Test debugger initialization""" + with new_kernel(kernel_name=KERNEL_NAME) as kc: + reply = send_initialize_request(kc) + assert reply["success"] == True + + +def test_debug_attach(): + """Test debugger attachment using attach method""" + with new_kernel(kernel_name=KERNEL_NAME) as kc: + init_reply = send_initialize_request(kc) + assert init_reply["success"] == True + + attach_reply = send_debug_request(kc, "attach") + assert attach_reply["success"] == True + + +def test_dump_cell(): + """Test dumping cell code""" + with new_kernel(kernel_name=KERNEL_NAME) as kc: + init_reply = send_initialize_request(kc) + assert init_reply["success"] == True + + attach_reply = send_debug_request(kc, "attach") + assert attach_reply["success"] == True + + code = """ +#include +int add(int a, int b) { + return a + b; +} +int result = add(2, 3); +""" + dump_reply = send_debug_request( + kc, + "dumpCell", + {"code": code} + ) + assert dump_reply["success"] == True + + source_path = dump_reply.get("body", {}).get("sourcePath") + assert source_path is not None + +def test_set_breakpoints(): + """Test setting breakpoint (without execution)""" + with new_kernel(kernel_name=KERNEL_NAME) as kc: + init_reply = send_initialize_request(kc) + assert init_reply["success"] == True + + attach_reply = send_debug_request(kc, "attach") + assert attach_reply["success"] == True + + code = """ +#include +int add(int a, int b) { + return a + b; +} +int result = add(2, 3); +""" + kc.execute(code) + + dump_reply = send_debug_request( + kc, + "dumpCell", + {"code": code} + ) + assert dump_reply["success"] == True + + source_path = dump_reply.get("body", {}).get("sourcePath") + assert source_path is not None + + bp_reply = send_debug_request( + kc, + "setBreakpoints", + { + "source": {"path": source_path}, + "breakpoints": [{"line": 2}], + "sourceModified": False, + } + ) + assert bp_reply["success"] == True + +def test_stop_on_breakpoint(): + """Test setting breakpoint and hitting it during execution""" + with new_kernel(kernel_name=KERNEL_NAME) as kc: + init_reply = send_initialize_request(kc) + assert init_reply["success"] == True + + attach_reply = send_debug_request(kc, "attach") + assert attach_reply["success"] == True + + code = """ +int divide(int a, int b) { + int result = a / b; + return result; +} +divide(10, 2); + """ + + kc.execute(code) + + dump_reply = send_debug_request(kc, "dumpCell", {"code": code}) + source_path = dump_reply.get("body", {}).get("sourcePath") + assert source_path is not None + + bp_reply = send_debug_request( + kc, + "setBreakpoints", + { + "source": {"path": source_path}, + "breakpoints": [{"line": 2}], + "sourceModified": False, + } + ) + assert bp_reply["success"] == True + + msg_id = kc.execute("divide(10, 2);") + + msg: dict = {"msg_type": "", "content": {}} + while msg.get("msg_type") != "debug_event" or msg["content"].get("event") != "stopped": + msg = kc.get_iopub_msg(timeout=TIMEOUT) + + assert msg["content"]["body"]["reason"] == "breakpoint" + +def test_step_in(): + """Test stepping into a function""" + with new_kernel(kernel_name=KERNEL_NAME) as kc: + init_reply = send_initialize_request(kc) + assert init_reply["success"] == True + + attach_reply = send_debug_request(kc, "attach") + assert attach_reply["success"] == True + + code = """ +int multiply(int a, int b) { + return a * b; +} +int calculate(int x, int y) { + int product = multiply(x, y); + return product; +} +calculate(5, 3); +""" + kc.execute(code) + + dump_reply = send_debug_request(kc, "dumpCell", {"code": code}) + source_path = dump_reply.get("body", {}).get("sourcePath") + assert source_path is not None + + bp_reply = send_debug_request( + kc, + "setBreakpoints", + { + "source": {"path": source_path}, + "breakpoints": [{"line": 5}], + "sourceModified": False, + } + ) + assert bp_reply["success"] == True + + msg_id = kc.execute("calculate(5, 3);") + + msg: dict = {"msg_type": "", "content": {}} + while msg.get("msg_type") != "debug_event" or msg["content"].get("event") != "stopped": + msg = kc.get_iopub_msg(timeout=TIMEOUT) + + assert msg["content"]["body"]["reason"] == "breakpoint" + + threadId = int(msg["content"]["body"]["threadId"]) + + step_in_reply = send_debug_request(kc, "stepIn", {"threadId": threadId}) + assert step_in_reply["success"] == True + + msg = {"msg_type": "", "content": {}} + while msg.get("msg_type") != "debug_event" or msg["content"].get("event") != "stopped": + msg = kc.get_iopub_msg(timeout=TIMEOUT) + + assert msg["content"]["body"]["reason"] == "step" + +def test_step_out(): + """Test stepping out of a function""" + with new_kernel(kernel_name=KERNEL_NAME) as kc: + init_reply = send_initialize_request(kc) + assert init_reply["success"] == True + + attach_reply = send_debug_request(kc, "attach") + assert attach_reply["success"] == True + + code = """ +int subtract(int a, int b) { + int result = a - b; + return result; +} +int compute(int x, int y) { + int difference = subtract(x, y); + return difference; +} +compute(10, 4); +""" + kc.execute(code) + + dump_reply = send_debug_request(kc, "dumpCell", {"code": code}) + source_path = dump_reply.get("body", {}).get("sourcePath") + assert source_path is not None + + # Set breakpoint inside subtract function + bp_reply = send_debug_request( + kc, + "setBreakpoints", + { + "source": {"path": source_path}, + "breakpoints": [{"line": 2}], + "sourceModified": False, + } + ) + assert bp_reply["success"] == True + + msg_id = kc.execute("compute(10, 4);") + + msg: dict = {"msg_type": "", "content": {}} + while msg.get("msg_type") != "debug_event" or msg["content"].get("event") != "stopped": + msg = kc.get_iopub_msg(timeout=TIMEOUT) + + assert msg["content"]["body"]["reason"] == "breakpoint" + + threadId = int(msg["content"]["body"]["threadId"]) + + step_out_reply = send_debug_request(kc, "stepOut", {"threadId": threadId}) + assert step_out_reply["success"] == True + + msg = {"msg_type": "", "content": {}} + while msg.get("msg_type") != "debug_event" or msg["content"].get("event") != "stopped": + msg = kc.get_iopub_msg(timeout=TIMEOUT) + + assert msg["content"]["body"]["reason"] == "step" + +def test_continue_execution(): + """Test continuing execution after hitting breakpoint""" + with new_kernel(kernel_name=KERNEL_NAME) as kc: + init_reply = send_initialize_request(kc) + assert init_reply["success"] == True + + attach_reply = send_debug_request(kc, "attach") + assert attach_reply["success"] == True + + code = """ +int factorial(int n) { + if (n <= 1) return 1; + return n * factorial(n - 1); +} +int result = factorial(5); +""" + kc.execute(code) + + dump_reply = send_debug_request(kc, "dumpCell", {"code": code}) + source_path = dump_reply.get("body", {}).get("sourcePath") + assert source_path is not None + + bp_reply = send_debug_request( + kc, + "setBreakpoints", + { + "source": {"path": source_path}, + "breakpoints": [{"line": 2}], + "sourceModified": False, + } + ) + assert bp_reply["success"] == True + + msg_id = kc.execute("factorial(5);") + + # Wait for breakpoint hit + msg: dict = {"msg_type": "", "content": {}} + while msg.get("msg_type") != "debug_event" or msg["content"].get("event") != "stopped": + msg = kc.get_iopub_msg(timeout=TIMEOUT) + + assert msg["content"]["body"]["reason"] == "breakpoint" + + threadId = int(msg["content"]["body"]["threadId"]) + + continue_reply = send_debug_request(kc, "continue", {"threadId": threadId}) + assert continue_reply["success"] == True + +def test_stack_trace(): + """Test getting stack trace at breakpoint""" + with new_kernel(kernel_name=KERNEL_NAME) as kc: + init_reply = send_initialize_request(kc) + assert init_reply["success"] == True + + attach_reply = send_debug_request(kc, "attach") + assert attach_reply["success"] == True + + code = """ +int inner(int x) { + return x * 2; +} +int outer(int y) { + return inner(y + 1); +} +outer(5); +""" + kc.execute(code) + + dump_reply = send_debug_request(kc, "dumpCell", {"code": code}) + source_path = dump_reply.get("body", {}).get("sourcePath") + assert source_path is not None + + bp_reply = send_debug_request( + kc, + "setBreakpoints", + { + "source": {"path": source_path}, + "breakpoints": [{"line": 2}], + "sourceModified": False, + } + ) + assert bp_reply["success"] == True + + msg_id = kc.execute("outer(5);") + + msg: dict = {"msg_type": "", "content": {}} + while msg.get("msg_type") != "debug_event" or msg["content"].get("event") != "stopped": + msg = kc.get_iopub_msg(timeout=TIMEOUT) + + assert msg["content"]["body"]["reason"] == "breakpoint" + + threadId = int(msg["content"]["body"]["threadId"]) + + stacktrace_reply = send_debug_request(kc, "stackTrace", {"threadId": threadId}) + assert stacktrace_reply["success"] == True + assert len(stacktrace_reply.get("body", {}).get("stackFrames", [])) > 0 + +def test_inspect_variables(): + """Test inspecting variables at breakpoint""" + with new_kernel(kernel_name=KERNEL_NAME) as kc: + init_reply = send_initialize_request(kc) + assert init_reply["success"] == True + + attach_reply = send_debug_request(kc, "attach") + assert attach_reply["success"] == True + + code = """ +int process(int a, int b) { + int sum = a + b; + int product = a * b; + return sum + product; +} +process(3, 4); +""" + kc.execute(code) + + dump_reply = send_debug_request(kc, "dumpCell", {"code": code}) + source_path = dump_reply.get("body", {}).get("sourcePath") + assert source_path is not None + + bp_reply = send_debug_request( + kc, + "setBreakpoints", + { + "source": {"path": source_path}, + "breakpoints": [{"line": 3}], + "sourceModified": False, + } + ) + assert bp_reply["success"] == True + + msg_id = kc.execute("process(3, 4);") + + # Wait for breakpoint hit + msg: dict = {"msg_type": "", "content": {}} + while msg.get("msg_type") != "debug_event" or msg["content"].get("event") != "stopped": + msg = kc.get_iopub_msg(timeout=TIMEOUT) + + assert msg["content"]["body"]["reason"] == "breakpoint" + + threadId = int(msg["content"]["body"]["threadId"]) + + # Get scopes + stacktrace_reply = send_debug_request(kc, "stackTrace", {"threadId": threadId}) + frame_id = stacktrace_reply["body"]["stackFrames"][0]["id"] + + scopes_reply = send_debug_request(kc, "scopes", {"frameId": frame_id}) + assert scopes_reply["success"] == True + + # Get variables + if scopes_reply["body"]["scopes"]: + var_ref = scopes_reply["body"]["scopes"][0]["variablesReference"] + vars_reply = send_debug_request(kc, "variables", {"variablesReference": var_ref}) + assert vars_reply["success"] == True + +if __name__ == "__main__": + pytest.main([__file__, "-v", "-s"])