Skip to content

feat(debug): make it possible to connect a remote ipython shell #900

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jan 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions hathor/builder/cli_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,11 @@ def create_manager(self, reactor: Reactor) -> HathorManager:
cpu_mining_service=cpu_mining_service
)

if self._args.x_ipython_kernel:
self.check_or_raise(self._args.x_asyncio_reactor,
'--x-ipython-kernel must be used with --x-asyncio-reactor')
self._start_ipykernel()

p2p_manager.set_manager(self.manager)

if self._args.stratum:
Expand Down Expand Up @@ -376,3 +381,14 @@ def create_wallet(self) -> BaseWallet:
return wallet
else:
raise BuilderError('Invalid type of wallet')

def _start_ipykernel(self) -> None:
# breakpoints are not expected to be used with the embeded ipykernel, to prevent this warning from being
# unnecessarily annoying, PYDEVD_DISABLE_FILE_VALIDATION should be set to 1 before debugpy is imported, or in
# practice, before importing hathor.ipykernel, if for any reason support for breakpoints is needed, the flag
# -Xfrozen_modules=off has to be passed to the python interpreter
# see:
# https://github.com/microsoft/debugpy/blob/main/src/debugpy/_vendored/pydevd/pydevd_file_utils.py#L587-L592
os.environ['PYDEVD_DISABLE_FILE_VALIDATION'] = '1'
from hathor.ipykernel import embed_kernel
embed_kernel(self.manager, runtime_dir=self._args.data, extra_ns=dict(run_node=self))
6 changes: 5 additions & 1 deletion hathor/cli/run_node.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@ class RunNode:
('--x-sync-bridge', lambda args: bool(args.x_sync_bridge)),
('--x-sync-v2-only', lambda args: bool(args.x_sync_v2_only)),
('--x-enable-event-queue', lambda args: bool(args.x_enable_event_queue)),
('--x-asyncio-reactor', lambda args: bool(args.x_asyncio_reactor))
('--x-asyncio-reactor', lambda args: bool(args.x_asyncio_reactor)),
('--x-ipython-kernel', lambda args: bool(args.x_ipython_kernel)),
]

@classmethod
Expand Down Expand Up @@ -120,6 +121,9 @@ def create_parser(cls) -> ArgumentParser:
help=f'Signal not support for a feature. One of {possible_features}')
parser.add_argument('--x-asyncio-reactor', action='store_true',
help='Use asyncio reactor instead of Twisted\'s default.')
# XXX: this is temporary, should be added as a sysctl instead before merging
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are we changing it in this PR?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, but in a future PR. On my initial conversation with @msbrogli we thought it would be better to do it in sysctl, allowing it to be turned on/off. However since turning it in/off requires somewhat more careful management of the IPKernelApp, and since there's a dependency on the asyncio-reactor which is easier to make explicit as a CLI parameter. I went with the current implementation as the initial proof of concept of this feature.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok! Maybe we should update this comment, then? Just to prevent future confusion

parser.add_argument('--x-ipython-kernel', action='store_true',
help='Launch embedded IPython kernel for remote debugging')
return parser

def prepare(self, *, register_resources: bool = True) -> None:
Expand Down
1 change: 1 addition & 0 deletions hathor/cli/run_node_args.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,3 +73,4 @@ class RunNodeArgs(BaseModel, extra=Extra.allow):
signal_support: set[Feature]
signal_not_support: set[Feature]
x_asyncio_reactor: bool
x_ipython_kernel: bool
13 changes: 10 additions & 3 deletions hathor/cli/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -206,18 +206,25 @@ def setup_logging(
'level': 'INFO' if debug else 'WARN',
'propagate': False,
},
'': {
'tornado': { # used by ipykernel's zmq
'handlers': handlers,
'level': 'DEBUG' if debug else 'INFO',
'level': 'INFO' if debug else 'WARN',
'propagate': False,
},
'hathor.p2p.sync_v1': {
'handlers': handlers,
'level': 'DEBUG' if debug_sync else 'INFO',
'propagate': False,
},
'hathor.p2p.sync_v2': {
'handlers': handlers,
'level': 'DEBUG' if debug_sync else 'INFO',
}
'propagate': False,
},
'': {
'handlers': handlers,
'level': 'DEBUG' if debug else 'INFO',
},
}
})

