hilda 1.4.2__py3-none-any.whl → 2.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
hilda/__main__.py CHANGED
@@ -1,5 +1,9 @@
1
- from hilda.launch_lldb import cli
1
+ from hilda.cli import cli
2
2
 
3
3
 
4
4
  def main():
5
5
  cli()
6
+
7
+
8
+ if __name__ == '__main__':
9
+ main()
hilda/_version.py CHANGED
@@ -12,5 +12,5 @@ __version__: str
12
12
  __version_tuple__: VERSION_TUPLE
13
13
  version_tuple: VERSION_TUPLE
14
14
 
15
- __version__ = version = '1.4.2'
16
- __version_tuple__ = version_tuple = (1, 4, 2)
15
+ __version__ = version = '2.0.0'
16
+ __version_tuple__ = version_tuple = (2, 0, 0)
hilda/cli.py ADDED
@@ -0,0 +1,89 @@
1
+ import logging
2
+ from pathlib import Path
3
+ from typing import List, Mapping, Optional
4
+
5
+ import click
6
+ import coloredlogs
7
+
8
+ from hilda import launch_lldb
9
+ from hilda._version import version
10
+
11
+ DEFAULT_HILDA_PORT = 1234
12
+
13
+ coloredlogs.install(level=logging.DEBUG)
14
+
15
+
16
+ @click.group()
17
+ def cli():
18
+ pass
19
+
20
+
21
+ startup_files_option = click.option('-f', '--startup_files', multiple=True, default=None, help='Files to run on start')
22
+
23
+
24
+ def parse_envp(ctx: click.Context, param: click.Parameter, value: List[str]) -> List[str]:
25
+ env_list = []
26
+ for item in value:
27
+ try:
28
+ key, val = item.split('=', 1)
29
+ env_list.append(f'{key}={val}')
30
+ except ValueError:
31
+ raise click.BadParameter(f'Invalid format for --envp: {item}. Expected KEY=VALUE.')
32
+ return env_list
33
+
34
+
35
+ @cli.command('remote')
36
+ @click.argument('hostname', default='localhost')
37
+ @click.argument('port', type=click.INT, default=DEFAULT_HILDA_PORT)
38
+ @startup_files_option
39
+ def remote(hostname: str, port: int, startup_files: Optional[List[str]] = None) -> None:
40
+ """ Connect to remote debugserver at given address """
41
+ launch_lldb.remote(hostname, port, startup_files)
42
+
43
+
44
+ @cli.command('attach')
45
+ @click.option('-n', '--name', help='process name to attach')
46
+ @click.option('-p', '--pid', type=click.INT, help='pid to attach')
47
+ @startup_files_option
48
+ def attach(name: str, pid: int, startup_files: Optional[List[str]] = None) -> None:
49
+ """ Attach to given process and start a lldb shell """
50
+ launch_lldb.attach(name=name, pid=pid, startup_files=startup_files)
51
+
52
+
53
+ @cli.command('launch')
54
+ @click.argument('exec_path')
55
+ @click.option('--argv', multiple=True, help='Command line arguments to pass to the process')
56
+ @click.option('--envp', multiple=True, callback=parse_envp, help='Environment variables in the form KEY=VALUE')
57
+ @click.option('--stdin', type=Path, help='Redirect stdin from this file path')
58
+ @click.option('--stdout', type=Path, help='Redirect stdout to this file path')
59
+ @click.option('--stderr', type=Path, help='Redirect stderr to this file path')
60
+ @click.option('--cwd', type=Path, help='Set the working directory for the process')
61
+ @click.option('--flags', type=click.INT, default=0, help='Launch flags (bitmask)')
62
+ @click.option('--stop-at-entry', is_flag=True, help='Stop the process at the entry point')
63
+ @startup_files_option
64
+ def launch(exec_path: str, argv: Optional[List] = None, envp: Optional[Mapping] = None,
65
+ stdin: Optional[Path] = None,
66
+ stdout: Optional[Path] = None, stderr: Optional[Path] = None, cwd: Optional[Path] = None,
67
+ flags: Optional[int] = 0, stop_at_entry: Optional[bool] = False,
68
+ startup_files: Optional[List[str]] = None) -> None:
69
+ """ Attach to given process and start a lldb shell """
70
+ if not argv:
71
+ argv = None
72
+ if not envp:
73
+ envp = None
74
+ launch_lldb.launch(exec_path, argv, envp, stdin, stdout, stderr, cwd, flags, stop_at_entry,
75
+ startup_files)
76
+
77
+
78
+ @cli.command('bare')
79
+ def cli_bare():
80
+ """ Just start a lldb shell """
81
+ commands = [f'command script import {Path(__file__).resolve().parent / "lldb_entrypoint.py"}']
82
+ commands = '\n'.join(commands)
83
+ launch_lldb.execute(f'lldb --one-line "{commands}"')
84
+
85
+
86
+ @cli.command('version')
87
+ def cli_version():
88
+ """Show the version information."""
89
+ click.echo(version)
hilda/exceptions.py CHANGED
@@ -1,7 +1,7 @@
1
1
  __all__ = ['HildaException', 'SymbolAbsentError', 'EvaluatingExpressionError', 'CreatingObjectiveCSymbolError',
2
2
  'ConvertingToNsObjectError', 'ConvertingFromNSObjectError', 'DisableJetsamMemoryChecksError',
3
3
  'GettingObjectiveCClassError', 'AccessingRegisterError', 'AccessingMemoryError',
4
- 'BrokenLocalSymbolsJarError', 'AddingLldbSymbolError']
4
+ 'BrokenLocalSymbolsJarError', 'AddingLldbSymbolError', 'LLDBException']
5
5
 
