Skip to content

Commit 4671941

Browse files
committed
Upgrade to prompt-toolkit 2.x
1 parent e36417d commit 4671941

File tree

3 files changed

+72
-94
lines changed

3 files changed

+72
-94
lines changed

objection/console/repl.py

+60-71
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,12 @@
55
import click
66
import delegator
77
import frida
8-
import pygments.styles
9-
from prompt_toolkit import AbortAction, prompt
8+
from prompt_toolkit import PromptSession
9+
from prompt_toolkit.application import run_in_terminal
1010
from prompt_toolkit.auto_suggest import AutoSuggestFromHistory
1111
from prompt_toolkit.history import FileHistory
12-
from prompt_toolkit.styles import default_style_extensions, style_from_dict
13-
from pygments.style import Style
14-
from pygments.token import Token
12+
from prompt_toolkit.key_binding import KeyBindings
13+
from prompt_toolkit.styles import Style
1514

1615
from objection.utils.agent import Agent
1716
from .commands import COMMANDS
@@ -22,58 +21,22 @@
2221
from ..state.connection import state_connection
2322
from ..utils.helpers import get_tokens
2423

24+
bindings = KeyBindings()
2525

26-
class PromptStyle(Style):
27-
"""
28-
Class used to define some visual attributes for the
29-
REPL prompt.
30-
"""
31-
32-
def __init__(self) -> None:
33-
self.style = self._init_style()
34-
35-
@staticmethod
36-
def _init_style() -> dict:
37-
"""
38-
Grab the values for the prompt styling.
39-
40-
:return:
41-
"""
42-
43-
style = pygments.styles.get_style_by_name('vim')
44-
45-
styles = {}
46-
styles.update(style.styles)
47-
styles.update(default_style_extensions)
48-
49-
styles.update({
50-
# completions
51-
Token.Menu.Completions.Completion.Current: 'bg:#00aaaa #000000',
52-
Token.Menu.Completions.Completion: 'bg:#008888 #ffffff',
53-
Token.Menu.Completions.ProgressButton: 'bg:#003333',
54-
Token.Menu.Completions.ProgressBar: 'bg:#00aaaa',
55-
56-
# User input.
57-
Token: '#ff0066',
5826

59-
# Prompt.
60-
Token.Applicationname: '#007cff',
61-
Token.On: '#00aa00',
62-
Token.Devicetype: '#00ff48',
63-
Token.Version: '#00ff48',
64-
Token.Connection: '#717171'
65-
})
66-
67-
return style_from_dict(styles)
27+
@bindings.add('c-c')
28+
def _(_):
29+
"""
30+
Warn about exiting when Ctrl+C is pressed
6831
69-
def get_style(self) -> dict:
70-
"""
71-
Return the style for this Class.
32+
:param _:
33+
:return:
34+
"""
7235

73-
:return:
74-
"""
36+
def print_warn():
37+
click.secho('[warning] To exit, press ctrl+d or issue the exit command.', dim=True)
7538

76-
return self.style
39+
run_in_terminal(print_warn)
7740

7841

7942
class Repl(object):
@@ -88,6 +51,31 @@ def __init__(self) -> None:
8851
self.completer = CommandCompleter()
8952
self.commands_repository = COMMANDS
9053

54+
self.session = PromptSession(
55+
history=FileHistory(os.path.expanduser('~/.objection/objection_history')),
56+
)
57+
58+
@staticmethod
59+
def get_prompt_style() -> Style:
60+
"""
61+
Get the style to use for our prompt
62+
63+
:return:
64+
"""
65+
66+
return Style.from_dict({
67+
# completions menu
68+
'completion-menu.completion.current': 'bg:#00aaaa #000000',
69+
'completion-menu.completion': 'bg:#008888 #ffffff',
70+
71+
# Prompt.
72+
'applicationname': '#007cff',
73+
'on': '#00aa00',
74+
'devicetype': '#00ff48',
75+
'version': '#00ff48',
76+
'connection': '#717171'
77+
})
78+
9179
def set_prompt_tokens(self, device_info: tuple) -> None:
9280
"""
9381
Set prompt tokens sourced from a command.device.device_info()
@@ -96,37 +84,35 @@ def set_prompt_tokens(self, device_info: tuple) -> None:
9684
:param device_info:
9785
:return:
9886
"""
99-
10087
device_name, system_name, model, system_version = device_info
10188

10289
self.prompt_tokens = [
103-
(Token.Applicationname, device_name),
104-
(Token.On, ' on '),
105-
(Token.Devicetype, '(' + model + ': '),
106-
(Token.Version, system_version + ') '),
107-
(Token.Connection, '[' + state_connection.get_comms_type_string() + '] # '),
90+
('class:applicationname', device_name),
91+
('class:on', ' on '),
92+
('class:devicetype', '(' + model + ': '),
93+
('class:version', system_version + ') '),
94+
('class:connection', '[' + state_connection.get_comms_type_string() + '] # '),
10895
]
10996

110-
def get_prompt_tokens(self, _) -> list:
97+
def get_prompt_message(self) -> list:
11198
"""
11299
Return prompt tokens to use in the cli app.
113100
114101
If none were set during the init of this class, it
115102
is assumed that the connection failed.
116103
117-
:param _:
118104
:return:
119105
"""
120106

121107
if self.prompt_tokens:
122108
return self.prompt_tokens
123109

