hilda 2.0.16__py3-none-any.whl → 3.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/hilda_client.py CHANGED
@@ -5,7 +5,6 @@ import importlib.util
5
5
  import json
6
6
  import logging
7
7
  import os
8
- import pickle
9
8
  import struct
10
9
  import sys
11
10
  import time
@@ -18,10 +17,9 @@ from functools import cached_property, wraps
18
17
  from pathlib import Path
19
18
  from typing import Any, Callable, Optional, Union
20
19
 
20
+ import click
21
21
  import hexdump
22
22
  import IPython
23
- from humanfriendly import prompts
24
- from humanfriendly.terminal.html import html_to_ansi
25
23
  from IPython.core.magic import register_line_magic # noqa: F401
26
24
  from pygments import highlight
27
25
  from pygments.formatters import TerminalTrueColorFormatter
@@ -30,11 +28,13 @@ from tqdm import tqdm
30
28
  from traitlets.config import Config
31
29
 
32
30
  from hilda import objective_c_class
31
+ from hilda.breakpoints import BreakpointList, HildaBreakpoint, WhereType
33
32
  from hilda.common import CfSerializable, selection_prompt
34
33
  from hilda.exceptions import AccessingMemoryError, AccessingRegisterError, AddingLldbSymbolError, \
35
- BrokenLocalSymbolsJarError, ConvertingFromNSObjectError, ConvertingToNsObjectError, CreatingObjectiveCSymbolError, \
34
+ ConvertingFromNSObjectError, ConvertingToNsObjectError, CreatingObjectiveCSymbolError, \
36
35
  DisableJetsamMemoryChecksError, EvaluatingExpressionError, HildaException, InvalidThreadIndexError, \
37
36
  SymbolAbsentError
37
+ from hilda.ipython_extensions.keybindings import get_keybindings
38
38
  from hilda.lldb_importer import lldb
39
39
  from hilda.objective_c_symbol import ObjectiveCSymbol
40
40
  from hilda.registers import Registers
@@ -50,31 +50,12 @@ except ImportError:
50
50
  lldb.KEYSTONE_SUPPORT = False
51
51
  print('failed to import keystone. disabling some features')
52
52
 
53
- hilda_art = Path(__file__).resolve().parent.joinpath('hilda_ascii_art.html').read_text()
54
-
55
- GREETING = f"""
56
- {hilda_art}
57
-
58
- <b>Hilda has been successfully loaded! 😎
59
- Usage:
60
- <span style="color: magenta">p</span> Global to access all features.
61
- <span style="color: magenta">F1</span> Show UI.
62
- <span style="color: magenta">F2</span> Toggle enabling of stdout & stderr.
63
- <span style="color: magenta">F7</span> Step Into.
64
- <span style="color: magenta">F8</span> Step Over.
65
- <span style="color: magenta">F9</span> Continue.
66
- <span style="color: magenta">F10</span> Stop.
67
-
68
- Have a nice flight ✈️! Starting an IPython shell...
69
- """
70
-
71
53
 
72
54
  def disable_logs() -> None:
73
55
  logging.getLogger('asyncio').disabled = True
74
56
  logging.getLogger('parso.cache').disabled = True
75
57
  logging.getLogger('parso.cache.pickle').disabled = True
76
58
  logging.getLogger('parso.python.diff').disabled = True
77
- logging.getLogger('humanfriendly.prompts').disabled = True
78
59
  logging.getLogger('blib2to3.pgen2.driver').disabled = True
79
60
  logging.getLogger('hilda.launch_lldb').setLevel(logging.INFO)
80
61
 
@@ -82,6 +63,17 @@ def disable_logs() -> None:
82
63
  SerializableSymbol = namedtuple('SerializableSymbol', 'address type_ filename')
83
64
 
84
65
 
