Skip to content

Commit c75a085

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

File tree

7 files changed

+429
-14
lines changed

7 files changed

+429
-14
lines changed

hathor/cli/run_node.py

+29-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:
@@ -227,13 +231,29 @@ def register_signal_handlers(self) -> None:
227231
if sigusr1 is not None:
228232
# USR1 is available in this OS.
229233
signal.signal(sigusr1, self.signal_usr1_handler)
234+
sigquit = getattr(signal, 'SIGQUIT', None)
235+
if sigquit is not None:
236+
# SIGQUIT is available in this OS.
237+
signal.signal(sigquit, self.signal_quit_handler)
230238

231239
def signal_usr1_handler(self, sig: int, frame: Any) -> None:
232240
"""Called when USR1 signal is received."""
233241
self.log.warn('USR1 received. Killing all connections...')
234242
if self.manager and self.manager.connections:
235243
self.manager.connections.disconnect_all_peers(force=True)
236244

245+
def signal_quit_handler(self, sig: int, frame: Any) -> None:
246+
self.log.warn('QUIT received. Exiting...')
247+
if self.manager.is_started:
248+
self.log.info('stop manager')
249+
self.manager.stop()
250+
if self.reactor.running:
251+
self.log.info('stop reactor')
252+
self.reactor.stop()
253+
else:
254+
self.log.warn('reactor already stopped, crashing it')
255+
self.reactor.crash()
256+
237257
def check_unsafe_arguments(self) -> None:
238258
unsafe_args_found = []
239259
for arg_cmdline, arg_test_fn in self.UNSAFE_ARGUMENTS:
@@ -415,7 +435,15 @@ def init_sysctl(self, description: str, sysctl_init_file: Optional[str] = None)
415435
def parse_args(self, argv: list[str]) -> Namespace:
416436
return self.parser.parse_args(argv)
417437

438+
def start_ipykernel(self) -> None:
439+
os.environ['PYDEVD_DISABLE_FILE_VALIDATION'] = '1' # silence ipykernel warning that does not apply
440+
from hathor.ipykernel import embed_kernel
441+
embed_kernel(self.manager, runtime_dir=self._args.data, extra_ns=dict(run_node=self))
442+
418443
def run(self) -> None:
444+
if self._args.x_ipython_kernel:
445+
assert self._args.x_asyncio_reactor
446+
self.start_ipykernel()
419447
self.reactor.run()
420448

421449

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': {
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

+64
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
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+
if runtime_dir is not None:
28+
self.connection_dir = runtime_dir
29+
self.connection_file = 'kernel.json'
30+
self.logging_config: dict[str, Any] = {} # empty out logging config
31+
self.default_extensions: list[str] = [] # disable default extensions
32+
self.no_stderr = True # disable forwarding of stderr (because we use it for logging)
33+
self.log = getLogger('hathor.ipykernel')
34+
35+
# ignore registering of signals
36+
def init_signal(self) -> None:
37+
pass
38+
39+
# custom logging of connection file
40+
def log_connection_info(self) -> None:
41+
self.log.info(f'ipykernel connection enabled at {self.abs_connection_file}')
42+
43+
# disable original logging setup
44+
def get_default_logging_config(self) -> dict[str, Any]:
45+
return {"version": 1, "disable_existing_loggers": False}
46+
47+
def configure_tornado_logger(self) -> None:
48+
pass
49+
50+
def start(self):
51+
self.kernel.start()
52+
53+
54+
def embed_kernel(manager: 'HathorManager', *,
55+
runtime_dir: Optional[str] = None, extra_ns: dict[str, Any] = {}) -> None:
56+
# get the app if it exists, or set it up if it doesn't
57+
if IPKernelApp.initialized():
58+
app = IPKernelApp.instance()
59+
else:
60+
app = IPKernelApp.instance(runtime_dir=runtime_dir)
61+
app.initialize([])
62+
app.kernel.user_ns = dict(manager=manager) | extra_ns
63+
app.shell.set_completer_frame()
64+
app.start()

hathor/manager.py

+5-1
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,7 @@ def __init__(self,
140140
self.reactor = reactor
141141
add_system_event_trigger = getattr(self.reactor, 'addSystemEventTrigger', None)
142142
if add_system_event_trigger is not None:
143-
add_system_event_trigger('after', 'shutdown', self.stop)
143+
add_system_event_trigger('after', 'shutdown', self.try_stop)
144144

145145
self.state: Optional[HathorManager.NodeState] = None
146146
self.profiler: Optional[Any] = None
@@ -315,6 +315,10 @@ def start(self) -> None:
315315
# Start running
316316
self.tx_storage.start_running_manager()
317317

318+
def try_stop(self) -> None:
319+
if self.is_started:
320+
self.stop()
321+
318322
def stop(self) -> Deferred:
319323
if not self.is_started:
320324
raise Exception('HathorManager is already stopped')

0 commit comments

Comments
 (0)