6
6
 
7
7
  class HildaException(Exception):
@@ -9,6 +9,14 @@ class HildaException(Exception):
9
9
  pass
10
10
 
11
11
 
12
+ class LLDBException(Exception):
13
+ """ A domain exception for lldb errors. """
14
+
15
+ def __init__(self, message: str):
16
+ super().__init__()
17
+ self.message = message
18
+
19
+
12
20
  class SymbolAbsentError(HildaException):
13
21
  """ Raise when trying to get a symbol that doesn't exist. """
14
22
  pass
hilda/hilda_client.py CHANGED
@@ -1,4 +1,3 @@
1
- import ast
2
1
  import base64
3
2
  import builtins
4
3
  import importlib
@@ -8,18 +7,19 @@ import logging
8
7
  import os
9
8
  import pickle
10
9
  import struct
10
+ import sys
11
11
  import time
12
12
  import typing
13
13
  from collections import namedtuple
14
14
  from contextlib import contextmanager, suppress
15
+ from dataclasses import dataclass, field
15
16
  from datetime import datetime, timezone
16
- from functools import cached_property
17
+ from functools import cached_property, wraps
17
18
  from pathlib import Path
18
19
  from typing import Any, Callable, List, Optional, Union
19
20
 
20
21
  import hexdump
21
22
  import IPython
22
- import lldb
23
23
  from humanfriendly import prompts
24
24
  from humanfriendly.terminal.html import html_to_ansi
25
25
  from IPython.core.magic import register_line_magic # noqa: F401
@@ -34,7 +34,7 @@ from hilda.common import CfSerializable
34
34
  from hilda.exceptions import AccessingMemoryError, AccessingRegisterError, AddingLldbSymbolError, \
35
35
  BrokenLocalSymbolsJarError, ConvertingFromNSObjectError, ConvertingToNsObjectError, CreatingObjectiveCSymbolError, \
36
36
  DisableJetsamMemoryChecksError, EvaluatingExpressionError, HildaException, SymbolAbsentError
37
- from hilda.launch_lldb import disable_logs # noqa: F401
37
+ from hilda.lldb_importer import lldb
38
38
  from hilda.objective_c_symbol import ObjectiveCSymbol
39
39
  from hilda.registers import Registers
40
40
  from hilda.snippets.mach import CFRunLoopServiceMachPort_hooks
@@ -49,48 +49,77 @@ except ImportError:
49
49
  lldb.KEYSTONE_SUPPORT = False