124110
return [
125-
(Token.Applicationname, 'unknown application'),
126-
(Token.On, ''),
127-
(Token.Devicetype, ''),
128-
(Token.Version, ' '),
129-
(Token.Connection, '[' + state_connection.get_comms_type_string() + '] # '),
111+
('class:applicationname', 'unknown application'),
112+
('class:on', ''),
113+
('class:devicetype', ''),
114+
('class:version', ' '),
115+
('class:connection', '[' + state_connection.get_comms_type_string() + '] # '),
130116
]
131117

132118
def run_command(self, document: str) -> None:
@@ -368,14 +354,14 @@ def start_repl(self, quiet: bool) -> None:
368354

369355
try:
370356

371-
document = prompt(
372-
get_prompt_tokens=self.get_prompt_tokens,
357+
document = self.session.prompt(
358+
self.get_prompt_message(), # prompt message
373359
completer=self.completer,
374-
style=PromptStyle().get_style(),
375-
history=FileHistory(os.path.expanduser('~/.objection/objection_history')),
360+
style=self.get_prompt_style(),
361+
key_bindings=bindings,
376362
auto_suggest=AutoSuggestFromHistory(),
377-
on_abort=AbortAction.RETRY,
378-
reserve_space_for_menu=4
363+
reserve_space_for_menu=4,
364+
complete_in_thread=True
379365
)
380366

381367
# check if this is an exit command
@@ -394,6 +380,9 @@ def start_repl(self, quiet: bool) -> None:
394380
# find something to run
395381
self.run_command(document)
396382

383+
except KeyboardInterrupt:
384+
pass
385+
397386
except frida.core.RPCException as e:
398387
click.secho('A Frida agent exception has occured.', fg='red', bold=True)
399388
click.secho('{0}'.format(e), fg='red')

requirements.txt

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
frida
22
frida-tools
3-
prompt_toolkit>=1.0.15,<2.0.0
3+
prompt_toolkit>=2.0.0,<3.0.0
44
click
55
tabulate
66
delegator.py

tests/console/test_repl.py

+11-22
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import unittest
22
from unittest import mock
3+
from unittest.mock import MagicMock
34

45
from frida import TimedOutError
56

@@ -32,7 +33,7 @@ def test_gets_prompt_tokens_without_having_them_set_first(self):
3233
self.assertEqual(type(self.repl.prompt_tokens), list)
3334
self.assertEqual(len(self.repl.prompt_tokens), 0)
3435

35-
tokens = self.repl.get_prompt_tokens(None)
36+
tokens = self.repl.get_prompt_message()
3637

3738
# [(Token.Applicationname, 'unknown application'),
3839
# (Token.On, ''), (Token.Devicetype, ''), (Token.Version, ' '), (Token.Connection, '[usb] # ')]
@@ -45,7 +46,7 @@ def test_gets_prompt_tokens_without_having_them_set_first(self):
4546
def test_gets_prompt_tokens_after_setting_them(self):
4647
self.repl.set_prompt_tokens(('a', 'b', 'c', 'd'))
4748

48-
tokens = self.repl.get_prompt_tokens(None)
49+
tokens = self.repl.get_prompt_message()
4950

5051
self.assertEqual(tokens[0][1], 'a')
5152
self.assertEqual(tokens[1][1], ' on ')
@@ -166,9 +167,9 @@ def test_handles_reconnects_and_reports_failures(self, mock_unload, get_device_i
166167

167168
self.assertEqual(output, expected_output)
168169

169-
@mock.patch('objection.console.repl.prompt')
170-
def test_starts_repl_and_exists_cleanly_with_banner(self, prompt):
171-
prompt.return_value = 'exit'
170+
def test_starts_repl_and_exists_cleanly_with_banner(self):
171+
self.repl.session = MagicMock(name='session')
172+
self.repl.session.prompt.return_value = 'exit'
172173

173174
with capture(self.repl.start_repl, False) as o:
174175
output = o
@@ -188,9 +189,9 @@ def test_starts_repl_and_exists_cleanly_with_banner(self, prompt):
188189

189190
self.assertEqual(output, expected_output)
190191

191-
@mock.patch('objection.console.repl.prompt')
192-
def test_starts_repl_and_exists_cleanly_with_banner_and_quiet_flag(self, prompt):
193-
prompt.return_value = 'exit'
192+
def test_starts_repl_and_exists_cleanly_with_banner_and_quiet_flag(self):
193+
self.repl.session = MagicMock(name='session')
194+
self.repl.session.prompt.return_value = 'exit'
194195

195196
with capture(self.repl.start_repl, True) as o:
196197
output = o
@@ -199,22 +200,10 @@ def test_starts_repl_and_exists_cleanly_with_banner_and_quiet_flag(self, prompt)
199200

200201
self.assertEqual(output, expected_output)
201202

202-
@mock.patch('objection.console.repl.prompt')
203-
def test_starts_repl_and_catches_ctrl_c(self, prompt):
204-
prompt.side_effect = KeyboardInterrupt()
205-
206-
with capture(self.repl.start_repl, True) as o:
207-
output = o
208-
209-
expected_output = 'Exiting...\n'
210-
211-
self.assertRaises(KeyboardInterrupt)
212-
self.assertEqual(output, expected_output)
213-
214-
@mock.patch('objection.console.repl.prompt')
203+
@mock.patch('objection.console.repl.PromptSession')
215204
@mock.patch('objection.console.repl.Repl.run_command')
216205
def test_runs_commands_and_catches_exceptions(self, prompt, run_command):
217-
prompt.return_value = 'ios keychain clear'
206+
prompt.return_value.prompt.return_value = 'ios keychain clear'
218207
run_command.side_effect = TypeError()
219208

220209
self.assertRaises(TypeError)

0 commit comments

Comments
 (0)