66
+ @dataclass
67
+ class HelpSnippet:
68
+ """ Small snippet line to occur from `HildaClient.show_help()` """
69
+
70
+ key: str
71
+ description: str
72
+
73
+ def __str__(self) -> str:
74
+ return click.style(self.key.ljust(8), bold=True, fg='magenta') + click.style(self.description, bold=True)
75
+
76
+
85
77
  @dataclass
86
78
  class Configs:
87
79
  """ Configuration settings for evaluation and monitoring. """
@@ -125,39 +117,17 @@ def stop_is_needed(func: Callable):
125
117
  return wrapper
126
118
 
127
119
 
128
- class HildaBreakpoint:
129
- def __init__(self, hilda_client: 'HildaClient', lldb_breakpoint: lldb.SBBreakpoint,
130
- address: Union[str, int], forced: bool = False, options: Optional[typing.Mapping] = None,
131
- callback: Optional[Callable] = None) -> None:
132
- self._hilda_client = hilda_client
133
- self.address = address
134
- self.forced = forced
135
- self.options = options
136
- self.callback = callback
137
- self.lldb_breakpoint = lldb_breakpoint
138
-
139
- def __repr__(self) -> str:
140
- return (f'<{self.__class__.__name__} LLDB:{self.lldb_breakpoint} FORCED:{self.forced} OPTIONS:{self.options} '
141
- f'CALLBACK:{self.callback}>')
142
-
143
- def __str__(self) -> str:
144
- return repr(self)
145
-
146
- def remove(self) -> None:
147
- self._hilda_client.remove_hilda_breakpoint(self.lldb_breakpoint.id)
148
-
149
-
150
120
  class HildaClient:
151
121
  RETVAL_BIT_COUNT = 64
152
122
 
153
- def __init__(self, debugger: lldb.SBDebugger):
123
+ def __init__(self, debugger: lldb.SBDebugger) -> None:
154
124
  self.logger = logging.getLogger(__name__)
155
125
  self.endianness = '<'
156
126
  self.debugger = debugger
157
127
  self.target = debugger.GetSelectedTarget()
158
128
  self.process = self.target.GetProcess()
159
129
  self.symbols = SymbolsJar.create(self)
160
- self.breakpoints = {}
130
+ self.breakpoints = BreakpointList(self)
161
131
  self.captured_objects = {}
162
132
  self.registers = Registers(self)
163
133
  self.arch = self.target.GetTriple().split('-')[0]
@@ -177,14 +147,15 @@ class HildaClient:
177
147
  self.log_info(f'Target: {self.target}')
178
148
  self.log_info(f'Process: {self.process}')
179
149
 
180
- def hd(self, buf):
150
+ def hd(self, buf: bytes) -> None:
181
151
  """
182
- Print an hexdump of given buffer
152
+ Print hexdump representation for given buffer.
153
+
183
154
  :param buf: buffer to print in hexdump form
184
155
  """
185
- print(hexdump.hexdump(buf))
156
+ hexdump.hexdump(buf)
186
157
 
187
- def lsof(self) -> dict:
158
+ def lsof(self) -> dict[int, Any]:
188
159
  """
189
160
  Get dictionary of all open FDs
190
161
  :return: Mapping between open FDs and their paths
@@ -201,7 +172,7 @@ class HildaClient:
201
172
  if i == depth:
202
173
  break
203
174
  row = ''
204
- row += html_to_ansi(f'<span style="color: cyan">0x{frame.addr.GetFileAddress():x}</span> ')
175
+ row += click.style(f'0x{frame.addr.GetFileAddress():x} ', fg='cyan')
205
176
  row += str(frame)
206
177
  if i == 0:
207
178
  # first line
@@ -222,7 +193,7 @@ class HildaClient:
222
193
  if result:
223
194
  raise DisableJetsamMemoryChecksError()
224
195
 
225
- def symbol(self, address):
196
+ def symbol(self, address: int) -> Symbol:
226
197
  """
227
198
  Get symbol object for a given address
228
199
  :param address:
