Skip to content

Commit 2410024

Browse files
committed
feat(debug): make it possible to connect a remote ipython shell
1 parent 9b1bbcc commit 2410024

File tree

7 files changed

+435
-13
lines changed

7 files changed

+435
-13
lines changed

hathor/builder/cli_builder.py

+16
Original file line numberDiff line numberDiff line change
@@ -270,6 +270,11 @@ def create_manager(self, reactor: Reactor) -> HathorManager:
270270
cpu_mining_service=cpu_mining_service
271271
)
272272

273+
if self._args.x_ipython_kernel:
274+
self.check_or_raise(self._args.x_asyncio_reactor,
275+
'--x-ipython-kernel must be used with --x-asyncio-reactor')
276+
self._start_ipykernel()
277+
273278
p2p_manager.set_manager(self.manager)
274279

275280
if self._args.stratum:
@@ -376,3 +381,14 @@ def create_wallet(self) -> BaseWallet:
376381
return wallet
377382
else:
378383
raise BuilderError('Invalid type of wallet')
384+
385+
def _start_ipykernel(self) -> None:
386+
# breakpoints are not expected to be used with the embeded ipykernel, to prevent this warning from being
387+
# unnecessarily annoying, PYDEVD_DISABLE_FILE_VALIDATION should be set to 1 before debugpy is imported, or in
388+
# practice, before importing hathor.ipykernel, if for any reason support for breakpoints is needed, the flag
389+
# -Xfrozen_modules=off has to be passed to the python interpreter
390+
# see:
391+
# https://github.com/microsoft/debugpy/blob/main/src/debugpy/_vendored/pydevd/pydevd_file_utils.py#L587-L592
392+
os.environ['PYDEVD_DISABLE_FILE_VALIDATION'] = '1'
393+
from hathor.ipykernel import embed_kernel
394+
embed_kernel(self.manager, runtime_dir=self._args.data, extra_ns=dict(run_node=self))

hathor/cli/run_node.py

+5-1
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,8 @@ class RunNode:
3434
('--x-sync-bridge', lambda args: bool(args.x_sync_bridge)),
3535
('--x-sync-v2-only', lambda args: bool(args.x_sync_v2_only)),
3636
('--x-enable-event-queue', lambda args: bool(args.x_enable_event_queue)),
37-
('--x-asyncio-reactor', lambda args: bool(args.x_asyncio_reactor))
37+
('--x-asyncio-reactor', lambda args: bool(args.x_asyncio_reactor)),
38+
('--x-ipython-kernel', lambda args: bool(args.x_ipython_kernel)),
3839
]
3940

4041
@classmethod
@@ -120,6 +121,9 @@ def create_parser(cls) -> ArgumentParser:
120121
help=f'Signal not support for a feature. One of {possible_features}')
121122
parser.add_argument('--x-asyncio-reactor', action='store_true',
122123
help='Use asyncio reactor instead of Twisted\'s default.')
124+
# XXX: this is temporary, should be added as a sysctl instead before merging
125+
parser.add_argument('--x-ipython-kernel', action='store_true',
126+
help='Launch embedded IPython kernel for remote debugging')
123127
return parser
124128

125129
def prepare(self, *, register_resources: bool = True) -> None:

hathor/cli/run_node_args.py

+1
Original file line numberDiff line numberDiff line change
@@ -73,3 +73,4 @@ class RunNodeArgs(BaseModel, extra=Extra.allow):
7373
signal_support: set[Feature]
7474
signal_not_support: set[Feature]
7575
x_asyncio_reactor: bool
76+
x_ipython_kernel: bool

hathor/cli/util.py

+10-3
Original file line numberDiff line numberDiff line change
@@ -206,18 +206,25 @@ def setup_logging(
206206
'level': 'INFO' if debug else 'WARN',
207207
'propagate': False,
208208
},
209-
'': {
209+
'tornado': { # used by ipykernel's zmq
210210
'handlers': handlers,
211-
'level': 'DEBUG' if debug else 'INFO',
211+
'level': 'INFO' if debug else 'WARN',
212+
'propagate': False,
212213
},
213214
'hathor.p2p.sync_v1': {
214215
'handlers': handlers,
215216
'level': 'DEBUG' if debug_sync else 'INFO',
217+
'propagate': False,
216218
},
217219
'hathor.p2p.sync_v2': {
218220
'handlers': handlers,
219221
'level': 'DEBUG' if debug_sync else 'INFO',
220-
}
222+
'propagate': False,
223+
},
224+
'': {
225+
'handlers': handlers,
226+
'level': 'DEBUG' if debug else 'INFO',
227+
},
221228
}
222229
})
223230