50
50
  print('failed to import keystone. disabling some features')
51
51
 
52
- with open(os.path.join(Path(__file__).resolve().parent, 'hilda_ascii_art.html'), 'r') as f:
53
- hilda_art = f.read()
52
+ hilda_art = Path(__file__).resolve().parent.joinpath('hilda_ascii_art.html').read_text()
54
53
 
55
54
  GREETING = f"""
56
55
  {hilda_art}
57
56
 
58
57
  <b>Hilda has been successfully loaded! 😎
59
- Use the <span style="color: magenta">p</span> global to access all features.
58
+ Usage:
59
+ <span style="color: magenta">p</span> Global to access all features.
60
+ <span style="color: magenta">F7</span> Step Into.
61
+ <span style="color: magenta">F8</span> Step Over.
62
+ <span style="color: magenta">F9</span> Continue.
63
+ <span style="color: magenta">F10</span> Stop.
64
+
60
65
  Have a nice flight ✈️! Starting an IPython shell...
61
66
  """
62
67
 
63
- MAGIC_FUNCTIONS = """
64
- import shlex
65
- from IPython.core.magic import register_line_magic, needs_local_scope
66
-
67
- @register_line_magic
68
- @needs_local_scope
69
- def objc(line, local_ns=None):
70
- p = local_ns['p']
71
- className = line.strip()
72
- if not className:
73
- p.log_error("Error: className is required.")
74
- return
75
- try:
76
- local_ns[className] = p.objc_get_class(className)
77
- p.log_info(f'{className} class loaded successfully')
78
- except Exception:
79
- p.log_error(f'Error loading class {className}')
80
-
81
-
82
- @register_line_magic
83
- @needs_local_scope
84
- def fbp(line, local_ns=None):
85
- p = local_ns['p']
86
- module_name, address = shlex.split(line.strip())
87
- address = int(address, 16)
88
- p.file_symbol(address, module_name).bp()
89
- """
68
+
69
+ def disable_logs() -> None:
70
+ logging.getLogger('asyncio').disabled = True
71
+ logging.getLogger('parso.cache').disabled = True
72
+ logging.getLogger('parso.cache.pickle').disabled = True
73
+ logging.getLogger('parso.python.diff').disabled = True
74
+ logging.getLogger('humanfriendly.prompts').disabled = True
75
+ logging.getLogger('blib2to3.pgen2.driver').disabled = True
76
+ logging.getLogger('hilda.launch_lldb').disabled = True
77
+
90
78
 
91
79
  SerializableSymbol = namedtuple('SerializableSymbol', 'address type_ filename')
92
80
 
93
81
 
82
+ @dataclass
83
+ class Configs:
84
+ """ Configuration settings for evaluation and monitoring. """
85
+ evaluation_unwind_on_error: bool = field(default=False,
86
+ metadata={'doc': 'Whether to unwind on error during evaluation.'})
87
+ evaluation_ignore_breakpoints: bool = field(default=False,
88
+ metadata={'doc': 'Whether to ignore breakpoints during evaluation.'})
89
+ nsobject_exclusion: bool = field(default=False, metadata={
90
+ 'doc': 'Whether to exclude NSObject during evaluation - reduce ipython autocomplete results.'})
91
+ objc_verbose_monitor: bool = field(default=False, metadata={
92
+ 'doc': 'When set to True, using monitor() will automatically print objc methods arguments.'})
93
+
94
+ def __repr__(self):
95
+ return self.__str__()
96
+
97
+ def __str__(self):
98
+ config_str = 'Configuration settings:\n'
99
+ max_len = max(len(field_name) for field_name in self.__dataclass_fields__) + 2
100
+
101
+ for field_name, field_info in self.__dataclass_fields__.items():
102
+ value = getattr(self, field_name)
103
+ doc = field_info.metadata.get('doc', 'No docstring available')
104
+ config_str += f'\t{field_name.ljust(max_len)}: {str(value).ljust(5)} | {doc}\n'
105
+
106
+ return config_str
107
+
108
+
109
+ def stop_is_needed(func: Callable):
110
+ """Decorator to check if the process must be stopped before proceeding."""
111
+
112
+ @wraps(func)
113
+ def wrapper(self, *args, **kwargs):
114
+ is_running = self.process.GetState() == lldb.eStateRunning
115
+ if is_running:
116
+ self.logger.error(f'Cannot {func.__name__.replace("_", "-")}: Process must be stopped first.')
117
+ return
118
+ return func(self, *args, **kwargs)
119
+
120
+ return wrapper
121
+
122
+
94
123
  class HildaClient:
95
124
  Breakpoint = namedtuple('Breakpoint', 'address options forced callback')
96
125
 
@@ -108,13 +137,7 @@ class HildaClient:
108
137
  self.registers = Registers(self)
109
138
  self.arch = self.target.GetTriple().split('-')[0]
110
139
  self.ui_manager = UiManager(self)
111
- # should unwind the stack on errors. change this to False in order to debug self-made calls
112
- # within hilda
113
- self._evaluation_unwind_on_error = True
114
-
115
- # should ignore breakpoints while evaluation
116
- self._evaluation_ignore_breakpoints = True
117
-
140
+ self.configs = Configs()
118
141
  self._dynamic_env_loaded = False
119
142
  self._symbols_loaded = False
120
143
 
@@ -144,7 +167,7 @@ class HildaClient:
144
167
  # convert FDs into int
145
168
  return {int(k): v for k, v in result.items()}
146
169
 
147
- def bt(self, should_print=True, depth: Optional[int] = None) -> List:
170
+ def bt(self, should_print: bool = False, depth: Optional[int] = None) -> List[Union[str, lldb.SBFrame]]:
148
171
  """ Print an improved backtrace. """
149
172
  backtrace = []
150
173
  for i, frame in enumerate(self.thread.frames):
@@ -251,6 +274,7 @@ class HildaClient:
251
274
  globals()['symbols'] = self.symbols
252
275
  self._symbols_loaded = True
253
276
 
277
+ @stop_is_needed
254
278
  def poke(self, address, buf: bytes):
255
279
  """
256
280
  Write data at given address
@@ -265,6 +289,7 @@ class HildaClient:
265
289
 
266
290
  return retval
267
291
 
292
+ @stop_is_needed
268
293
  def poke_text(self, address: int, code: str) -> int:
269
294
  """
270
295
  Write instructions to address.
@@ -276,6 +301,7 @@ class HildaClient:
276
301
  bytecode, count = self._ks.asm(code, as_bytes=True)
277
302
  return self.poke(address, bytecode)
278
303
 
304
+ @stop_is_needed
279
305
  def peek(self, address, size: int) -> bytes:
280
306
  """
281
307
  Read data at given address
@@ -294,6 +320,7 @@ class HildaClient:
294
320
 
295
321
  return retval
296
322
 
323
+ @stop_is_needed
297
324
  def peek_str(self, address: Symbol) -> str:
298
325
  """
299
326
  Peek a buffer till null termination
@@ -302,7 +329,7 @@ class HildaClient:
302
329
  """
303
330
  return address.po('char *')[1:-1] # strip the ""
304
331
 
305
- def stop(self):
332
+ def stop(self, *args) -> None:
306
333
  """ Stop process. """
307
334
  self.debugger.SetAsync(False)
308
335
 
@@ -313,8 +340,10 @@ class HildaClient:
313
340
 
314
341
  if not self.process.Stop().Success():
315
342
  self.log_critical('failed to stop process')
343
+ else:
344
+ self.log_info('Process Stopped')
316
345
 
317
- def cont(self):
346
+ def cont(self, *args) -> None:
318
347
  """ Continue process. """
319
348
  is_running = self.process.GetState() == lldb.eStateRunning
320
349
 
@@ -328,6 +357,8 @@ class HildaClient:
328
357
 
329
358
  if not self.process.Continue().Success():
330
359
  self.log_critical('failed to continue process')
360
+ else:
361
+ self.log_info('Process Continued')
331
362
 
332
363
  def detach(self):
333
364
  """