@@ -230,7 +201,7 @@ class HildaClient:
230
201
  """
231
202
  return Symbol.create(address, self)
232
203
 
233
- def objc_symbol(self, address) -> ObjectiveCSymbol:
204
+ def objc_symbol(self, address: int) -> ObjectiveCSymbol:
234
205
  """
235
206
  Get objc symbol wrapper for given address
236
207
  :param address:
@@ -241,11 +212,12 @@ class HildaClient:
241
212
  except HildaException as e:
242
213
  raise CreatingObjectiveCSymbolError from e
243
214
 
244
- def inject(self, filename):
215
+ def inject(self, filename: str) -> SymbolsJar:
245
216
  """
246
- Inject a single library into currently running process
247
- :param filename:
248
- :return: module object
217
+ Inject a single library into currently running process.
218
+
219
+ :param filename: library to inject (dylib)
220
+ :return: SymbolsJar
249
221
  """
250
222
  module = self.target.FindModule(lldb.SBFileSpec(os.path.basename(filename), False))
251
223
  if module.file.basename is not None:
@@ -459,7 +431,8 @@ class HildaClient:
459
431
 
460
432
  def set_register(self, name: str, value: Union[float, int]) -> None:
461
433
  """
462
- Set value for register by its name
434
+ Set value for register by its name.
435
+
463
436
  :param name: Register name
464
437
  :param value: Register value
465
438
  """
@@ -473,7 +446,8 @@ class HildaClient:
473
446
 
474
447
  def objc_call(self, obj: int, selector: str, *params):
475
448
  """
476
- Simulate a call to an objc selector
449
+ Simulate a call to an objc selector.
450
+
477
451
  :param obj: obj to pass into `objc_msgSend`
478
452
  :param selector: selector to execute
479
453
  :param params: any other additional parameters the selector requires
@@ -489,7 +463,7 @@ class HildaClient:
489
463
  with self.stopped():
490
464
  return self.evaluate_expression(call_expression)
491
465
 
492
- def call(self, address, argv: list = None):
466
+ def call(self, address, argv: Optional[list] = None):
493
467
  """
494
468
  Call function at given address with given parameters
495
469
  :param address:
@@ -506,107 +480,11 @@ class HildaClient:
506
480
  """
507
481
  Monitor every time a given address is called
508
482
 
509
- The following options are available:
510
- regs={reg1: format}
511
- will print register values
512
-
513
- Available formats:
514
- x: hex
515
- s: string
516
- cf: use CFCopyDescription() to get more informative description of the object
517
- po: use LLDB po command
518
- std::string: for std::string
519
-
520
- User defined function, will be called like `format_function(hilda_client, value)`.
521
-
522
- For example:
523
- regs={'x0': 'x'} -> x0 will be printed in HEX format
524
- expr={lldb_expression: format}
525
- lldb_expression can be for example '$x0' or '$arg1'
526
- format behaves just like 'regs' option
527
- retval=format
528
- Print function's return value. The format is the same as regs format.
529
- stop=True
530
- force a stop at every hit
531
- bt=True
532
- print backtrace
533
- cmd=[cmd1, cmd2]
534
- run several LLDB commands, one by another
535
- force_return=value
536
- force a return from function with the specified value
537
- name=some_value
538
- use `some_name` instead of the symbol name automatically extracted from the calling frame
539
- override=True
540
- override previous break point at same location
541
-
542
-
543
- :param address:
544
- :param condition: set as a conditional breakpoint using an lldb expression
545
- :param options:
546
- :return:
483
+ Alias of self.breakpoints.add_monitor()
547
484
  """
485
+ return self.breakpoints.add_monitor(address, condition, **options)
548
486
 