hathor/ipykernel.py

+83
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
# Copyright 2024 Hathor Labs
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
from logging import getLogger
16+
from typing import TYPE_CHECKING, Any, Optional
17+
18+
from ipykernel.kernelapp import IPKernelApp as OriginalIPKernelApp
19+
20+
if TYPE_CHECKING:
21+
from hathor.manager import HathorManager
22+
23+
24+
class IPKernelApp(OriginalIPKernelApp):
25+
def __init__(self, runtime_dir: Optional[str] = None):
26+
super().__init__()
27+
# https://traitlets.readthedocs.io/en/stable/config-api.html#traitlets.config.Application.logging_config
28+
self.logging_config: dict[str, Any] = {} # empty out logging config
29+
# https://traitlets.readthedocs.io/en/stable/config-api.html#traitlets.config.LoggingConfigurable.log
30+
self.log = getLogger('hathor.ipykernel') # use custom name for the logging adapter
31+
if runtime_dir is not None:
32+
# https://ipykernel.readthedocs.io/en/stable/api/ipykernel.html#ipykernel.kernelapp.IPKernelApp.connection_dir
33+
# https://github.com/ipython/ipykernel/blob/main/ipykernel/kernelapp.py#L301-L320
34+
# if not defined now, when init_connection_file is called it will be set to 'kernel-<PID>.json', it is
35+
# defined now because it's more convinient to have a fixed path that doesn't depend on the PID of the
36+
# running process, which doesn't benefit us anyway since the data dir
37+
self.connection_dir = runtime_dir
38+
self.connection_file = 'kernel.json'
39+
# https://ipykernel.readthedocs.io/en/stable/api/ipykernel.html#ipykernel.kernelapp.IPKernelApp.no_stderr
40+
self.no_stderr = True # disable forwarding of stderr (because we use it for logging)
41+
42+
# https://traitlets.readthedocs.io/en/stable/config-api.html#traitlets.config.Application.get_default_logging_config
43+
def get_default_logging_config(self) -> dict[str, Any]:
44+
# XXX: disable original logging setup
45+
return {"version": 1, "disable_existing_loggers": False}
46+
47+
# https://ipykernel.readthedocs.io/en/stable/api/ipykernel.html#ipykernel.kernelapp.IPKernelApp.init_signal
48+
def init_signal(self) -> None:
49+
# XXX: ignore registering of signals
50+
pass
51+
52+
# https://ipykernel.readthedocs.io/en/stable/api/ipykernel.html#ipykernel.kernelapp.IPKernelApp.log_connection_info
53+
def log_connection_info(self) -> None:
54+
# XXX: this method is only used to log this info, we can customize it freely
55+
self.log.info(f'ipykernel connection enabled at {self.abs_connection_file}')
56+
57+
# https://ipykernel.readthedocs.io/en/stable/api/ipykernel.html#ipykernel.kernelapp.IPKernelApp.configure_tornado_logger
58+
def configure_tornado_logger(self) -> None:
59+
# XXX: we already setup tornago logging on hathor.cli.util.setup_logging prevent this class from overriding it
60+
pass
61+
62+
# https://ipykernel.readthedocs.io/en/stable/api/ipykernel.html#ipykernel.kernelapp.IPKernelApp.start
63+
def start(self):
64+
# XXX: custom start to prevent it from running an event loop and capturing KeyboardInterrupt
65+
self.kernel.start()
66+
67+
68+
# https://ipykernel.readthedocs.io/en/stable/api/ipykernel.html#ipykernel.embed.embed_kernel
69+
def embed_kernel(manager: 'HathorManager', *,
70+
runtime_dir: Optional[str] = None, extra_ns: dict[str, Any] = {}) -> None:
71+
""" Customized version of ipykernel.embed.embed_kernel that takes parameters specific to this project.
72+
73+
In theory this method could be called multiple times, like the original ipykernel.embed.embed_kernel.
74+
"""
75+
# get the app if it exists, or set it up if it doesn't
76+
if IPKernelApp.initialized():
77+
app = IPKernelApp.instance()
78+
else:
79+
app = IPKernelApp.instance(runtime_dir=runtime_dir)
80+
app.initialize([])
81+
app.kernel.user_ns = dict(manager=manager) | extra_ns
82+
app.shell.set_completer_frame()
83+
app.start()

0 commit comments

Comments
 (0)