Expand Down
83 changes: 83 additions & 0 deletions hathor/ipykernel.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
# Copyright 2024 Hathor Labs
#
# 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.

from logging import getLogger
from typing import TYPE_CHECKING, Any, Optional

from ipykernel.kernelapp import IPKernelApp as OriginalIPKernelApp

if TYPE_CHECKING:
from hathor.manager import HathorManager


class IPKernelApp(OriginalIPKernelApp):
def __init__(self, runtime_dir: Optional[str] = None):
super().__init__()
# https://traitlets.readthedocs.io/en/stable/config-api.html#traitlets.config.Application.logging_config
self.logging_config: dict[str, Any] = {} # empty out logging config
# https://traitlets.readthedocs.io/en/stable/config-api.html#traitlets.config.LoggingConfigurable.log
self.log = getLogger('hathor.ipykernel') # use custom name for the logging adapter
if runtime_dir is not None:
# https://ipykernel.readthedocs.io/en/stable/api/ipykernel.html#ipykernel.kernelapp.IPKernelApp.connection_dir
# https://github.com/ipython/ipykernel/blob/main/ipykernel/kernelapp.py#L301-L320
# if not defined now, when init_connection_file is called it will be set to 'kernel-<PID>.json', it is
# defined now because it's more convinient to have a fixed path that doesn't depend on the PID of the
# running process, which doesn't benefit us anyway since the data dir
self.connection_dir = runtime_dir
self.connection_file = 'kernel.json'
# https://ipykernel.readthedocs.io/en/stable/api/ipykernel.html#ipykernel.kernelapp.IPKernelApp.no_stderr
self.no_stderr = True # disable forwarding of stderr (because we use it for logging)

# https://traitlets.readthedocs.io/en/stable/config-api.html#traitlets.config.Application.get_default_logging_config
def get_default_logging_config(self) -> dict[str, Any]:
# XXX: disable original logging setup
return {"version": 1, "disable_existing_loggers": False}

# https://ipykernel.readthedocs.io/en/stable/api/ipykernel.html#ipykernel.kernelapp.IPKernelApp.init_signal
def init_signal(self) -> None:
# XXX: ignore registering of signals
pass

# https://ipykernel.readthedocs.io/en/stable/api/ipykernel.html#ipykernel.kernelapp.IPKernelApp.log_connection_info
def log_connection_info(self) -> None:
# XXX: this method is only used to log this info, we can customize it freely
self.log.info(f'ipykernel connection enabled at {self.abs_connection_file}')

# https://ipykernel.readthedocs.io/en/stable/api/ipykernel.html#ipykernel.kernelapp.IPKernelApp.configure_tornado_logger
def configure_tornado_logger(self) -> None:
# XXX: we already setup tornago logging on hathor.cli.util.setup_logging prevent this class from overriding it
pass

# https://ipykernel.readthedocs.io/en/stable/api/ipykernel.html#ipykernel.kernelapp.IPKernelApp.start
def start(self):
# XXX: custom start to prevent it from running an event loop and capturing KeyboardInterrupt
self.kernel.start()


# https://ipykernel.readthedocs.io/en/stable/api/ipykernel.html#ipykernel.embed.embed_kernel
def embed_kernel(manager: 'HathorManager', *,
runtime_dir: Optional[str] = None, extra_ns: dict[str, Any] = {}) -> None:
""" Customized version of ipykernel.embed.embed_kernel that takes parameters specific to this project.

In theory this method could be called multiple times, like the original ipykernel.embed.embed_kernel.
"""
# get the app if it exists, or set it up if it doesn't
if IPKernelApp.initialized():
app = IPKernelApp.instance()
else:
app = IPKernelApp.instance(runtime_dir=runtime_dir)
app.initialize([])
app.kernel.user_ns = dict(manager=manager) | extra_ns
app.shell.set_completer_frame()
app.start()
Loading