549
- def callback(hilda, frame, bp_loc, options):
550
- """
551
- :param HildaClient hilda: Hilda client.
552
- :param lldb.SBFrame frame: LLDB Frame object.
553
- :param lldb.SBBreakpointLocation bp_loc: LLDB Breakpoint location object.
554
- :param dict options: User defined options.
555
- """
556
- bp = bp_loc.GetBreakpoint()
557
-
558
- symbol = hilda.symbol(hilda.frame.addr.GetLoadAddress(hilda.target)) # type: Symbol
559
-
560
- # by default, attempt to resolve the symbol name through lldb
561
- name = str(symbol.lldb_symbol)
562
- if options.get('name', False):
563
- name = options['name']
564
-
565
- log_message = f'🚨 #{bp.id} 0x{symbol:x} {name} - Thread #{self.thread.idx}:{hex(self.thread.id)}'
566
-
567
- if 'regs' in options:
568
- log_message += '\nregs:'
569
- for name, fmt in options['regs'].items():
570
- value = hilda.symbol(frame.FindRegister(name).unsigned)
571
- log_message += f'\n\t{name} = {hilda._monitor_format_value(fmt, value)}'
572
-
573
- if 'expr' in options:
574
- log_message += '\nexpr:'
575
- for name, fmt in options['expr'].items():
576
- value = hilda.symbol(hilda.evaluate_expression(name))
577
- log_message += f'\n\t{name} = {hilda._monitor_format_value(fmt, value)}'
578
-
579
- force_return = options.get('force_return')
580
- if force_return is not None:
581
- hilda.force_return(force_return)
582
- log_message += f'\nforced return: {force_return}'
583
-
584
- if options.get('bt'):
585
- # bugfix: for callstacks from xpc events
586
- hilda.finish()
587
- for frame in hilda.bt():
588
- log_message += f'\n\t{frame[0]} {frame[1]}'
589
-
590
- retval = options.get('retval')
591
- if retval is not None:
592
- # return from function
593
- hilda.finish()
594
- value = hilda.evaluate_expression('$arg1')
595
- log_message += f'\nreturned: {hilda._monitor_format_value(retval, value)}'
596
-
597
- hilda.log_info(log_message)
598
-
599
- for cmd in options.get('cmd', []):
600
- hilda.lldb_handle_command(cmd)
601
-
602
- if options.get('stop', False):
603
- hilda.log_info('Process remains stopped and focused on current thread')
604
- else:
605
- hilda.cont()
606
-
607
- return self.bp(address, callback, condition=condition, **options)
608
-
609
- def show_current_source(self):
487
+ def show_current_source(self) -> None:
610
488
  """ print current source code if possible """
611
489
  self.lldb_handle_command('f')
612
490
 
@@ -632,26 +510,7 @@ class HildaClient:
632
510
  if self.ui_manager.active:
633
511
  self.ui_manager.show()
634
512
 
635
- def remove_all_hilda_breakpoints(self, remove_forced=False):
636
- """
637
- Remove all breakpoints created by Hilda
638
- :param remove_forced: include removed of "forced" breakpoints
639
- """
640
- breakpoints = list(self.breakpoints.items())
641
- for bp_id, bp in breakpoints:
642
- if remove_forced or not bp.forced:
643
- self.remove_hilda_breakpoint(bp_id)
644
-
645
- def remove_hilda_breakpoint(self, bp_id: int) -> None:
646
- """
647
- Remove a single breakpoint placed by Hilda
648
- :param bp_id: Breakpoint's ID
649
- """
650
- self.target.BreakpointDelete(bp_id)
651
- del self.breakpoints[bp_id]
652
- self.log_info(f'BP #{bp_id} has been removed')
653
-
654
- def force_return(self, value=0):
513
+ def force_return(self, value: int = 0) -> None:
655
514
  """
656
515
  Prematurely return from a stack frame, short-circuiting exection of newer frames and optionally
657
516
  yielding a specified value.
@@ -661,11 +520,11 @@ class HildaClient:
661
520
  self.finish()
662
521
  self.set_register('x0', value)
663
522
 
664
- def proc_info(self):
523
+ def proc_info(self) -> None:
665
524
  """ Print information about currently running mapped process. """
666
525
  print(self.process)
667
526
 
668
- def print_proc_entitlements(self):
527
+ def print_proc_entitlements(self) -> None:
669
528
  """ Get the plist embedded inside the process' __LINKEDIT section. """