@@ -338,8 +369,12 @@ class HildaClient:
338
369
  """
339
370
  if not self.process.Detach().Success():
340
371
  self.log_critical('failed to detach')
372
+ else:
373
+ self.log_info('Process Detached')
341
374
 
342
- def disass(self, address, buf, flavor='intel', should_print=True) -> lldb.SBInstructionList:
375
+ @stop_is_needed
376
+ def disass(self, address: int, buf: bytes, flavor: str = 'intel',
377
+ should_print: bool = False) -> lldb.SBInstructionList:
343
378
  """
344
379
  Print disassembly from a given address
345
380
  :param flavor:
@@ -528,14 +563,16 @@ class HildaClient:
528
563
  self.thread.StepOutOfFrame(self.frame)
529
564
  self._bp_frame = None
530
565
 
531
- def step_into(self):
566
+ @stop_is_needed
567
+ def step_into(self, *args):
532
568
  """ Step into current instruction. """
533
569
  with self.sync_mode():
534
570
  self.thread.StepInto()
535
571
  if self.ui_manager.active:
536
572
  self.ui_manager.show()
537
573
 
538
- def step_over(self):
574
+ @stop_is_needed
575
+ def step_over(self, *args):
539
576
  """ Step over current instruction. """
540
577
  with self.sync_mode():
541
578
  self.thread.StepOver()
@@ -821,9 +858,9 @@ class HildaClient:
821
858
  formatted_expression = str(expression)
822
859
 
823
860
  options = lldb.SBExpressionOptions()
824
- options.SetIgnoreBreakpoints(self._evaluation_ignore_breakpoints)
861
+ options.SetIgnoreBreakpoints(self.configs.evaluation_ignore_breakpoints)
825
862
  options.SetTryAllThreads(True)
826
- options.SetUnwindOnError(self._evaluation_unwind_on_error)
863
+ options.SetUnwindOnError(self.configs.evaluation_unwind_on_error)
827
864
 
828
865
  e = self.frame.EvaluateExpression(formatted_expression, options)
829
866
 
@@ -847,35 +884,6 @@ class HildaClient:
847
884
  spec.loader.exec_module(m)
848
885
  return m
849
886
 
850
- def set_evaluation_unwind(self, value: bool):
851
- """
852
- Set whether LLDB will attempt to unwind the stack whenever an expression evaluation error occurs.
853
-
854
- Use unwind() to restore when an error is raised in this case.
855
- """
856
- self._evaluation_unwind_on_error = value
857
-
858
- def get_evaluation_unwind(self) -> bool:
859
- """
860
- Get evaluation unwind state.
861
-
862
- When this value is True, LLDB will attempt unwinding the stack on evaluation errors.
863
- Otherwise, the stack frame will remain the same on errors to help you investigate the error.
864
- """
865
- return self._evaluation_unwind_on_error
866
-
867
- def set_evaluation_ignore_breakpoints(self, value: bool):
868
- """
869
- Set whether to ignore breakpoints while evaluating expressions
870
- """
871
- self._evaluation_ignore_breakpoints = value
872
-
873
- def get_evaluation_ignore_breakpoints(self) -> bool:
874
- """
875
- Get evaluation "ignore-breakpoints" state.
876
- """
877
- return self._evaluation_ignore_breakpoints
878
-
879
887
  def unwind(self) -> bool:
880
888
  """ Unwind the stack (useful when get_evaluation_unwind() == False) """
881
889
  return self.thread.UnwindInnermostExpression().Success()
@@ -1016,28 +1024,32 @@ class HildaClient:
1016
1024
 
1017
1025
  return value
1018
1026
 
1019
- def interact(self, additional_namespace: typing.Mapping = None) -> None:
1027
+ def interact(self, additional_namespace: Optional[typing.Mapping] = None,
1028
+ startup_files: Optional[List[str]] = None) -> None:
1020
1029
  """ Start an interactive Hilda shell """
1021
1030
  if not self._dynamic_env_loaded:
1022
1031
  self.init_dynamic_environment()
1023
1032
  print('\n')
