1388 lines
48 KiB
Python
1388 lines
48 KiB
Python
import argparse
|
|
import codecs
|
|
import hashlib
|
|
import json
|
|
import os
|
|
import platform
|
|
import re
|
|
import shlex
|
|
import signal
|
|
import string
|
|
import sys
|
|
import threading
|
|
import time
|
|
from timeit import default_timer as timer
|
|
from typing import Any, AnyStr, Callable, Dict, Iterable, List, Mapping, MutableMapping, Optional, Tuple, TypeVar, Union
|
|
from urllib.request import build_opener
|
|
|
|
import frida
|
|
from colorama import Fore, Style
|
|
from prompt_toolkit import PromptSession
|
|
from prompt_toolkit.completion import CompleteEvent, Completer, Completion
|
|
from prompt_toolkit.document import Document
|
|
from prompt_toolkit.history import FileHistory
|
|
from prompt_toolkit.lexers import PygmentsLexer
|
|
from prompt_toolkit.shortcuts import prompt
|
|
from prompt_toolkit.styles import Style as PromptToolkitStyle
|
|
from pygments.lexers.javascript import JavascriptLexer
|
|
from pygments.token import Token
|
|
|
|
from frida_tools import _repl_magic
|
|
from frida_tools.application import ConsoleApplication
|
|
from frida_tools.cli_formatting import format_compiled, format_compiling, format_diagnostic
|
|
from frida_tools.reactor import Reactor
|
|
|
|
T = TypeVar("T")
|
|
|
|
|
|
class REPLApplication(ConsoleApplication):
|
|
def __init__(self) -> None:
|
|
self._script = None
|
|
self._ready = threading.Event()
|
|
self._stopping = threading.Event()
|
|
self._errors = 0
|
|
self._completer = FridaCompleter(self)
|
|
self._cli = None
|
|
self._last_change_id = 0
|
|
self._compilers: Dict[str, CompilerContext] = {}
|
|
self._monitored_files: MutableMapping[Union[str, bytes], frida.FileMonitor] = {}
|
|
self._autoperform = False
|
|
self._autoperform_option = False
|
|
self._autoreload = True
|
|
self._quiet_start: Optional[float] = None
|
|
|
|
super().__init__(self._process_input, self._on_stop)
|
|
|
|
if self._have_terminal and not self._plain_terminal:
|
|
style = PromptToolkitStyle(
|
|
[
|
|
("completion-menu", "bg:#3d3d3d #ef6456"),
|
|
("completion-menu.completion.current", "bg:#ef6456 #3d3d3d"),
|
|
]
|
|
)
|
|
history = FileHistory(self._get_or_create_history_file())
|
|
self._cli = PromptSession(
|
|
lexer=PygmentsLexer(JavascriptLexer),
|
|
style=style,
|
|
history=history,
|
|
completer=self._completer,
|
|
complete_in_thread=True,
|
|
enable_open_in_editor=True,
|
|
tempfile_suffix=".js",
|
|
)
|
|
self._dumb_stdin_reader = None
|
|
else:
|
|
self._cli = None
|
|
self._dumb_stdin_reader = DumbStdinReader(valid_until=self._stopping.is_set)
|
|
|
|
if not self._have_terminal:
|
|
self._rpc_complete_server = start_completion_thread(self)
|
|
|
|
def _add_options(self, parser: argparse.ArgumentParser) -> None:
|
|
parser.add_argument(
|
|
"-l", "--load", help="load SCRIPT", metavar="SCRIPT", dest="user_scripts", action="append", default=[]
|
|
)
|
|
parser.add_argument(
|
|
"-P",
|
|
"--parameters",
|
|
help="parameters as JSON, same as Gadget",
|
|
metavar="PARAMETERS_JSON",
|
|
dest="user_parameters",
|
|
)
|
|
parser.add_argument("-C", "--cmodule", help="load CMODULE", dest="user_cmodule")
|
|
parser.add_argument(
|
|
"--toolchain",
|
|
help="CModule toolchain to use when compiling from source code",
|
|
choices=["any", "internal", "external"],
|
|
default="any",
|
|
)
|
|
parser.add_argument(
|
|
"-c", "--codeshare", help="load CODESHARE_URI", metavar="CODESHARE_URI", dest="codeshare_uri"
|
|
)
|
|
parser.add_argument("-e", "--eval", help="evaluate CODE", metavar="CODE", action="append", dest="eval_items")
|
|
parser.add_argument(
|
|
"-q",
|
|
help="quiet mode (no prompt) and quit after -l and -e",
|
|
action="store_true",
|
|
dest="quiet",
|
|
default=False,
|
|
)
|
|
parser.add_argument(
|
|
"-t", "--timeout", help="seconds to wait before terminating in quiet mode", dest="timeout", default=0
|
|
)
|
|
parser.add_argument(
|
|
"--pause",
|
|
help="leave main thread paused after spawning program",
|
|
action="store_const",
|
|
const="pause",
|
|
dest="on_spawn_complete",
|
|
default="resume",
|
|
)
|
|
parser.add_argument("-o", "--output", help="output to log file", dest="logfile")
|
|
parser.add_argument(
|
|
"--eternalize",
|
|
help="eternalize the script before exit",
|
|
action="store_true",
|
|
dest="eternalize",
|
|
default=False,
|
|
)
|
|
parser.add_argument(
|
|
"--exit-on-error",
|
|
help="exit with code 1 after encountering any exception in the SCRIPT",
|
|
action="store_true",
|
|
dest="exit_on_error",
|
|
default=False,
|
|
)
|
|
parser.add_argument(
|
|
"--kill-on-exit",
|
|
help="kill the spawned program when Frida exits",
|
|
action="store_true",
|
|
dest="kill_on_exit",
|
|
default=False,
|
|
)
|
|
parser.add_argument(
|
|
"--auto-perform",
|
|
help="wrap entered code with Java.perform",
|
|
action="store_true",
|
|
dest="autoperform",
|
|
default=False,
|
|
)
|
|
parser.add_argument(
|
|
"--auto-reload",
|
|
help="Enable auto reload of provided scripts and c module (on by default, will be required in the future)",
|
|
action="store_true",
|
|
dest="autoreload",
|
|
default=True,
|
|
)
|
|
parser.add_argument(
|
|
"--no-auto-reload",
|
|
help="Disable auto reload of provided scripts and c module",
|
|
action="store_false",
|
|
dest="autoreload",
|
|
default=True,
|
|
)
|
|
|
|
def _initialize(self, parser: argparse.ArgumentParser, options: argparse.Namespace, args: List[str]) -> None:
|
|
self._user_scripts = list(map(os.path.abspath, options.user_scripts))
|
|
for user_script in self._user_scripts:
|
|
with open(user_script, "r"):
|
|
pass
|
|
|
|
if options.user_parameters is not None:
|
|
try:
|
|
params = json.loads(options.user_parameters)
|
|
except Exception as e:
|
|
raise ValueError(f"failed to parse parameters argument as JSON: {e}")
|
|
if not isinstance(params, dict):
|
|
raise ValueError("failed to parse parameters argument as JSON: not an object")
|
|
self._user_parameters = params
|
|
else:
|
|
self._user_parameters = {}
|
|
|
|
if options.user_cmodule is not None:
|
|
self._user_cmodule = os.path.abspath(options.user_cmodule)
|
|
with open(self._user_cmodule, "rb"):
|
|
pass
|
|
else:
|
|
self._user_cmodule = None
|
|
self._toolchain = options.toolchain
|
|
|
|
self._codeshare_uri = options.codeshare_uri
|
|
self._codeshare_script: Optional[str] = None
|
|
|
|
self._pending_eval = options.eval_items
|
|
|
|
self._quiet = options.quiet
|
|
self._quiet_timeout = float(options.timeout)
|
|
self._on_spawn_complete = options.on_spawn_complete
|
|
self._eternalize = options.eternalize
|
|
self._exit_on_error = options.exit_on_error
|
|
self._kill_on_exit = options.kill_on_exit
|
|
self._autoperform_option = options.autoperform
|
|
self._autoreload = options.autoreload
|
|
|
|
self._logfile: Optional[codecs.StreamReaderWriter] = None
|
|
if options.logfile is not None:
|
|
self._logfile = codecs.open(options.logfile, "w", "utf-8")
|
|
|
|
def _log(self, level: str, text: str) -> None:
|
|
ConsoleApplication._log(self, level, text)
|
|
if self._logfile is not None:
|
|
self._logfile.write(text + "\n")
|
|
|
|
def _usage(self) -> str:
|
|
return "%(prog)s [options] target"
|
|
|
|
def _needs_target(self) -> bool:
|
|
return True
|
|
|
|
def _start(self) -> None:
|
|
self._set_autoperform(self._autoperform_option)
|
|
self._refresh_prompt()
|
|
|
|
if self._codeshare_uri is not None:
|
|
self._codeshare_script = self._load_codeshare_script(self._codeshare_uri)
|
|
if self._codeshare_script is None:
|
|
self._print("Exiting!")
|
|
self._exit(1)
|
|
return
|
|
|
|
try:
|
|
self._load_script()
|
|
except Exception as e:
|
|
self._update_status(f"Failed to load script: {e}")
|
|
self._exit(1)
|
|
return
|
|
|
|
if self._spawned_argv is not None or self._selected_spawn is not None:
|
|
command = (
|
|
" ".join(self._spawned_argv) if self._spawned_argv is not None else self._selected_spawn.identifier
|
|
)
|
|
if self._on_spawn_complete == "resume":
|
|
self._update_status(f"Spawned `{command}`. Resuming main thread!")
|
|
self._do_magic("resume")
|
|
else:
|
|
self._update_status(
|
|
"Spawned `{command}`. Use %resume to let the main thread start executing!".format(command=command)
|
|
)
|
|
else:
|
|
self._clear_status()
|
|
self._ready.set()
|
|
|
|
def _on_stop(self) -> None:
|
|
self._stopping.set()
|
|
|
|
if self._cli is not None:
|
|
try:
|
|
self._cli.app.exit()
|
|
except:
|
|
pass
|
|
|
|
def _stop(self) -> None:
|
|
if self._eternalize:
|
|
self._eternalize_script()
|
|
else:
|
|
self._unload_script()
|
|
|
|
with frida.Cancellable():
|
|
self._demonitor_all()
|
|
|
|
if self._logfile is not None:
|
|
self._logfile.close()
|
|
|
|
if self._kill_on_exit and self._spawned_pid is not None:
|
|
if self._session is not None:
|
|
self._session.detach()
|
|
self._device.kill(self._spawned_pid)
|
|
|
|
if not self._quiet:
|
|
self._print("\nThank you for using Frida!")
|
|
|
|
def _load_script(self) -> None:
|
|
if self._autoreload:
|
|
self._monitor_all()
|
|
|
|
is_first_load = self._script is None
|
|
|
|
assert self._session is not None
|
|
script = self._session.create_script(name="repl", source=self._create_repl_script(), runtime=self._runtime)
|
|
script.set_log_handler(self._log)
|
|
self._unload_script()
|
|
self._script = script
|
|
|
|
def on_message(message: Mapping[Any, Any], data: Any) -> None:
|
|
self._reactor.schedule(lambda: self._process_message(message, data))
|
|
|
|
script.on("message", on_message)
|
|
self._on_script_created(script)
|
|
script.load()
|
|
|
|
cmodule_code = self._load_cmodule_code()
|
|
if cmodule_code is not None:
|
|
# TODO: Remove this hack once RPC implementation supports passing binary data in both directions.
|
|
if isinstance(cmodule_code, bytes):
|
|
script.post({"type": "frida:cmodule-payload"}, data=cmodule_code)
|
|
cmodule_code = None
|
|
script.exports_sync.frida_load_cmodule(cmodule_code, self._toolchain)
|
|
|
|
stage = "early" if self._target[0] == "file" and is_first_load else "late"
|
|
try:
|
|
script.exports_sync.init(stage, self._user_parameters)
|
|
except:
|
|
pass
|
|
|
|
def _get_script_name(self, path: str) -> str:
|
|
return os.path.splitext(os.path.basename(path))[0]
|
|
|
|
def _eternalize_script(self) -> None:
|
|
if self._script is None:
|
|
return
|
|
|
|
try:
|
|
self._script.eternalize()
|
|
except:
|
|
pass
|
|
self._script = None
|
|
|
|
def _unload_script(self) -> None:
|
|
if self._script is None:
|
|
return
|
|
|
|
try:
|
|
self._script.unload()
|
|
except:
|
|
pass
|
|
self._script = None
|
|
|
|
def _monitor_all(self) -> None:
|
|
for path in self._user_scripts + [self._user_cmodule]:
|
|
self._monitor(path)
|
|
|
|
def _demonitor_all(self) -> None:
|
|
for monitor in self._monitored_files.values():
|
|
monitor.disable()
|
|
self._monitored_files = {}
|
|
|
|
def _monitor(self, path: AnyStr) -> None:
|
|
if path is None or path in self._monitored_files or script_needs_compilation(path):
|
|
return
|
|
|
|
monitor = frida.FileMonitor(path)
|
|
monitor.on("change", self._on_change)
|
|
monitor.enable()
|
|
self._monitored_files[path] = monitor
|
|
|
|
def _process_input(self, reactor: Reactor) -> None:
|
|
if not self._quiet:
|
|
self._print_startup_message()
|
|
|
|
try:
|
|
while self._ready.wait(0.5) != True:
|
|
if not reactor.is_running():
|
|
return
|
|
except KeyboardInterrupt:
|
|
self._reactor.cancel_io()
|
|
return
|
|
|
|
while True:
|
|
expression = ""
|
|
line = ""
|
|
while len(expression) == 0 or line.endswith("\\"):
|
|
if not reactor.is_running():
|
|
return
|
|
|
|
prompt = f"[{self._prompt_string}]" + "-> " if len(expression) == 0 else "... "
|
|
|
|
pending_eval = self._pending_eval
|
|
if pending_eval is not None:
|
|
if len(pending_eval) > 0:
|
|
expression = pending_eval.pop(0)
|
|
if not self._quiet:
|
|
self._print(prompt + expression)
|
|
else:
|
|
self._pending_eval = None
|
|
else:
|
|
if self._quiet:
|
|
if self._quiet_timeout > 0:
|
|
if self._quiet_start is None:
|
|
self._quiet_start = time.time()
|
|
passed_time = time.time() - self._quiet_start
|
|
while self._quiet_timeout > passed_time and reactor.is_running():
|
|
sleep_time = min(1, self._quiet_timeout - passed_time)
|
|
if self._stopping.wait(sleep_time):
|
|
break
|
|
if self._dumb_stdin_reader is not None:
|
|
with self._dumb_stdin_reader._lock:
|
|
if self._dumb_stdin_reader._saw_sigint:
|
|
break
|
|
passed_time = time.time() - self._quiet_start
|
|
|
|
self._exit_status = 0 if self._errors == 0 else 1
|
|
return
|
|
|
|
try:
|
|
if self._cli is not None:
|
|
line = self._cli.prompt(prompt)
|
|
if line is None:
|
|
return
|
|
else:
|
|
assert self._dumb_stdin_reader is not None
|
|
line = self._dumb_stdin_reader.read_line(prompt)
|
|
self._print(line)
|
|
except EOFError:
|
|
if not self._have_terminal and os.environ.get("TERM", "") != "dumb":
|
|
while not self._stopping.wait(1):
|
|
pass
|
|
return
|
|
except KeyboardInterrupt:
|
|
line = ""
|
|
if not self._have_terminal:
|
|
sys.stdout.write("\n" + prompt)
|
|
continue
|
|
if len(line.strip()) > 0:
|
|
if len(expression) > 0:
|
|
expression += "\n"
|
|
expression += line.rstrip("\\")
|
|
|
|
if expression.endswith("?"):
|
|
try:
|
|
self._print_help(expression)
|
|
except JavaScriptError as e:
|
|
error = e.error
|
|
self._print(Style.BRIGHT + error["name"] + Style.RESET_ALL + ": " + error["message"])
|
|
except frida.InvalidOperationError:
|
|
return
|
|
elif expression == "help":
|
|
self._do_magic("help")
|
|
elif expression in ("exit", "quit", "q"):
|
|
return
|
|
else:
|
|
try:
|
|
if expression.startswith("%"):
|
|
self._do_magic(expression[1:].rstrip())
|
|
elif expression.startswith("."):
|
|
self._do_quick_command(expression[1:].rstrip())
|
|
else:
|
|
if self._autoperform:
|
|
expression = f"Java.performNow(() => {{ return {expression}\n/**/ }});"
|
|
if not self._exec_and_print(self._evaluate_expression, expression):
|
|
self._errors += 1
|
|
except frida.OperationCancelledError:
|
|
return
|
|
|
|
def _get_confirmation(self, question: str, default_answer: bool = False) -> bool:
|
|
if default_answer:
|
|
prompt_string = question + " [Y/n] "
|
|
else:
|
|
prompt_string = question + " [y/N] "
|
|
|
|
if self._have_terminal and not self._plain_terminal:
|
|
answer = prompt(prompt_string)
|
|
else:
|
|
answer = self._dumb_stdin_reader.read_line(prompt_string)
|
|
self._print(answer)
|
|
|
|
if answer.lower() not in ("y", "yes", "n", "no", ""):
|
|
return self._get_confirmation(question, default_answer=default_answer)
|
|
|
|
if default_answer:
|
|
return answer.lower() != "n" and answer.lower() != "no"
|
|
|
|
return answer.lower() == "y" or answer.lower() == "yes"
|
|
|
|
def _exec_and_print(self, exec: Callable[[T], Tuple[str, bytes]], arg: T) -> bool:
|
|
success = False
|
|
try:
|
|
(t, value) = self._perform_on_reactor_thread(lambda: exec(arg))
|
|
if t in ("function", "undefined", "null"):
|
|
output = t
|
|
elif t == "binary":
|
|
output = hexdump(value).rstrip("\n")
|
|
else:
|
|
output = json.dumps(value, sort_keys=True, indent=4, separators=(",", ": "))
|
|
success = True
|
|
except JavaScriptError as e:
|
|
error = e.error
|
|
|
|
output = Fore.RED + Style.BRIGHT + error["name"] + Style.RESET_ALL + ": " + error["message"]
|
|
|
|
stack = error.get("stack", None)
|
|
if stack is not None:
|
|
message_len = len(error["message"].split("\n"))
|
|
trim_amount = 6 if self._runtime == "v8" else 7
|
|
trimmed_stack = stack.split("\n")[message_len:-trim_amount]
|
|
if len(trimmed_stack) > 0:
|
|
output += "\n" + "\n".join(trimmed_stack)
|
|
except frida.InvalidOperationError:
|
|
return success
|
|
if output != "undefined":
|
|
self._print(output)
|
|
return success
|
|
|
|
def _print_startup_message(self) -> None:
|
|
self._print(
|
|
"""\
|
|
____
|
|
/ _ | Frida {version} - A world-class dynamic instrumentation toolkit
|
|
| (_| |
|
|
> _ | Commands:
|
|
/_/ |_| help -> Displays the help system
|
|
. . . . object? -> Display information about 'object'
|
|
. . . . exit/quit -> Exit
|
|
. . . .
|
|
. . . . More info at https://frida.re/docs/home/""".format(
|
|
version=frida.__version__
|
|
)
|
|
)
|
|
|
|
def _print_help(self, expression: str) -> None:
|
|
# TODO: Figure out docstrings and implement here. This is real jankaty right now.
|
|
help_text = ""
|
|
if expression.endswith(".?"):
|
|
expression = expression[:-2] + "?"
|
|
|
|
obj_to_identify = [x for x in expression.split(" ") if x.endswith("?")][0][:-1]
|
|
(obj_type, obj_value) = self._evaluate_expression(obj_to_identify)
|
|
|
|
if obj_type == "function":
|
|
signature = self._evaluate_expression("%s.toString()" % obj_to_identify)[1].decode()
|
|
clean_signature = signature.split("{")[0][:-1].split("function ")[-1]
|
|
|
|
if "[native code]" in signature:
|
|
help_text += "Type: Function (native)\n"
|
|
else:
|
|
help_text += "Type: Function\n"
|
|
|
|
help_text += f"Signature: {clean_signature}\n"
|
|
help_text += "Docstring: #TODO :)"
|
|
|
|
elif obj_type == "object":
|
|
help_text += "Type: Object\n"
|
|
help_text += "Docstring: #TODO :)"
|
|
|
|
elif obj_type == "boolean":
|
|
help_text += "Type: Boolean\n"
|
|
help_text += "Docstring: #TODO :)"
|
|
|
|
elif obj_type == "string":
|
|
bool_text = self._evaluate_expression(obj_to_identify + ".toString()")[1]
|
|
help_text += "Type: Boolean\n"
|
|
help_text += f"Text: {bool_text.decode()}\n"
|
|
help_text += "Docstring: #TODO :)"
|
|
|
|
self._print(help_text)
|
|
|
|
# Negative means at least abs(val) - 1
|
|
_magic_command_args = {
|
|
"resume": _repl_magic.Resume(),
|
|
"load": _repl_magic.Load(),
|
|
"reload": _repl_magic.Reload(),
|
|
"unload": _repl_magic.Unload(),
|
|
"autoperform": _repl_magic.Autoperform(),
|
|
"autoreload": _repl_magic.Autoreload(),
|
|
"exec": _repl_magic.Exec(),
|
|
"time": _repl_magic.Time(),
|
|
"help": _repl_magic.Help(),
|
|
}
|
|
|
|
def _do_magic(self, statement: str) -> None:
|
|
tokens = shlex.split(statement)
|
|
command = tokens[0]
|
|
args = tokens[1:]
|
|
|
|
magic_command = self._magic_command_args.get(command)
|
|
if magic_command is None:
|
|
self._print(f"Unknown command: {command}")
|
|
self._print("Valid commands: {}".format(", ".join(self._magic_command_args.keys())))
|
|
return
|
|
|
|
required_args = magic_command.required_args_count
|
|
atleast_args = False
|
|
if required_args < 0:
|
|
atleast_args = True
|
|
required_args = abs(required_args) - 1
|
|
|
|
if (not atleast_args and len(args) != required_args) or (atleast_args and len(args) < required_args):
|
|
self._print(
|
|
"{cmd} command expects {atleast}{n} argument{s}".format(
|
|
cmd=command,
|
|
atleast="atleast " if atleast_args else "",
|
|
n=required_args,
|
|
s="" if required_args == 1 else " ",
|
|
)
|
|
)
|
|
return
|
|
|
|
magic_command.execute(self, args)
|
|
|
|
def _do_quick_command(self, statement: str) -> None:
|
|
tokens = shlex.split(statement)
|
|
if len(tokens) == 0:
|
|
self._print("Invalid quick command")
|
|
return
|
|
|
|
if not self._exec_and_print(self._evaluate_quick_command, tokens):
|
|
self._errors += 1
|
|
|
|
def _autoperform_command(self, state_argument: str) -> None:
|
|
if state_argument not in ("on", "off"):
|
|
self._print("autoperform only accepts on and off as parameters")
|
|
return
|
|
self._set_autoperform(state_argument == "on")
|
|
|
|
def _set_autoperform(self, state: bool) -> None:
|
|
if self._is_java_available():
|
|
self._autoperform = state
|
|
self._refresh_prompt()
|
|
elif state:
|
|
self._print("autoperform is only available in Java processes")
|
|
|
|
def _is_java_available(self) -> bool:
|
|
assert self._session is not None
|
|
script = None
|
|
try:
|
|
script = self._session.create_script(
|
|
name="java_check", source="rpc.exports.javaAvailable = () => Java.available;", runtime=self._runtime
|
|
)
|
|
script.load()
|
|
return script.exports_sync.java_available()
|
|
except:
|
|
return False
|
|
finally:
|
|
if script is not None:
|
|
script.unload()
|
|
|
|
def _refresh_prompt(self) -> None:
|
|
self._prompt_string = self._create_prompt()
|
|
|
|
def _create_prompt(self) -> str:
|
|
assert self._device is not None
|
|
device_type = self._device.type
|
|
type_name = self._target[0]
|
|
if type_name == "pid":
|
|
if self._target[1] == 0:
|
|
target = "SystemSession"
|
|
else:
|
|
target = "PID::%u" % self._target[1]
|
|
elif type_name == "file":
|
|
target = os.path.basename(self._target[1][0])
|
|
else:
|
|
target = self._target[1]
|
|
|
|
suffix = ""
|
|
if self._autoperform:
|
|
suffix = "(ap)"
|
|
|
|
if device_type in ("local", "remote"):
|
|
prompt_string = "%s::%s %s" % (device_type.title(), target, suffix)
|
|
else:
|
|
prompt_string = "%s::%s %s" % (self._device.name, target, suffix)
|
|
|
|
return prompt_string
|
|
|
|
def _evaluate_expression(self, expression: str) -> Tuple[str, bytes]:
|
|
assert self._script is not None
|
|
result = self._script.exports_sync.frida_evaluate_expression(expression)
|
|
return self._parse_evaluate_result(result)
|
|
|
|
def _evaluate_quick_command(self, tokens: List[str]) -> Tuple[str, bytes]:
|
|
assert self._script is not None
|
|
result = self._script.exports_sync.frida_evaluate_quick_command(tokens)
|
|
return self._parse_evaluate_result(result)
|
|
|
|
def _parse_evaluate_result(self, result: Union[bytes, Mapping[Any, Any], Tuple[str, bytes]]) -> Tuple[str, bytes]:
|
|
if isinstance(result, bytes):
|
|
return ("binary", result)
|
|
elif isinstance(result, dict):
|
|
return ("binary", bytes())
|
|
elif result[0] == "error":
|
|
raise JavaScriptError(result[1])
|
|
return (result[0], result[1])
|
|
|
|
def _process_message(self, message: Mapping[Any, Any], data: Any) -> None:
|
|
message_type = message["type"]
|
|
if message_type == "error":
|
|
text = message.get("stack", message["description"])
|
|
self._log("error", text)
|
|
self._errors += 1
|
|
if self._exit_on_error:
|
|
self._exit(1)
|
|
else:
|
|
self._print("message:", message, "data:", data)
|
|
|
|
def _on_change(self, changed_file, other_file, event_type) -> None:
|
|
if event_type == "changes-done-hint":
|
|
return
|
|
self._last_change_id += 1
|
|
change_id = self._last_change_id
|
|
self._reactor.schedule(lambda: self._process_change(change_id), delay=0.05)
|
|
|
|
def _process_change(self, change_id: int) -> None:
|
|
if change_id != self._last_change_id:
|
|
return
|
|
self._try_load_script()
|
|
|
|
def _try_load_script(self) -> None:
|
|
try:
|
|
self._load_script()
|
|
except Exception as e:
|
|
self._print(f"Failed to load script: {e}")
|
|
|
|
def _create_repl_script(self) -> str:
|
|
raw_fragments = []
|
|
|
|
raw_fragments.append(self._make_repl_runtime())
|
|
|
|
if self._codeshare_script is not None:
|
|
raw_fragments.append(
|
|
self._wrap_user_script(f"/codeshare.frida.re/{self._codeshare_uri}.js", self._codeshare_script)
|
|
)
|
|
|
|
for user_script in self._user_scripts:
|
|
if script_needs_compilation(user_script):
|
|
compilation_started = None
|
|
|
|
context = self._compilers.get(user_script, None)
|
|
if context is None:
|
|
context = CompilerContext(user_script, self._autoreload, self._on_bundle_updated)
|
|
context.compiler.on("diagnostics", self._on_compiler_diagnostics)
|
|
self._compilers[user_script] = context
|
|
self._update_status(format_compiling(user_script, os.getcwd()))
|
|
compilation_started = timer()
|
|
|
|
raw_fragments.append(context.get_bundle())
|
|
|
|
if compilation_started is not None:
|
|
compilation_finished = timer()
|
|
self._update_status(
|
|
format_compiled(user_script, os.getcwd(), compilation_started, compilation_finished)
|
|
)
|
|
else:
|
|
with codecs.open(user_script, "rb", "utf-8") as f:
|
|
raw_fragments.append(self._wrap_user_script(user_script, f.read()))
|
|
|
|
fragments = []
|
|
next_script_id = 1
|
|
for raw_fragment in raw_fragments:
|
|
if raw_fragment.startswith("📦\n"):
|
|
fragments.append(raw_fragment[2:])
|
|
else:
|
|
script_id = next_script_id
|
|
next_script_id += 1
|
|
size = len(raw_fragment.encode("utf-8"))
|
|
fragments.append(f"{size} /frida/repl-{script_id}.js\n✄\n{raw_fragment}")
|
|
|
|
return "📦\n" + "\n✄\n".join(fragments)
|
|
|
|
def _wrap_user_script(self, name, script):
|
|
if script.startswith("📦\n"):
|
|
return script
|
|
return f"Script.evaluate({json.dumps(name)}, {json.dumps(script)});"
|
|
|
|
def _on_bundle_updated(self) -> None:
|
|
self._reactor.schedule(lambda: self._try_load_script())
|
|
|
|
def _on_compiler_diagnostics(self, diagnostics) -> None:
|
|
self._reactor.schedule(lambda: self._print_compiler_diagnostics(diagnostics))
|
|
|
|
def _print_compiler_diagnostics(self, diagnostics) -> None:
|
|
cwd = os.getcwd()
|
|
for diag in diagnostics:
|
|
self._print(format_diagnostic(diag, cwd))
|
|
|
|
def _make_repl_runtime(self) -> str:
|
|
return """\
|
|
global.cm = null;
|
|
global.cs = {};
|
|
|
|
class REPL {
|
|
#quickCommands;
|
|
constructor() {
|
|
this.#quickCommands = new Map();
|
|
}
|
|
registerQuickCommand(name, handler) {
|
|
this.#quickCommands.set(name, handler);
|
|
}
|
|
unregisterQuickCommand(name) {
|
|
this.#quickCommands.delete(name);
|
|
}
|
|
_invokeQuickCommand(tokens) {
|
|
const name = tokens[0];
|
|
const handler = this.#quickCommands.get(name);
|
|
if (handler !== undefined) {
|
|
const { minArity, onInvoke } = handler;
|
|
if (tokens.length - 1 < minArity) {
|
|
throw Error(`${name} needs at least ${minArity} arg${(minArity === 1) ? '' : 's'}`);
|
|
}
|
|
return onInvoke(...tokens.slice(1));
|
|
} else {
|
|
throw Error(`Unknown command ${name}`);
|
|
}
|
|
}
|
|
}
|
|
const repl = new REPL();
|
|
global.REPL = repl;
|
|
|
|
const rpcExports = {
|
|
fridaEvaluateExpression(expression) {
|
|
return evaluate(() => (1, eval)(expression));
|
|
},
|
|
fridaEvaluateQuickCommand(tokens) {
|
|
return evaluate(() => repl._invokeQuickCommand(tokens));
|
|
},
|
|
fridaLoadCmodule(code, toolchain) {
|
|
const cs = global.cs;
|
|
|
|
if (cs._frida_log === undefined)
|
|
cs._frida_log = new NativeCallback(onLog, 'void', ['pointer']);
|
|
|
|
if (code === null) {
|
|
recv('frida:cmodule-payload', (message, data) => {
|
|
code = data;
|
|
});
|
|
}
|
|
|
|
global.cm = new CModule(code, cs, { toolchain });
|
|
},
|
|
};
|
|
|
|
function evaluate(func) {
|
|
try {
|
|
const result = func();
|
|
if (result instanceof ArrayBuffer) {
|
|
return result;
|
|
} else {
|
|
const type = (result === null) ? 'null' : typeof result;
|
|
return [type, result];
|
|
}
|
|
} catch (e) {
|
|
return ['error', {
|
|
name: e.name,
|
|
message: e.message,
|
|
stack: e.stack
|
|
}];
|
|
}
|
|
}
|
|
|
|
Object.defineProperty(rpc, 'exports', {
|
|
get() {
|
|
return rpcExports;
|
|
},
|
|
set(value) {
|
|
for (const [k, v] of Object.entries(value)) {
|
|
rpcExports[k] = v;
|
|
}
|
|
}
|
|
});
|
|
|
|
function onLog(messagePtr) {
|
|
const message = messagePtr.readUtf8String();
|
|
console.log(message);
|
|
}
|
|
"""
|
|
|
|
def _load_cmodule_code(self) -> Union[str, bytes, None]:
|
|
if self._user_cmodule is None:
|
|
return None
|
|
|
|
with open(self._user_cmodule, "rb") as f:
|
|
code = f.read()
|
|
if code_is_native(code):
|
|
return code
|
|
source = code.decode("utf-8")
|
|
|
|
name = os.path.basename(self._user_cmodule)
|
|
|
|
return (
|
|
"""static void frida_log (const char * format, ...);\n#line 1 "{name}"\n""".format(name=name)
|
|
+ source
|
|
+ """\
|
|
#line 1 "frida-repl-builtins.c"
|
|
#include <glib.h>
|
|
|
|
extern void _frida_log (const gchar * message);
|
|
|
|
static void
|
|
frida_log (const char * format,
|
|
...)
|
|
{
|
|
gchar * message;
|
|
va_list args;
|
|
|
|
va_start (args, format);
|
|
message = g_strdup_vprintf (format, args);
|
|
va_end (args);
|
|
|
|
_frida_log (message);
|
|
|
|
g_free (message);
|
|
}
|
|
"""
|
|
)
|
|
|
|
def _load_codeshare_script(self, uri: str) -> Optional[str]:
|
|
trust_store = self._get_or_create_truststore()
|
|
|
|
project_url = f"https://codeshare.frida.re/api/project/{uri}/"
|
|
response_json = None
|
|
try:
|
|
request = build_opener()
|
|
request.addheaders = [("User-Agent", f"Frida v{frida.__version__} | {platform.platform()}")]
|
|
response = request.open(project_url)
|
|
response_content = response.read().decode("utf-8")
|
|
response_json = json.loads(response_content)
|
|
except Exception as e:
|
|
self._print(f"Got an unhandled exception while trying to retrieve {uri} - {e}")
|
|
return None
|
|
|
|
trusted_signature = trust_store.get(uri, "")
|
|
fingerprint = hashlib.sha256(response_json["source"].encode("utf-8")).hexdigest()
|
|
if fingerprint == trusted_signature:
|
|
return response_json["source"]
|
|
|
|
self._print(
|
|
"""Hello! This is the first time you're running this particular snippet, or the snippet's source code has changed.
|
|
|
|
Project Name: {project_name}
|
|
Author: {author}
|
|
Slug: {slug}
|
|
Fingerprint: {fingerprint}
|
|
URL: {url}
|
|
""".format(
|
|
project_name=response_json["project_name"],
|
|
author="@" + uri.split("/")[0],
|
|
slug=uri,
|
|
fingerprint=fingerprint,
|
|
url=f"https://codeshare.frida.re/@{uri}",
|
|
)
|
|
)
|
|
|
|
answer = self._get_confirmation("Are you sure you'd like to trust this project?")
|
|
if answer:
|
|
self._print(
|
|
"Adding fingerprint {} to the trust store! You won't be prompted again unless the code changes.".format(
|
|
fingerprint
|
|
)
|
|
)
|
|
script = response_json["source"]
|
|
self._update_truststore({uri: fingerprint})
|
|
if not isinstance(script, str):
|
|
raise ValueError("Expected the script source to be string")
|
|
return script
|
|
|
|
return None
|
|
|
|
def _update_truststore(self, record: Mapping[str, str]) -> None:
|
|
trust_store = self._get_or_create_truststore()
|
|
trust_store.update(record)
|
|
|
|
codeshare_trust_store = self._get_or_create_truststore_file()
|
|
|
|
with open(codeshare_trust_store, "w") as f:
|
|
f.write(json.dumps(trust_store))
|
|
|
|
def _get_or_create_truststore(self) -> None:
|
|
codeshare_trust_store = self._get_or_create_truststore_file()
|
|
|
|
if os.path.exists(codeshare_trust_store):
|
|
try:
|
|
with open(codeshare_trust_store) as f:
|
|
trust_store = json.load(f)
|
|
except Exception as e:
|
|
self._print(
|
|
"Unable to load the codeshare truststore ({}), defaulting to an empty truststore. You will be prompted every time you want to run a script!".format(
|
|
e
|
|
)
|
|
)
|
|
trust_store = {}
|
|
else:
|
|
with open(codeshare_trust_store, "w") as f:
|
|
f.write(json.dumps({}))
|
|
trust_store = {}
|
|
|
|
return trust_store
|
|
|
|
def _get_or_create_truststore_file(self) -> str:
|
|
truststore_file = os.path.join(self._get_or_create_data_dir(), "codeshare-truststore.json")
|
|
if not os.path.isfile(truststore_file):
|
|
self._migrate_old_config_file("codeshare-truststore.json", truststore_file)
|
|
return truststore_file
|
|
|
|
def _get_or_create_history_file(self) -> str:
|
|
history_file = os.path.join(self._get_or_create_state_dir(), "history")
|
|
if os.path.isfile(history_file):
|
|
return history_file
|
|
|
|
found_old = self._migrate_old_config_file("history", history_file)
|
|
if not found_old:
|
|
open(history_file, "a").close()
|
|
|
|
return history_file
|
|
|
|
def _migrate_old_config_file(self, name: str, new_path: str) -> bool:
|
|
xdg_config_home = os.getenv("XDG_CONFIG_HOME")
|
|
if xdg_config_home is not None:
|
|
old_file = os.path.exists(os.path.join(xdg_config_home, "frida", name))
|
|
if os.path.isfile(old_file):
|
|
os.rename(old_file, new_path)
|
|
return True
|
|
|
|
old_file = os.path.join(os.path.expanduser("~"), ".frida", name)
|
|
if os.path.isfile(old_file):
|
|
os.rename(old_file, new_path)
|
|
return True
|
|
|
|
return False
|
|
|
|
def _on_device_found(self) -> None:
|
|
assert self._device is not None
|
|
if not self._quiet:
|
|
self._print(
|
|
"""\
|
|
. . . .
|
|
. . . . Connected to {device_name} (id={device_id})""".format(
|
|
device_id=self._device.id, device_name=self._device.name
|
|
)
|
|
)
|
|
|
|
|
|
class CompilerContext:
|
|
def __init__(self, user_script, autoreload, on_bundle_updated) -> None:
|
|
self._user_script = user_script
|
|
self._project_root = os.getcwd()
|
|
self._autoreload = autoreload
|
|
self._on_bundle_updated = on_bundle_updated
|
|
|
|
self.compiler = frida.Compiler()
|
|
self._bundle = None
|
|
|
|
def get_bundle(self) -> str:
|
|
compiler = self.compiler
|
|
|
|
if not self._autoreload:
|
|
return compiler.build(self._user_script, project_root=self._project_root)
|
|
|
|
if self._bundle is None:
|
|
ready = threading.Event()
|
|
|
|
def on_compiler_output(bundle) -> None:
|
|
is_initial_update = self._bundle is None
|
|
self._bundle = bundle
|
|
if is_initial_update:
|
|
ready.set()
|
|
else:
|
|
self._on_bundle_updated()
|
|
|
|
compiler.on("output", on_compiler_output)
|
|
compiler.watch(self._user_script, project_root=self._project_root)
|
|
ready.wait()
|
|
|
|
return self._bundle
|
|
|
|
|
|
class FridaCompleter(Completer):
|
|
def __init__(self, repl: REPLApplication) -> None:
|
|
self._repl = repl
|
|
self._lexer = JavascriptLexer()
|
|
|
|
def get_completions(self, document: Document, complete_event: CompleteEvent) -> Iterable[Completion]:
|
|
prefix = document.text_before_cursor
|
|
|
|
magic = len(prefix) > 0 and prefix[0] == "%" and not any(map(lambda c: c.isspace(), prefix))
|
|
|
|
tokens = list(self._lexer.get_tokens(prefix))[:-1]
|
|
|
|
# 0.toString() is invalid syntax,
|
|
# but pygments doesn't seem to know that
|
|
for i in range(len(tokens) - 1):
|
|
if (
|
|
tokens[i][0] == Token.Literal.Number.Integer
|
|
and tokens[i + 1][0] == Token.Punctuation
|
|
and tokens[i + 1][1] == "."
|
|
):
|
|
tokens[i] = (Token.Literal.Number.Float, tokens[i][1] + tokens[i + 1][1])
|
|
del tokens[i + 1]
|
|
|
|
before_dot = ""
|
|
after_dot = ""
|
|
encountered_dot = False
|
|
for t in tokens[::-1]:
|
|
if t[0] in Token.Name.subtypes:
|
|
before_dot = t[1] + before_dot
|
|
elif t[0] == Token.Punctuation and t[1] == ".":
|
|
before_dot = "." + before_dot
|
|
if not encountered_dot:
|
|
encountered_dot = True
|
|
after_dot = before_dot[1:]
|
|
before_dot = ""
|
|
else:
|
|
if encountered_dot:
|
|
# The value/contents of the string, number or array doesn't matter,
|
|
# so we just use the simplest value with that type
|
|
if t[0] in Token.Literal.String.subtypes:
|
|
before_dot = '""' + before_dot
|
|
elif t[0] in Token.Literal.Number.subtypes:
|
|
before_dot = "0.0" + before_dot
|
|
elif t[0] == Token.Punctuation and t[1] == "]":
|
|
before_dot = "[]" + before_dot
|
|
elif t[0] == Token.Punctuation and t[1] == ")":
|
|
# we don't know the returned value of the function call so we abort the completion
|
|
return
|
|
|
|
break
|
|
|
|
try:
|
|
if encountered_dot:
|
|
if before_dot == "" or before_dot.endswith("."):
|
|
return
|
|
for key in self._get_keys(
|
|
"""\
|
|
(() => {
|
|
let o;
|
|
try {
|
|
o = """
|
|
+ before_dot
|
|
+ """;
|
|
} catch (e) {
|
|
return [];
|
|
}
|
|
|
|
if (o === undefined || o === null)
|
|
return [];
|
|
|
|
let k = Object.getOwnPropertyNames(o);
|
|
|
|
let p;
|
|
if (typeof o !== 'object')
|
|
p = o.__proto__;
|
|
else
|
|
p = Object.getPrototypeOf(o);
|
|
if (p !== null && p !== undefined)
|
|
k = k.concat(Object.getOwnPropertyNames(p));
|
|
|
|
return k;
|
|
})();"""
|
|
):
|
|
if self._pattern_matches(after_dot, key):
|
|
yield Completion(key, -len(after_dot))
|
|
else:
|
|
if magic:
|
|
keys = self._repl._magic_command_args.keys()
|
|
else:
|
|
keys = self._get_keys("Object.getOwnPropertyNames(this)")
|
|
for key in keys:
|
|
if not self._pattern_matches(before_dot, key) or (key.startswith("_") and before_dot == ""):
|
|
continue
|
|
yield Completion(key, -len(before_dot))
|
|
except frida.InvalidOperationError:
|
|
pass
|
|
except frida.OperationCancelledError:
|
|
pass
|
|
except Exception as e:
|
|
self._repl._print(e)
|
|
|
|
def _get_keys(self, code):
|
|
repl = self._repl
|
|
with repl._reactor.io_cancellable:
|
|
(t, value) = repl._evaluate_expression(code)
|
|
|
|
if t == "error":
|
|
return []
|
|
|
|
return sorted(filter(self._is_valid_name, set(value)))
|
|
|
|
def _is_valid_name(self, name) -> bool:
|
|
tokens = list(self._lexer.get_tokens(name))
|
|
return len(tokens) == 2 and tokens[0][0] in Token.Name.subtypes
|
|
|
|
def _pattern_matches(self, pattern: str, text: str) -> bool:
|
|
return re.search(re.escape(pattern), text, re.IGNORECASE) is not None
|
|
|
|
|
|
def script_needs_compilation(path: AnyStr) -> bool:
|
|
if isinstance(path, str):
|
|
return path.endswith(".ts")
|
|
return path.endswith(b".ts")
|
|
|
|
|
|
def hexdump(src, length: int = 16) -> str:
|
|
FILTER = "".join([(len(repr(chr(x))) == 3) and chr(x) or "." for x in range(256)])
|
|
lines = []
|
|
for c in range(0, len(src), length):
|
|
chars = src[c : c + length]
|
|
hex = " ".join(["%02x" % x for x in iter(chars)])
|
|
printable = "".join(["%s" % ((x <= 127 and FILTER[x]) or ".") for x in iter(chars)])
|
|
lines.append("%04x %-*s %s\n" % (c, length * 3, hex, printable))
|
|
return "".join(lines)
|
|
|
|
|
|
OS_BINARY_SIGNATURES = {
|
|
b"\x4d\x5a", # PE
|
|
b"\xca\xfe\xba\xbe", # Fat Mach-O
|
|
b"\xcf\xfa\xed\xfe", # Mach-O
|
|
b"\x7fELF", # ELF
|
|
}
|
|
|
|
|
|
def code_is_native(code: bytes) -> bool:
|
|
return (code[:4] in OS_BINARY_SIGNATURES) or (code[:2] in OS_BINARY_SIGNATURES)
|
|
|
|
|
|
class JavaScriptError(Exception):
|
|
def __init__(self, error) -> None:
|
|
super().__init__(error["message"])
|
|
|
|
self.error = error
|
|
|
|
|
|
class DumbStdinReader:
|
|
def __init__(self, valid_until: Callable[[], bool]) -> None:
|
|
self._valid_until = valid_until
|
|
|
|
self._saw_sigint = False
|
|
self._prompt: Optional[str] = None
|
|
self._result: Optional[Tuple[Optional[str], Optional[Exception]]] = None
|
|
self._lock = threading.Lock()
|
|
self._cond = threading.Condition(self._lock)
|
|
self._get_input = input
|
|
|
|
worker = threading.Thread(target=self._process_requests, name="stdin-reader")
|
|
worker.daemon = True
|
|
worker.start()
|
|
|
|
signal.signal(signal.SIGINT, lambda n, f: self._cancel_line())
|
|
|
|
def read_line(self, prompt_string: str) -> str:
|
|
with self._lock:
|
|
self._prompt = prompt_string
|
|
self._cond.notify()
|
|
|
|
with self._lock:
|
|
while self._result is None:
|
|
if self._valid_until():
|
|
raise EOFError()
|
|
self._cond.wait(1)
|
|
line, error = self._result
|
|
self._result = None
|
|
|
|
if error is not None:
|
|
raise error
|
|
|
|
assert isinstance(line, str)
|
|
return line
|
|
|
|
def _process_requests(self) -> None:
|
|
error = None
|
|
while error is None:
|
|
with self._lock:
|
|
while self._prompt is None:
|
|
self._cond.wait()
|
|
prompt = self._prompt
|
|
|
|
try:
|
|
line = self._get_input(prompt)
|
|
except Exception as e:
|
|
line = None
|
|
error = e
|
|
|
|
with self._lock:
|
|
self._prompt = None
|
|
self._result = (line, error)
|
|
self._cond.notify()
|
|
|
|
def _cancel_line(self) -> None:
|
|
with self._lock:
|
|
self._saw_sigint = True
|
|
self._prompt = None
|
|
self._result = (None, KeyboardInterrupt())
|
|
self._cond.notify()
|
|
|
|
|
|
if os.environ.get("TERM", "") == "dumb":
|
|
try:
|
|
from collections import namedtuple
|
|
|
|
from epc.client import EPCClient
|
|
except ImportError:
|
|
|
|
def start_completion_thread(repl: REPLApplication, epc_port=None) -> None:
|
|
# Do nothing when we cannot import the EPC module.
|
|
_, _ = repl, epc_port
|
|
|
|
else:
|
|
|
|
class EPCCompletionClient(EPCClient):
|
|
def __init__(self, address="localhost", port=None, *args, **kargs) -> None:
|
|
if port is not None:
|
|
args = ((address, port),) + args
|
|
EPCClient.__init__(self, *args, **kargs)
|
|
|
|
def complete(*cargs, **ckargs):
|
|
return self.complete(*cargs, **ckargs)
|
|
|
|
self.register_function(complete)
|
|
|
|
EpcDocument = namedtuple(
|
|
"EpcDocument",
|
|
[
|
|
"text_before_cursor",
|
|
],
|
|
)
|
|
|
|
SYMBOL_CHARS = "._" + string.ascii_letters + string.digits
|
|
FIRST_SYMBOL_CHARS = "_" + string.ascii_letters
|
|
|
|
class ReplEPCCompletion:
|
|
def __init__(self, repl: "REPLApplication", *args, **kargs) -> None:
|
|
_, _ = args, kargs
|
|
self._repl = repl
|
|
|
|
def complete(self, *to_complete):
|
|
to_complete = "".join(to_complete)
|
|
prefix = ""
|
|
if len(to_complete) != 0:
|
|
for i, x in enumerate(to_complete[::-1]):
|
|
if x not in SYMBOL_CHARS:
|
|
while i >= 0 and to_complete[-i] not in FIRST_SYMBOL_CHARS:
|
|
i -= 1
|
|
prefix, to_complete = to_complete[:-i], to_complete[-i:]
|
|
break
|
|
pos = len(prefix)
|
|
if "." in to_complete:
|
|
prefix += to_complete.rsplit(".", 1)[0] + "."
|
|
try:
|
|
completions = self._repl._completer.get_completions(
|
|
EpcDocument(text_before_cursor=to_complete), None
|
|
)
|
|
except Exception as ex:
|
|
_ = ex
|
|
return tuple()
|
|
completions = [
|
|
{
|
|
"word": prefix + c.text,
|
|
"pos": pos,
|
|
}
|
|
for c in completions
|
|
]
|
|
return tuple(completions)
|
|
|
|
class ReplEPCCompletionClient(EPCCompletionClient, ReplEPCCompletion):
|
|
def __init__(self, repl, *args, **kargs) -> None:
|
|
EPCCompletionClient.__init__(self, *args, **kargs)
|
|
ReplEPCCompletion.__init__(self, repl)
|
|
|
|
def start_completion_thread(repl: "REPLApplication", epc_port=None) -> threading.Thread:
|
|
if epc_port is None:
|
|
epc_port = os.environ.get("EPC_COMPLETION_SERVER_PORT", None)
|
|
rpc_complete_thread = None
|
|
if epc_port is not None:
|
|
epc_port = int(epc_port)
|
|
rpc_complete = ReplEPCCompletionClient(repl, port=epc_port)
|
|
rpc_complete_thread = threading.Thread(
|
|
target=rpc_complete.connect,
|
|
name="PythonModeEPCCompletion",
|
|
kwargs={"socket_or_address": ("localhost", epc_port)},
|
|
)
|
|
if rpc_complete_thread is not None:
|
|
rpc_complete_thread.daemon = True
|
|
rpc_complete_thread.start()
|
|
return rpc_complete_thread
|
|
|
|
else:
|
|
|
|
def start_completion_thread(repl: "REPLApplication", epc_port=None) -> None:
|
|
# Do nothing as completion-epc is not needed when not running in Emacs.
|
|
_, _ = repl, epc_port
|
|
|
|
|
|
def main() -> None:
|
|
app = REPLApplication()
|
|
app.run()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
try:
|
|
main()
|
|
except KeyboardInterrupt:
|
|
pass
|