670
529
  linkedit_section = self.target.modules[0].FindSection('__LINKEDIT')
671
530
  linkedit_data = self.symbol(linkedit_section.GetLoadAddress(self.target)).peek(linkedit_section.size)
@@ -675,112 +534,24 @@ class HildaClient:
675
534
  entitlements = str(linkedit_data[linkedit_data.find(b'<?xml'):].split(b'\xfa', 1)[0], 'utf8')
676
535
  print(highlight(entitlements, XmlLexer(), TerminalTrueColorFormatter()))
677
536
 
678
- def bp(self, address_or_name: Union[int, str], callback: Optional[Callable] = None, condition: str = None,
679
- forced=False, module_name: Optional[str] = None, **options) -> HildaBreakpoint:
537
+ def bp(self, address_or_name: WhereType, callback: Optional[Callable] = None, condition: Optional[str] = None,
538
+ guarded: bool = False, description: Optional[str] = None, **options) -> HildaBreakpoint:
680
539
  """
681
540
  Add a breakpoint
682
- :param address_or_name:
541
+
542
+ Alias of self.breakpoints.add()
543
+
544
+ :param address_or_name: Where to place the breakpoint
683
545
  :param condition: set as a conditional breakpoint using lldb expression
684
546
  :param callback: callback(hilda, *args) to be called
685
- :param forced: whether the breakpoint should be protected frm usual removal.
686
- :param module_name: Specify module name to place the BP in (used with `address_or_name` when using a name)
547
+ :param guarded: whether the breakpoint should be protected frm usual removal.
548
+ :param description: Attach a breakpoint description
687
549
  :param options: can contain an `override` keyword to specify if to override an existing BP
688
550
  :return: native LLDB breakpoint
689
551
  """
690
- if address_or_name in [bp.address for bp in self.breakpoints.values()]:
691
- override = True if options.get('override', True) else False
692
- if override or prompts.prompt_for_confirmation('A breakpoint already exist in given location. '
693
- 'Would you like to delete the previous one?', True):
694
- breakpoints = list(self.breakpoints.items())
695
- for bp_id, bp in breakpoints:
696
- if address_or_name == bp.address:
697
- self.remove_hilda_breakpoint(bp_id)
698
-
699
- if isinstance(address_or_name, int):
700
- bp = self.target.BreakpointCreateByAddress(address_or_name)
701
- elif isinstance(address_or_name, str):
702
- bp = self.target.BreakpointCreateByName(address_or_name)
703
-
704
- if condition is not None:
705
- bp.SetCondition(condition)
706
-
707
- # add into Hilda's internal list of breakpoints
708
- self.breakpoints[bp.id] = HildaBreakpoint(self, bp, address=address_or_name, forced=forced, options=options,
709
- callback=callback)
710
-
711
- if callback is not None:
712
- bp.SetScriptCallbackFunction('lldb.hilda_client.bp_callback_router')
713
-
714
- self.log_info(f'Breakpoint #{bp.id} has been set')
715
- return self.breakpoints[bp.id]
716
-
717
- def bp_callback_router(self, frame, bp_loc, *_):
718
- """
719
- Route the breakpoint callback the specific breakpoint callback.
720
- :param lldb.SBFrame frame: LLDB Frame object.
721
- :param lldb.SBBreakpointLocation bp_loc: LLDB Breakpoint location object.
722
- """
723
- bp_id = bp_loc.GetBreakpoint().GetID()
724
- self._bp_frame = frame
725
- try:
726
- self.breakpoints[bp_id].callback(self, frame, bp_loc, self.breakpoints[bp_id].options)
727
- finally:
728
- self._bp_frame = None
729
-
730
- def show_hilda_breakpoints(self):
731
- """ Show existing breakpoints created by Hilda. """
732
- for bp_id, bp in self.breakpoints.items():
733
- print(f'🚨 Breakpoint #{bp_id}: Forced: {bp.forced}')
734
- if isinstance(bp.address, int):
735
- print(f'\tAddress: 0x{bp.address:x}')
736
- elif isinstance(bp.address, str):
737
- print(f'\tName: {bp.address}')
738
- print(f'\tOptions: {bp.options}')
739
-
740
- def save(self, filename=None):
741
- """
742
- Save loaded symbols map (for loading later using the load() command)
743
- :param filename: optional filename for where to store
744
- """
745
- if filename is None:
746
- filename = self._get_saved_state_filename()
747
-
748
- self.log_info(f'saving current state info: {filename}')
749
- with open(filename, 'wb') as f:
750
- symbols_copy = {}
751
- for k, v in self.symbols.items():
752
- # converting the symbols into serializable objects
753
- symbols_copy[k] = SerializableSymbol(address=int(v),
754
- type_=v.type_,
755
- filename=v.filename)
756
- pickle.dump(symbols_copy, f)
552
+ return self.breakpoints.add(address_or_name, callback, condition, guarded, description=description, **options)
757
553
 