1024
1033
  self.log_info(html_to_ansi(GREETING))
1034
+ ipython_config = Config()
1035
+ ipython_config.IPCompleter.use_jedi = True
1036
+ ipython_config.BaseIPythonApplication.profile = 'hilda'
1037
+ ipython_config.InteractiveShellApp.extensions = ['hilda.ipython_extensions.magics',
1038
+ 'hilda.ipython_extensions.events',
1039
+ 'hilda.ipython_extensions.keybindings']
1040
+ ipython_config.InteractiveShellApp.exec_lines = ["disable_logs()"]
1041
+ if startup_files is not None:
1042
+ ipython_config.InteractiveShellApp.exec_files = startup_files
1043
+ self.log_debug(f'Startup files - {startup_files}')
1025
1044
 
1026
- config = Config()
1027
- config.IPCompleter.use_jedi = True
1028
- config.InteractiveShellApp.exec_lines = [
1029
- """disable_logs()""",
1030
- """IPython.get_ipython().events.register('pre_run_cell', self._ipython_run_cell_hook)""",
1031
- MAGIC_FUNCTIONS,
1032
- ]
1033
- config.BaseIPythonApplication.profile = 'hilda'
1034
1045
  namespace = globals()
1035
1046
  namespace.update(locals())
1036
1047
  namespace['p'] = self
1037
1048
  if additional_namespace is not None:
1038
1049
  namespace.update(additional_namespace)
1039
-
1040
- IPython.start_ipython(config=config, user_ns=namespace)
1050
+ sys.argv = ['a']
1051
+ IPython.start_ipython(config=ipython_config, user_ns=namespace)
1052
+ self.detach()
1041
1053
 
1042
1054
  @staticmethod
1043
1055
  def _add_global(name: str, value: Any, reserved_names=None):
@@ -1108,35 +1120,6 @@ class HildaClient:
1108
1120
 
1109
1121
  return f'((intptr_t(*)({args_type}))({address}))({args_conv})'
1110
1122
 
1111
- def _ipython_run_cell_hook(self, info):
1112
- """
1113
- Enable lazy loading for symbols
1114
- :param info: IPython's CellInfo object
1115
- """
1116
- if info.raw_cell[0] in ['!', '%'] or info.raw_cell.endswith('?'):
1117
- return
1118
-
1119
- for node in ast.walk(ast.parse(info.raw_cell)):
1120
- if not isinstance(node, ast.Name):
1121
- # we are only interested in names
1122
- continue
1123
-
1124
- if node.id in locals() or node.id in globals() or node.id in dir(builtins):
1125
- # That are undefined
1126
- continue
1127
-
1128
- if not hasattr(SymbolsJar, node.id):
1129
- # ignore SymbolsJar properties
1130
- try:
1131
- symbol = getattr(self.symbols, node.id)
1132
- except SymbolAbsentError:
1133
- pass
1134
- else:
1135
- self._add_global(
1136
- node.id,
1137
- symbol if symbol.type_ != lldb.eSymbolTypeObjCMetaClass else self.objc_get_class(node.id)
1138
- )
1139
-
1140
1123
  @staticmethod
1141
1124
  def _std_string(value):
1142
1125
  if struct.unpack("b", (value + 23).peek(1))[0] >= 0:
@@ -0,0 +1,53 @@
1
+ import ast
2
+ import builtins
3
+
4
+ from IPython.terminal.interactiveshell import TerminalInteractiveShell
5
+
6
+ from hilda.exceptions import EvaluatingExpressionError, SymbolAbsentError
7
+ from hilda.hilda_client import HildaClient
8
+ from hilda.lldb_importer import lldb
9
+ from hilda.symbols_jar import SymbolsJar
10
+
11
+
12
+ class HIEvents:
13
+ def __init__(self, ip: TerminalInteractiveShell):
14
+ self.shell = ip
15
+ self.hilda_client: HildaClient = self.shell.user_ns['p']
16
+
17
+ def pre_run_cell(self, info):
18
+ """
19
+ Enable lazy loading for symbols
20
+ :param info: IPython's CellInfo object
21
+ """
22
+ if info.raw_cell[0] in ['!', '%'] or info.raw_cell.endswith('?'):
23
+ return
24
+ for node in ast.walk(ast.parse(info.raw_cell)):
25
+ if not isinstance(node, ast.Name):
26
+ # we are only interested in names
27
+ continue
28
+
29
+ if node.id in locals() or node.id in globals() or node.id in dir(builtins):
30
+ # That are undefined
31
+ continue
32
+
33
+ if not hasattr(SymbolsJar, node.id):
34
+ # ignore SymbolsJar properties
35
+ try:
36
+ symbol = getattr(self.hilda_client.symbols, node.id)
37
+ except SymbolAbsentError:
38
+ pass
39
+ else:
40
+ try:
41
+ self.hilda_client._add_global(
42
+ node.id,
43
+ symbol if symbol.type_ != lldb.eSymbolTypeObjCMetaClass else self.hilda_client.objc_get_class(
44
+ node.id)
45
+ )
46
+ except EvaluatingExpressionError:
47
+ self.hilda_client.log_warning(
48
+ f'Process is running. Pause execution in order to resolve "{node.id}"')
49
+
50
+
51
+ def load_ipython_extension(ip: TerminalInteractiveShell):
52
+ hie = HIEvents(ip)
53
+ ip.events.register('pre_run_cell', hie.pre_run_cell)
@@ -0,0 +1,22 @@
1
+ from prompt_toolkit.enums import DEFAULT_BUFFER
2
+ from prompt_toolkit.filters import EmacsInsertMode, HasFocus, HasSelection, ViInsertMode
3
+ from prompt_toolkit.keys import Keys
4
+
5
+
6
+ def load_ipython_extension(ipython):
7
+ def register_keybindings():
8
+ hilda = ipython.user_ns['p']
9
+ keys_mapping = {Keys.F7: hilda.step_into,
10
+ Keys.F8: hilda.step_over,
11
+ Keys.F9: hilda.cont,
12
+ Keys.F10: hilda.stop}
13
+
14
+ insert_mode = ViInsertMode() | EmacsInsertMode()
15
+ registry = ipython.pt_app.key_bindings
16
+
17
+ for key, callback in keys_mapping.items():
18
+ registry.add_binding(key, filter=(HasFocus(DEFAULT_BUFFER) & ~HasSelection() & insert_mode))(
19
+ callback)
20
+
21
+ register_keybindings()
22
+ ipython.events.register('shell_initialized', register_keybindings)
@@ -0,0 +1,40 @@
1
+ import shlex
2
+
3
+ from IPython.core.magic import Magics, line_magic, magics_class, needs_local_scope
4
+
5
+
6
+ @magics_class
7
+ class HIMagics(Magics):
8
+
9
+ @line_magic
10
+ @needs_local_scope
11
+ def objc(self, line, local_ns=None):
12
+ """Load an Objective-C class by name into the IPython session."""
13
+ p = local_ns.get('p')
14
+ class_name = line.strip()
15
+ if not class_name:
16
+ p.log_error("Error: className is required.")
17
+ return
18
+ try:
19
+ local_ns[class_name] = p.objc_get_class(class_name)
20
+ p.log_info(f'{class_name} class loaded successfully')
21
+ except Exception as e:
22
+ p.log_error(f'Error loading class {class_name}: {str(e)}')
23
+
24
+ @line_magic
25
+ @needs_local_scope
26
+ def fbp(self, line, local_ns=None):
27
+ """Set a file breakpoint in the debugger."""
28
+ p = local_ns.get('p')
29
+ try:
30
+ module_name, address = shlex.split(line.strip())
31
+ address = int(address, 16)
32
+ p.file_symbol(address, module_name).bp()
33
+ except ValueError as ve:
34
+ p.log_error(f"Error parsing arguments: {str(ve)}")
35
+ except Exception as e:
36
+ p.log_error(f"Error setting breakpoint: {str(e)}")
37
+
38
+
39
+ def load_ipython_extension(ipython):
40
+ ipython.register_magics(HIMagics)