758
- def load(self, filename=None):
759
- """
760
- Load an existing symbols map (previously saved by the save() command)
761
- :param filename: filename to load from
762
- """
763
- if filename is None:
764
- filename = self._get_saved_state_filename()
765
-
766
- self.log_info(f'loading current state from: {filename}')
767
- with open(filename, 'rb') as f:
768
- symbols_copy = pickle.load(f)
769
-
770
- for k, v in tqdm(symbols_copy.items()):
771
- self.symbols[k] = self.symbol(v.address)
772
-
773
- # perform sanity test for symbol rand
774
- if self.symbols.rand() == 0 and self.symbols.rand() == 0:
775
- # rand returning 0 twice means the loaded file is probably outdated
776
- raise BrokenLocalSymbolsJarError()
777
-
778
- # assuming the first main image will always change
779
- self.rebind_symbols(image_range=[0, 0])
780
- self.init_dynamic_environment()
781
- self._symbols_loaded = True
782
-
783
- def po(self, expression, cast=None):
554
+ def po(self, expression: str, cast: Optional[str] = None) -> str:
784
555
  """
785
556
  Print given object using LLDB's po command
786
557
 
@@ -804,7 +575,7 @@ class HildaClient:
804
575
  raise EvaluatingExpressionError(res.GetError())
805
576
  return res.GetOutput().strip()
806
577
 
807
- def globalize_symbols(self):
578
+ def globalize_symbols(self) -> None:
808
579
  """
809
580
  Make all symbols in python's global scope
810
581
  """
@@ -817,18 +588,18 @@ class HildaClient:
817
588
  and '.' not in name:
818
589
  self._add_global(name, value, reserved_names)
819
590
 
820
- def jump(self, symbol: int):
591
+ def jump(self, symbol: int) -> None:
821
592
  """ jump to given symbol """
822
593
  self.lldb_handle_command(f'j *{symbol}')
823
594
 
824
- def lldb_handle_command(self, cmd):
595
+ def lldb_handle_command(self, cmd: str) -> None:
825
596
  """
826
597
  Execute an LLDB command
827
598
 
828
599
  For example:
829
600
  lldb_handle_command('register read')
830
601
 
831
- :param cmd:
602
+ :param cmd: LLDB command
832
603
  """
833
604
  self.debugger.HandleCommand(cmd)
834
605
 
@@ -968,7 +739,7 @@ class HildaClient:
968
739
  return self.thread.GetSelectedFrame()
969
740
 
970
741
  @contextmanager
971
- def stopped(self, interval=0):
742
+ def stopped(self, interval: int = 0):
972
743
  """
973
744
  Context-Manager for execution while process is stopped.
974
745
  If interval is supplied, then if the device is in running state, it will sleep for the interval
@@ -988,7 +759,7 @@ class HildaClient:
988
759
  self.cont()
989
760
 
990
761
  @contextmanager
991
- def safe_malloc(self, size):
762
+ def safe_malloc(self, size: int):
992
763
  """
993
764
  Context-Manager for allocating a block of memory which is freed afterwards
994
765
  :param size:
@@ -1097,19 +868,36 @@ class HildaClient:
1097
868
  return
1098
869
  client.finish()
1099
870
  client.log_info(f'Desired module has been loaded: {expression}. Process remains stopped')
1100
- bp = bp_loc.GetBreakpoint()
1101
- client.remove_hilda_breakpoint(bp.id)
871
+ bp_id = bp_loc.GetBreakpoint().GetID()
872
+ client.breakpoints.remove(bp_id)
1102
873
 
1103
874
  self.bp('dlopen', bp)
1104
875
  self.cont()
1105
876
 
877
+ def show_help(self, *_) -> None:
878
+ """
879
+ Show banner help message
880
+ """
881
+ help_snippets = [HelpSnippet(key='p', description='Global to access all features')]
882
+ for keybinding in get_keybindings(self):
883
+ help_snippets.append(HelpSnippet(key=keybinding.key.upper(), description=keybinding.description))
884
+
885
+ for help_snippet in help_snippets:
886
+ click.echo(help_snippet)
887
+
1106
888
  def interact(self, additional_namespace: Optional[typing.Mapping] = None,
1107
889
  startup_files: Optional[list[str]] = None) -> None:
1108
890
  """ Start an interactive Hilda shell """
1109
891
  if not self._dynamic_env_loaded:
1110
892
  self.init_dynamic_environment()
1111
- print('\n')
1112
- self.log_info(html_to_ansi(GREETING))
893
+
894
+ # Show greeting
895
+ click.secho('Hilda has been successfully loaded! 😎', bold=True)
896
+ click.secho('Usage:', bold=True)
897
+ self.show_help()
898
+ click.echo(click.style('Have a nice flight ✈️! Starting an IPython shell...', bold=True))
899
+
900
+ # Configure and start IPython shell
1113
901
  ipython_config = Config()
1114
902
  ipython_config.IPCompleter.use_jedi = True
1115
903
  ipython_config.BaseIPythonApplication.profile = 'hilda'
@@ -1215,21 +1003,6 @@ class HildaClient:
1215
1003
  else:
1216
1004
  return value[0].peek_str()
1217
1005
 
1218
- def _monitor_format_value(self, fmt, value):
1219
- if callable(fmt):
1220
- return fmt(self, value)
1221
- formatters = {
1222
- 'x': lambda val: f'0x{int(val):x}',
1223
- 's': lambda val: val.peek_str() if val else None,
1224
- 'cf': lambda val: val.cf_description,
1225
- 'po': lambda val: val.po(),
1226
- 'std::string': self._std_string
1227
- }
1228
- if fmt in formatters:
1229
- return formatters[fmt](value)
1230
- else:
1231
- return f'{value:x} (unsupported format)'
1232
-
1233
1006
  @cached_property
1234
1007
  def _object_identifier(self) -> Symbol:
1235
1008
  return self.symbols.objc_getClass('VMUObjectIdentifier').objc_call('alloc').objc_call(
@@ -1,24 +1,47 @@
1
+ from dataclasses import dataclass
2
+ from typing import Callable
3
+
1
4
  from prompt_toolkit.enums import DEFAULT_BUFFER
2
5
  from prompt_toolkit.filters import EmacsInsertMode, HasFocus, HasSelection, ViInsertMode
3
6
  from prompt_toolkit.keys import Keys
4
7
 
5
8
 
9
+ @dataclass
10
+ class Keybinding:
11
+ key: str
12
+ description: str
13
+ callback: Callable
14
+
15
+
16
+ def get_keybindings(hilda_client) -> list[Keybinding]:
17
+ """
18
+ Get list of keybindings
19
+
20
+ :param hilda.hilda_client.HildaClient hilda_client: Hilda client to bind the keys to operations
21
+ """
22
+ return [
23
+ Keybinding(key=Keys.F1, description='Show this help', callback=hilda_client.show_help),
24
+ Keybinding(key=Keys.F2, description='Show process state UI', callback=hilda_client.ui_manager.show),
25
+ Keybinding(key=Keys.F3, description='Toggle enabling of stdout & stderr',
26
+ callback=hilda_client.toggle_enable_stdout_stderr),
27
+ Keybinding(key=Keys.F7, description='Step Into', callback=hilda_client.step_into),
28
+ Keybinding(key=Keys.F8, description='Step Over', callback=hilda_client.step_over),
29
+ Keybinding(key=Keys.F9, description='Continue',
30
+ callback=lambda _: (hilda_client.log_info('Sending continue'), hilda_client.cont())),
31
+ Keybinding(key=Keys.F10, description='Stop',
32
+ callback=lambda _: (hilda_client.log_info('Sending stop'), hilda_client.stop())),
33
+ ]
34
+
35
+
6
36
  def load_ipython_extension(ipython):
7
37
  def register_keybindings():
8
- hilda = ipython.user_ns['p']
9
- keys_mapping = {Keys.F1: hilda.ui_manager.show,
10
- Keys.F2: hilda.toggle_enable_stdout_stderr,
11
- Keys.F7: hilda.step_into,
12
- Keys.F8: hilda.step_over,
13
- Keys.F9: lambda _: (hilda.log_info('Sending continue'), hilda.cont()),
14
- Keys.F10: lambda _: (hilda.log_info('Sending stop'), hilda.stop())}
15
-
38
+ hilda_client = ipython.user_ns['p']
16
39
  insert_mode = ViInsertMode() | EmacsInsertMode()
17
40
  registry = ipython.pt_app.key_bindings
18
41
 
19
- for key, callback in keys_mapping.items():
20
- registry.add_binding(key, filter=(HasFocus(DEFAULT_BUFFER) & ~HasSelection() & insert_mode))(
21
- callback)
42
+ for keybind in get_keybindings(hilda_client):
43
+ registry.add_binding(
44
+ keybind.key, filter=(HasFocus(DEFAULT_BUFFER) & ~HasSelection() & insert_mode))(keybind.callback)
22
45
 
23
46
  register_keybindings()
24
47
  ipython.events.register('shell_initialized', register_keybindings)
@@ -4,7 +4,6 @@ from collections import namedtuple
4
4
  from dataclasses import dataclass, field
5
5
  from functools import partial
6
6
  from typing import Any
7
- from uuid import uuid4
8
7
 
9
8
  from objc_types_decoder.decode import decode as decode_type
10
9
  from objc_types_decoder.decode import decode_with_tail
@@ -188,19 +187,16 @@ class Class:
188
187
  hilda.log_info('removing breakpoints')
189
188
  for bp_id, bp in list(hilda.breakpoints.items()):
190
189
  if 'group_uuid' in bp.options and bp.options.get('group_uuid', '') == options['group_uuid']:
191
- hilda.remove_hilda_breakpoint(bp_id)
190
+ hilda.breakpoints.remove(bp_id)
192
191
  captured = hilda.evaluate_expression('$arg1')
193
192
  captured = captured.objc_symbol
194
193
  hilda.captured_objects[options['name'].split(' ')[0].split('[')[1]] = captured
195
194
  hilda.cont()
196
195
 
197
- group_uuid = str(uuid4())
198
-
199
196
  for method in self.methods:
200
197
  if not method.is_class:
201
198
  # only instance methods are relevant for capturing self
202
- method.imp.bp(hook, group_uuid=group_uuid,
203
- name=f'-[{class_name} {method.name}]')
199
+ method.imp.bp(hook)
204
200
 
205
201
  if sync:
206
202
  self._client.cont()